python / mypy

Optional static typing for Python
https://www.mypy-lang.org/
Other
18.48k stars 2.83k forks source link

PEP 695 variance inferred as simultaneously covariant and contravariant #17709

Closed gschaffner closed 2 months ago

gschaffner commented 2 months ago

Bug Report

When a PEP 695 generic classdef does not contain any methods/etc. that constrain the variance of its generic position, Mypy infers the type variable to be bivariant.

To Reproduce

https://mypy-play.net/?mypy=latest&python=3.12&enable-incomplete-feature=NewGenericSyntax&flags=strict&gist=a82dad9f13f2d8d94c448ea6fa8d327a

class A[T]:
    pass

def typetest_A() -> None:
    # Test variance of position 0.
    upper_0 = A[object]()
    lower_0 = A[int]()
    # Assert covariant in position 0.
    upper_0 = lower_0
    # Assert not contravariant in position 0. (We use `--warn-unused-ignores` to get an
    # error if "assert contravariant" doesn't fail.)
    lower_0 = upper_0  # type: ignore[assignment]
    # ^ Mypy emits unused-ignore for this, i.e. Mypy infers that A[T] is both covariant
    # and contravariant in the position of T.

Expected Behavior

https://typing.readthedocs.io/en/latest/spec/generics.html#variance-inference states that type checkers should infer covariance as the default in this case:

  1. Determine whether lower can be assigned to upper using normal assignability rules. If so, the target type parameter is covariant. If not, determine whether upper can be assigned to lower. If so, the target type parameter is contravariant. If neither of these combinations are assignable, the target type parameter is invariant.

(Personally I would prefer that type checkers did not make this assumption about the programmer's intent and instead https://peps.python.org/pep-0695/#explicit-variance was required[^1] either in this case or in all cases to prevent subtle miscommunications between the programmer and the type system, but AFAIU the spec. does seem clear that checkers should short-circuit and return covariant in this case.)

[^1]: Or if not required, at least possible to explicitly specify in the new PEP 695 syntax, so that programmers could opt in to checks that they are not mistakenly exposing types with an unintended variance.

Actual Behavior

A[T] is inferred as bivariant in the position of T. Technically this is correct, but the spec. says to do something else in this case.

Your Environment

erictraut commented 2 months ago

I think your analysis might be incorrect here. The reason you're not seeing an error generated for the statement lower_0 = upper_0 is that you've previously assigned a value of lower_0 to upper_0, and the type of upper_0 has been narrowed on assignment.

You can see this by inserting a few reveal_type directives.

class A[T]:
    pass

def typetest_A() -> None:
    upper_0 = A[object]()
    lower_0 = A[int]()

    reveal_type(upper_0)  # Reveals A[object]

    upper_0 = lower_0  # OK
    reveal_type(upper_0)  # Reveals A[int]

    lower_0 = upper_0  # OK

If you move the statement lower_0 = upper_0 immediately before the first reveal_type, you'll see that it does produce an error as expected.

So, it looks like mypy is correctly inferring covariance consistent with the spec.

gschaffner commented 2 months ago

Oops :facepalm:, thank you! Sorry for the time waste.