
This primarily comes up with annotated `self` parameters in constructors: ```py class C[T]: def __init__(self: C[int]): ... ``` Here, we want infer a specialization of `{T = int}` for a call that hits this overload. Normally when inferring a specialization of a function call, typevars appear in the parameter annotations, and not in the argument types. In this case, this is reversed: we need to verify that the `self` argument (`C[T]`, as we have not yet completed specialization inference) is assignable to the parameter type `C[int]`. To do this, we simply look for a typevar/type in both directions when performing inference, and apply the inferred specialization to argument types as well as parameter types before verifying assignability. As a wrinkle, this exposed that we were not checking subtyping/assignability for function literals correctly. Our function literal representation includes an optional specialization that should be applied to the signature. Before, function literals were considered subtypes of (assignable to) each other only if they were identical Salsa objects. Two function literals with different specializations should still be considered subtypes of (assignable to) each other if those specializations result in the same function signature (typically because the function doesn't use the typevars in the specialization). Closes https://github.com/astral-sh/ty/issues/370 Closes https://github.com/astral-sh/ty/issues/100 Closes https://github.com/astral-sh/ty/issues/258 --------- Co-authored-by: Carl Meyer <carl@astral.sh>
6.7 KiB
Scoping rules for type variables
[environment]
python-version = "3.12"
Most of these tests come from the Scoping rules for type variables section of the typing spec.
Typevar used outside of generic function or class
Typevars may only be used in generic function or class definitions.
from typing import TypeVar
T = TypeVar("T")
# TODO: error
x: T
class C:
# TODO: error
x: T
def f() -> None:
# TODO: error
x: T
Legacy typevar used multiple times
A type variable used in a generic function could be inferred to represent different types in the same code block.
This only applies to typevars defined using the legacy syntax, since the PEP 695 syntax creates a new distinct typevar for each occurrence.
from typing import TypeVar
T = TypeVar("T")
def f1(x: T) -> T:
return x
def f2(x: T) -> T:
return x
f1(1)
f2("a")
Typevar inferred multiple times
A type variable used in a generic function could be inferred to represent different types in the same code block.
This also applies to a single generic function being used multiple times, instantiating the typevar to a different type each time.
def f[T](x: T) -> T:
return x
reveal_type(f(1)) # revealed: Literal[1]
reveal_type(f("a")) # revealed: Literal["a"]
Methods can mention class typevars
A type variable used in a method of a generic class that coincides with one of the variables that parameterize this class is always bound to that variable.
class C[T]:
def m1(self, x: T) -> T:
return x
def m2(self, x: T) -> T:
return x
c: C[int] = C[int]()
c.m1(1)
c.m2(1)
# error: [invalid-argument-type] "Argument to bound method `m2` is incorrect: Expected `int`, found `Literal["string"]`"
c.m2("string")
Functions on generic classes are descriptors
This repeats the tests in the Functions as descriptors test suite, but on a
generic class. This ensures that we are carrying any specializations through the entirety of the
descriptor protocol, which is how self
parameters are bound to instance methods.
from inspect import getattr_static
class C[T]:
def f(self, x: T) -> str:
return "a"
reveal_type(getattr_static(C[int], "f")) # revealed: def f(self, x: int) -> str
reveal_type(getattr_static(C[int], "f").__get__) # revealed: <method-wrapper `__get__` of `f`>
reveal_type(getattr_static(C[int], "f").__get__(None, C[int])) # revealed: def f(self, x: int) -> str
# revealed: bound method C[int].f(x: int) -> str
reveal_type(getattr_static(C[int], "f").__get__(C[int](), C[int]))
reveal_type(C[int].f) # revealed: def f(self, x: int) -> str
reveal_type(C[int]().f) # revealed: bound method C[int].f(x: int) -> str
bound_method = C[int]().f
reveal_type(bound_method.__self__) # revealed: C[int]
reveal_type(bound_method.__func__) # revealed: def f(self, x: int) -> str
reveal_type(C[int]().f(1)) # revealed: str
reveal_type(bound_method(1)) # revealed: str
C[int].f(1) # error: [missing-argument]
reveal_type(C[int].f(C[int](), 1)) # revealed: str
class D[U](C[U]):
pass
reveal_type(D[int]().f) # revealed: bound method D[int].f(x: int) -> str
Methods can mention other typevars
A type variable used in a method that does not match any of the variables that parameterize the class makes this method a generic function in that variable.
from typing import TypeVar, Generic
T = TypeVar("T")
S = TypeVar("S")
class Legacy(Generic[T]):
def m(self, x: T, y: S) -> S:
return y
legacy: Legacy[int] = Legacy()
reveal_type(legacy.m(1, "string")) # revealed: Literal["string"]
With PEP 695 syntax, it is clearer that the method uses a separate typevar:
class C[T]:
def m[S](self, x: T, y: S) -> S:
return y
c: C[int] = C()
reveal_type(c.m(1, "string")) # revealed: Literal["string"]
Unbound typevars
Unbound type variables should not appear in the bodies of generic functions, or in the class bodies apart from method definitions.
This is true with the legacy syntax:
from typing import TypeVar, Generic
T = TypeVar("T")
S = TypeVar("S")
def f(x: T) -> None:
x: list[T] = []
# TODO: invalid-assignment error
y: list[S] = []
class C(Generic[T]):
# TODO: error: cannot use S if it's not in the current generic context
x: list[S] = []
# This is not an error, as shown in the previous test
def m(self, x: S) -> S:
return x
This is true with PEP 695 syntax, as well, though we must use the legacy syntax to define the unbound typevars:
pep695.py
:
from typing import TypeVar
S = TypeVar("S")
def f[T](x: T) -> None:
x: list[T] = []
# TODO: invalid assignment error
y: list[S] = []
class C[T]:
# TODO: error: cannot use S if it's not in the current generic context
x: list[S] = []
def m1(self, x: S) -> S:
return x
def m2[S](self, x: S) -> S:
return x
Nested formal typevars must be distinct
Generic functions and classes can be nested in each other, but it is an error for the same typevar to be used in nested generic definitions.
Note that the typing spec only mentions two specific versions of this rule:
A generic class definition that appears inside a generic function should not use type variables that parameterize the generic function.
and
A generic class nested in another generic class cannot use the same type variables.
We assume that the more general form holds.
Generic function within generic function
def f[T](x: T, y: T) -> None:
def ok[S](a: S, b: S) -> None: ...
# TODO: error
def bad[T](a: T, b: T) -> None: ...
Generic method within generic class
class C[T]:
def ok[S](self, a: S, b: S) -> None: ...
# TODO: error
def bad[T](self, a: T, b: T) -> None: ...
Generic class within generic function
from typing import Iterable
def f[T](x: T, y: T) -> None:
class Ok[S]: ...
# TODO: error for reuse of typevar
class Bad1[T]: ...
# TODO: error for reuse of typevar
class Bad2(Iterable[T]): ...
Generic class within generic class
from typing import Iterable
class C[T]:
class Ok1[S]: ...
# TODO: error for reuse of typevar
class Bad1[T]: ...
# TODO: error for reuse of typevar
class Bad2(Iterable[T]): ...
Class scopes do not cover inner scopes
Just like regular symbols, the typevars of a generic class are only available in that class's scope, and are not available in nested scopes.
class C[T]:
ok1: list[T] = []
class Bad:
# TODO: error: cannot refer to T in nested scope
bad: list[T] = []
class Inner[S]: ...
ok2: Inner[T]