crates/ty_python_semantic/resources/mdtest/generics/pep695/aliases.md
[environment]
python-version = "3.13"
At its simplest, to define a type alias using PEP 695 syntax, you add a list of TypeVars,
ParamSpecs or TypeVarTuples after the alias name.
from typing import Callable
from ty_extensions import generic_context
type SingleTypevar[T] = list[T]
type MultipleTypevars[T, S] = tuple[T, S]
type SingleParamSpec[**P] = Callable[P, int]
type TypeVarAndParamSpec[T, **P] = Callable[P, T]
type SingleTypeVarTuple[*Ts] = tuple[*Ts]
type TypeVarAndTypeVarTuple[T, *Ts] = tuple[T, *Ts]
# revealed: ty_extensions.GenericContext[T@SingleTypevar]
reveal_type(generic_context(SingleTypevar))
# revealed: ty_extensions.GenericContext[T@MultipleTypevars, S@MultipleTypevars]
reveal_type(generic_context(MultipleTypevars))
# TODO: support `TypeVarTuple` properly
# (these should include the `TypeVarTuple`s in their generic contexts)
# revealed: ty_extensions.GenericContext[P@SingleParamSpec]
reveal_type(generic_context(SingleParamSpec))
# revealed: ty_extensions.GenericContext[T@TypeVarAndParamSpec, P@TypeVarAndParamSpec]
reveal_type(generic_context(TypeVarAndParamSpec))
# revealed: ty_extensions.GenericContext[]
reveal_type(generic_context(SingleTypeVarTuple))
# revealed: ty_extensions.GenericContext[T@TypeVarAndTypeVarTuple]
reveal_type(generic_context(TypeVarAndTypeVarTuple))
You cannot use the same typevar more than once.
# error: [invalid-syntax] "duplicate type parameter"
type RepeatedTypevar[T, T] = tuple[T, T]
Legacy type variables cannot be used:
from typing import TypeVar
V = TypeVar("V")
# error: [unbound-type-variable]
type TA1[K] = dict[K, V]
The type parameter can be specified explicitly:
from typing import Literal
type C[T] = T
def _(a: C[int], b: C[Literal[5]]):
reveal_type(a) # revealed: int
reveal_type(b) # revealed: Literal[5]
The specialization must match the generic types:
# error: [invalid-type-arguments] "Too many type arguments: expected 1, got 2"
reveal_type(C[int, int]) # revealed: <type alias 'C[Unknown]'>
And non-generic types cannot be specialized:
from typing import TypeVar, Protocol, TypedDict
type B = int
# error: [not-subscriptable] "Cannot subscript non-generic type alias `B`"
reveal_type(B[int]) # revealed: Unknown
# error: [not-subscriptable] "Cannot specialize non-generic type alias `B`"
def _(b: B[int]):
reveal_type(b) # revealed: Unknown
type IntOrStr = int | str
# error: [not-subscriptable] "Cannot specialize non-generic type alias `IntOrStr`"
def _(c: IntOrStr[int]):
reveal_type(c) # revealed: Unknown
type ListOfInts = list[int]
# error: [not-subscriptable] "Cannot specialize non-generic type alias `ListOfInts`"
def _(l: ListOfInts[int]):
reveal_type(l) # revealed: Unknown
type List[T] = list[T]
# error: [invalid-type-form] "Only simple names and dotted names can be subscripted in parameter annotations"
def _(l: List[int][int]):
reveal_type(l) # revealed: Unknown
# error: [invalid-type-form] "Only simple names and dotted names can be subscripted in type alias values"
type DoubleSpecialization[T] = list[T][T]
def _(d: DoubleSpecialization[int]):
reveal_type(d) # revealed: Unknown
type Tuple = tuple[int, str]
# error: [not-subscriptable] "Cannot specialize non-generic type alias `Tuple`"
def _(doubly_specialized: Tuple[int]):
reveal_type(doubly_specialized) # revealed: Unknown
T = TypeVar("T")
class LegacyProto(Protocol[T]):
pass
type LegacyProtoInt = LegacyProto[int]
# error: [not-subscriptable] "Cannot specialize non-generic type alias `LegacyProtoInt`"
def _(x: LegacyProtoInt[int]):
reveal_type(x) # revealed: Unknown
class Proto[T](Protocol):
pass
type ProtoInt = Proto[int]
# error: [not-subscriptable] "Cannot specialize non-generic type alias `ProtoInt`"
def _(x: ProtoInt[int]):
reveal_type(x) # revealed: Unknown
# TODO: TypedDict is just a function object at runtime, we should emit an error
class LegacyDict(TypedDict[T]):
# error: [unbound-type-variable]
x: T
type LegacyDictInt = LegacyDict[int]
# error: [not-subscriptable] "Cannot specialize non-generic type alias `LegacyDictInt`"
def _(x: LegacyDictInt[int]):
reveal_type(x) # revealed: Unknown
class Dict[T](TypedDict):
x: T
type DictInt = Dict[int]
# error: [not-subscriptable] "Cannot specialize non-generic type alias `DictInt`"
def _(x: DictInt[int]):
reveal_type(x) # revealed: Unknown
type Union = list[str] | list[int]
# error: [not-subscriptable] "Cannot specialize non-generic type alias `Union`"
def _(x: Union[int]):
reveal_type(x) # revealed: Unknown
If the type variable has an upper bound, the specialized type must satisfy that bound:
type Bounded[T: int] = list[T]
type BoundedByUnion[T: int | str] = list[T]
class IntSubclass(int): ...
reveal_type(Bounded[int]) # revealed: <type alias 'Bounded[int]'>
reveal_type(Bounded[IntSubclass]) # revealed: <type alias 'Bounded[IntSubclass]'>
# error: [invalid-type-arguments] "Type `str` is not assignable to upper bound `int` of type variable `T@Bounded`"
reveal_type(Bounded[str]) # revealed: <type alias 'Bounded[Unknown]'>
# error: [invalid-type-arguments] "Type `int | str` is not assignable to upper bound `int` of type variable `T@Bounded`"
reveal_type(Bounded[int | str]) # revealed: <type alias 'Bounded[Unknown]'>
reveal_type(BoundedByUnion[int]) # revealed: <type alias 'BoundedByUnion[int]'>
reveal_type(BoundedByUnion[IntSubclass]) # revealed: <type alias 'BoundedByUnion[IntSubclass]'>
reveal_type(BoundedByUnion[str]) # revealed: <type alias 'BoundedByUnion[str]'>
reveal_type(BoundedByUnion[int | str]) # revealed: <type alias 'BoundedByUnion[int | str]'>
type TupleOfIntAndStr[T: int, U: str] = tuple[T, U]
def _(x: TupleOfIntAndStr[int, str]):
reveal_type(x) # revealed: tuple[int, str]
# error: [invalid-type-arguments] "Type `int` is not assignable to upper bound `str` of type variable `U@TupleOfIntAndStr`"
def _(x: TupleOfIntAndStr[int, int]):
reveal_type(x) # revealed: tuple[int, Unknown]
If the type variable is constrained, the specialized type must satisfy those constraints:
type Constrained[T: (int, str)] = list[T]
reveal_type(Constrained[int]) # revealed: <type alias 'Constrained[int]'>
# TODO: error: [invalid-argument-type]
# TODO: revealed: Constrained[Unknown]
reveal_type(Constrained[IntSubclass]) # revealed: <type alias 'Constrained[IntSubclass]'>
reveal_type(Constrained[str]) # revealed: <type alias 'Constrained[str]'>
# TODO: error: [invalid-argument-type]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]) # revealed: <type alias 'Constrained[int | str]'>
# error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `T@Constrained`"
reveal_type(Constrained[object]) # revealed: <type alias 'Constrained[Unknown]'>
type TupleOfIntOrStr[T: (int, str), U: (int, str)] = tuple[T, U]
def _(x: TupleOfIntOrStr[int, str]):
reveal_type(x) # revealed: tuple[int, str]
# error: [invalid-type-arguments] "Type `object` does not satisfy constraints `int`, `str` of type variable `U@TupleOfIntOrStr`"
def _(x: TupleOfIntOrStr[int, object]):
reveal_type(x) # revealed: tuple[int, Unknown]
If the type variable has a default, it can be omitted:
type WithDefault[T, U = int] = dict[T, U]
reveal_type(WithDefault[str, str]) # revealed: <type alias 'WithDefault[str, str]'>
reveal_type(WithDefault[str]) # revealed: <type alias 'WithDefault[str, int]'>
If the type alias is not specialized explicitly, it is implicitly specialized to Unknown:
type G[T] = list[T]
def _(g: G):
reveal_type(g) # revealed: list[Unknown]
Unless a type default was provided:
type G[T = int] = list[T]
def _(g: G):
reveal_type(g) # revealed: list[int]
Self-referential defaults should not crash type inference:
# error: [cyclic-type-alias-definition] "Cyclic definition of `A`"
type A[T = A] = A[int]
A self-referential default that does not reference itself in the alias body should also not crash, even when the default is evaluated (e.g., by omitting the type argument):
type B[T = B] = list[T]
def _(x: B) -> None:
pass
Mutually-referential defaults (where two type aliases reference each other via their typevar defaults) should also not crash:
type X[T = Y] = list[T]
type Y[U = X] = list[U]
def _(x: X, y: Y) -> None:
pass
Indirect self-references through a chain of type aliases should also not crash:
type P[T = R] = list[T]
type Q[T = P] = list[T]
type R[T = Q] = list[T]
def _(p: P) -> None:
pass
class A: ...
class B[T]: ...
type AliasA = A
type AliasB = B[int]
# snapshot: not-subscriptable
def _(a: AliasA[int]): ...
error[not-subscriptable]: Cannot specialize non-generic type alias `AliasA`
--> src/mdtest_snippet.py:8:10
|
8 | def _(a: AliasA[int]): ...
| ------^^^^^
| |
| Alias to `A`, which is not generic
|
# snapshot: not-subscriptable
def _(b: AliasB[int]): ...
error[not-subscriptable]: Cannot specialize non-generic type alias `AliasB`
--> src/mdtest_snippet.py:10:10
|
10 | def _(b: AliasB[int]): ...
| ------^^^^^
| |
| Alias to `B[int]`, which is already specialized
|
type A = int
type B[T] = T
# error: [call-non-callable] "Object of type `TypeAliasType` is not callable"
reveal_type(A()) # revealed: Unknown
# error: [call-non-callable] "Object of type `GenericAlias` is not callable"
reveal_type(B[int]()) # revealed: Unknown
Make sure we handle cycles correctly when computing the truthiness of a generic type alias:
type X[T: X] = T
def _(x: X):
assert x
type RecursiveList[T] = T | list[RecursiveList[T]]
r1: RecursiveList[int] = 1
r2: RecursiveList[int] = [1, [1, 2, 3]]
# error: [invalid-assignment] "Object of type `Literal["a"]` is not assignable to `RecursiveList[int]`"
r3: RecursiveList[int] = "a"
# error: [invalid-assignment]
r4: RecursiveList[int] = ["a"]
# TODO: this should be an error
r5: RecursiveList[int] = [1, ["a"]]
def _(x: RecursiveList[int]):
if isinstance(x, list):
# TODO: should be `list[RecursiveList[int]]
reveal_type(x[0]) # revealed: int | list[Any]
if isinstance(x, list) and isinstance(x[0], list):
# TODO: should be `list[RecursiveList[int]]`
reveal_type(x[0]) # revealed: list[Any]
Assignment checks respect structural subtyping, i.e. type aliases with the same structure are assignable to each other.
# This is structurally equivalent to RecursiveList[T].
type RecursiveList2[T] = T | list[T | list[RecursiveList[T]]]
# This is not structurally equivalent to RecursiveList[T].
type RecursiveList3[T] = T | list[list[RecursiveList[T]]]
def _(x: RecursiveList[int], y: RecursiveList2[int]):
r1: RecursiveList2[int] = x
# error: [invalid-assignment]
r2: RecursiveList3[int] = x
r3: RecursiveList[int] = y
# error: [invalid-assignment]
r4: RecursiveList3[int] = y
It is also possible to handle divergent type aliases that are not actually have instances.
# The type variable `T` has no meaning here, it's just to make sure it works correctly.
type DivergentList[T] = list[DivergentList[T]]
d1: DivergentList[int] = []
# error: [invalid-assignment]
d2: DivergentList[int] = [1]
# error: [invalid-assignment]
d3: DivergentList[int] = ["a"]
# TODO: this should be an error
d4: DivergentList[int] = [[1]]
def _(x: DivergentList[int]):
d1: DivergentList[int] = [x]
d2: DivergentList[int] = x[0]
A generic function parameter annotated with a PEP 695 type alias that contains a type variable should properly infer the specialization from the argument:
type MyList[T] = list[T]
type MyDict[K, V] = dict[K, V]
def head[T](my_list: MyList[T]) -> T:
return my_list[0]
def get_value[K, V](my_dict: MyDict[K, V], key: K) -> V:
return my_dict[key]
reveal_type(head([1, 2])) # revealed: int
reveal_type(head(["a", "b"])) # revealed: str
d: dict[str, int] = {"a": 1}
reveal_type(get_value(d, "a")) # revealed: int
It also works in the reverse direction, where the type alias is used as the argument type:
type MyList[T] = list[T]
def head[T](l: list[T]) -> T:
return l[0]
def _(x: MyList[int]):
reveal_type(head(x)) # revealed: int
When a PEP 695 type alias expands to a union, bidirectional type inference should still work correctly. The type alias should be expanded to its value type when determining the expected type for specialization inference.
type MaybeList[T] = list[T] | T
def test[X: int](items: list[X]) -> list[X]:
# The annotation MaybeList[str | int] expands to `list[str | int] | str | int`.
# Bidirectional inference should infer list[str | int] from the list() call.
a: MaybeList[str | int] = list(items)
# The revealed type is list[str | int] because that's what list() returns.
reveal_type(a) # revealed: list[str | int]
return items
This also works for more complex cases with multiple generic functions:
type OptionalList[T] = list[T] | None
def copy_list[T](items: list[T]) -> list[T]:
return list(items)
def _(values: list[int]) -> OptionalList[int]:
result: OptionalList[int] = copy_list(values)
# The revealed type is list[int] because that's what copy_list returns.
reveal_type(result) # revealed: list[int]
return result
Union type aliases also work correctly with TypedDict dict literal inference:
from typing import TypedDict
class Person(TypedDict):
name: str
age: int
type MaybePerson = Person | None
def _(p: MaybePerson):
# Dict literal should be inferred as Person, not dict[str, str | int]
x: MaybePerson = {"name": "Alice", "age": 30}
reveal_type(x) # revealed: Person
And with dict() calls in TypedDict context:
from typing import TypedDict
class Dog(TypedDict):
name: str
breed: str
type MaybeDog = Dog | None
def _():
# dict() call with keyword args should be inferred as Dog
animal: MaybeDog = dict(name="Buddy", breed="Labrador")
reveal_type(animal) # revealed: Dog
And with set literal inference:
type MaybeSet[T] = set[T] | T
def _():
# Set literal should be inferred as set[int]
x: MaybeSet[int] = {1, 2, 3}
reveal_type(x) # revealed: set[int]
When two type aliases have the same name but are in different scopes, they should be fully qualified in error messages to distinguish them:
class A:
class B[T]:
pass
type D = list[int]
class C:
class B[T]:
pass
type D = list[str]
def f(b: C.B[C.D]) -> None:
# error: [invalid-assignment] "Object of type `mdtest_snippet.C.B[mdtest_snippet.C.D]` is not assignable to `mdtest_snippet.A.B[mdtest_snippet.A.D]`"
a: A.B[A.D] = b
Type aliases in nested classes should include the full class path:
class Outer1:
class Inner:
type Alias = int
class Outer2:
class Inner:
type Alias = str
def g(x: Outer1.Inner.Alias, y: Outer2.Inner.Alias) -> None:
# error: [invalid-assignment] "Object of type `mdtest_snippet.Outer2.Inner.Alias` is not assignable to `mdtest_snippet.Outer1.Inner.Alias`"
a: Outer1.Inner.Alias = y
Ambiguous generic type aliases should also be fully qualified:
class X:
type GenAlias[T] = list[T]
class Y:
type GenAlias[T] = dict[str, T]
def h(x: X.GenAlias[int], y: Y.GenAlias[int]) -> None:
# error: [invalid-assignment] "Object of type `mdtest_snippet.Y.GenAlias[int]` is not assignable to `mdtest_snippet.X.GenAlias[int]`"
a: X.GenAlias[int] = y
Type aliases with unique names should NOT be qualified:
class P:
type UniqueAlias1 = int
class Q:
type UniqueAlias2 = str
def i(x: P.UniqueAlias1, y: Q.UniqueAlias2) -> None:
# error: [invalid-assignment] "Object of type `UniqueAlias2` is not assignable to `UniqueAlias1`"
a: P.UniqueAlias1 = y
When a class and a type alias have the same name in different scopes, both should be fully qualified to distinguish them in error messages:
class Container1:
class Item:
pass
class Container2:
type Item = str
def j(x: Container1.Item, y: Container2.Item) -> None:
# error: [invalid-assignment] "Object of type `mdtest_snippet.Container2.Item` is not assignable to `mdtest_snippet.Container1.Item`"
a: Container1.Item = y
TypeVarTupleA type parameter with a default cannot follow a TypeVarTuple in a type parameter list. This is
prohibited by the typing spec because a TypeVarTuple consumes all remaining positional type
arguments, making any subsequent defaults meaningless.
# snapshot: invalid-type-variable-default
type Alias1[*Ts, T = int] = tuple[*Ts, T]
error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter
--> src/mdtest_snippet.py:2:13
|
2 | type Alias1[*Ts, T = int] = tuple[*Ts, T]
| --- ^^^^^^^ `T` has a default
| |
| `Ts` is a TypeVarTuple
|
info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple
# snapshot: invalid-type-variable-default
type Alias2[T1, *Ts, T2 = int] = tuple[T1, *Ts, T2]
error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter
--> src/mdtest_snippet.py:4:17
|
4 | type Alias2[T1, *Ts, T2 = int] = tuple[T1, *Ts, T2]
| --- ^^^^^^^^ `T2` has a default
| |
| `Ts` is a TypeVarTuple
|
info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple
# snapshot: invalid-type-variable-default
type Alias3[*Ts, T1 = int, T2 = str] = tuple[*Ts, T1, T2]
error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter
--> src/mdtest_snippet.py:6:13
|
6 | type Alias3[*Ts, T1 = int, T2 = str] = tuple[*Ts, T1, T2]
| --- ^^^^^^^^ -------- `T2` also has a default
| | |
| | `T1` has a default
| `Ts` is a TypeVarTuple
|
info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple
# snapshot: invalid-type-variable-default
type Alias4[*Us, *Ts = *tuple[int, str]] = tuple[*Us, *Ts]
error[invalid-type-variable-default]: Type parameters with defaults cannot follow a TypeVarTuple parameter
--> src/mdtest_snippet.py:8:13
|
8 | type Alias4[*Us, *Ts = *tuple[int, str]] = tuple[*Us, *Ts]
| --- ^^^^^^^^^^^^^^^^^^^^^^ `Ts` has a default
| |
| `Us` is a TypeVarTuple
|
info: See https://typing.python.org/en/latest/spec/generics.html#defaults-following-typevartuple
# These are fine:
type Ok1[T, *Ts] = tuple[T, *Ts]