
## Summary Allow a typevar to be callable if it is bound to a callable type, or constrained to callable types. I spent some time digging into why this support didn't fall out naturally, and ultimately the reason is that we look up `__call__` on the meta type (since its a dunder), and our implementation of `Type::to_meta_type` for `Type::Callable` does not return a type with `__call__`. A more general solution here would be to have `Type::to_meta_type` for `Type::Callable` synthesize a protocol with `__call__` and return an intersection with that protocol (since for a type to be callable, we know its meta-type must have `__call__`). That solution could in principle also replace the special-case handling of `Type::Callable` itself, here in `Type::bindings`. But that more general approach would also be slower, and our protocol support isn't quite ready for that yet, and handling this directly in `Type::bindings` is really not bad. Fixes https://github.com/astral-sh/ty/issues/480 ## Test Plan Added mdtests.
4.9 KiB
Legacy type variables
The tests in this file focus on how type variables are defined using the legacy notation. Most uses of type variables are tested in other files in this directory; we do not duplicate every test for both type variable syntaxes.
Unless otherwise specified, all quotations come from the Generics section of the typing spec.
Type variables
Defining legacy type variables
Generics can be parameterized by using a factory available in
typing
calledTypeVar
.
This was the only way to create type variables prior to PEP 695/Python 3.12. It is still available in newer Python releases.
from typing import TypeVar
T = TypeVar("T")
reveal_type(type(T)) # revealed: <class 'TypeVar'>
reveal_type(T) # revealed: typing.TypeVar
reveal_type(T.__name__) # revealed: Literal["T"]
Directly assigned to a variable
A
TypeVar()
expression must always directly be assigned to a variable (it should not be used as part of a larger expression).
from typing import TypeVar
T = TypeVar("T")
# TODO: no error
# error: [invalid-legacy-type-variable]
U: TypeVar = TypeVar("U")
# error: [invalid-legacy-type-variable] "A legacy `typing.TypeVar` must be immediately assigned to a variable"
# error: [invalid-type-form] "Function calls are not allowed in type expressions"
TestList = list[TypeVar("W")]
TypeVar
parameter must match variable name
The argument to
TypeVar()
must be a string equal to the variable name to which it is assigned.
from typing import TypeVar
# error: [invalid-legacy-type-variable] "The name of a legacy `typing.TypeVar` (`Q`) must match the name of the variable it is assigned to (`T`)"
T = TypeVar("Q")
No redefinition
Type variables must not be redefined.
from typing import TypeVar
T = TypeVar("T")
# TODO: error
T = TypeVar("T")
Type variables with a default
Note that the __default__
property is only available in Python ≥3.13.
[environment]
python-version = "3.13"
from typing import TypeVar
T = TypeVar("T", default=int)
reveal_type(T.__default__) # revealed: int
reveal_type(T.__bound__) # revealed: None
reveal_type(T.__constraints__) # revealed: tuple[()]
S = TypeVar("S")
reveal_type(S.__default__) # revealed: NoDefault
Using other typevars as a default
from typing import Generic, TypeVar, Union
T = TypeVar("T")
U = TypeVar("U", default=T)
V = TypeVar("V", default=Union[T, U])
class Valid(Generic[T, U, V]): ...
reveal_type(Valid()) # revealed: Valid[Unknown, Unknown, Unknown]
reveal_type(Valid[int]()) # revealed: Valid[int, int, int]
reveal_type(Valid[int, str]()) # revealed: Valid[int, str, int | str]
reveal_type(Valid[int, str, None]()) # revealed: Valid[int, str, None]
# TODO: error, default value for U isn't available in the generic context
class Invalid(Generic[U]): ...
Type variables with an upper bound
from typing import TypeVar
T = TypeVar("T", bound=int)
reveal_type(T.__bound__) # revealed: int
reveal_type(T.__constraints__) # revealed: tuple[()]
S = TypeVar("S")
reveal_type(S.__bound__) # revealed: None
Type variables with constraints
from typing import TypeVar
T = TypeVar("T", int, str)
reveal_type(T.__constraints__) # revealed: tuple[int, str]
S = TypeVar("S")
reveal_type(S.__constraints__) # revealed: tuple[()]
Cannot have only one constraint
TypeVar
supports constraining parametric types to a fixed set of possible types...There should be at least two constraints, if any; specifying a single constraint is disallowed.
from typing import TypeVar
# TODO: error: [invalid-type-variable-constraints]
T = TypeVar("T", int)
Cannot be both covariant and contravariant
To facilitate the declaration of container types where covariant or contravariant type checking is acceptable, type variables accept keyword arguments
covariant=True
orcontravariant=True
. At most one of these may be passed.
from typing import TypeVar
# error: [invalid-legacy-type-variable]
T = TypeVar("T", covariant=True, contravariant=True)
Variance parameters must be unambiguous
from typing import TypeVar
def cond() -> bool:
return True
# error: [invalid-legacy-type-variable]
T = TypeVar("T", covariant=cond())
# error: [invalid-legacy-type-variable]
U = TypeVar("U", contravariant=cond())
Callability
A typevar bound to a Callable type is callable:
from typing import Callable, TypeVar
T = TypeVar("T", bound=Callable[[], int])
def bound(f: T):
reveal_type(f) # revealed: T
reveal_type(f()) # revealed: int
Same with a constrained typevar, as long as all constraints are callable:
T = TypeVar("T", Callable[[], int], Callable[[], str])
def constrained(f: T):
reveal_type(f) # revealed: T
reveal_type(f()) # revealed: int | str