This patch lets us create specializations from a constraint set. The constraint encodes the restrictions on which types each typevar can specialize to. Given a generic context and a constraint set, we iterate through all of the generic context's typevars. For each typevar, we abstract the constraint set so that it only mentions the typevar in question (propagating derived facts if needed). We then find the "best representative type" for the typevar given the abstracted constraint set. When considering the BDD structure of the abstracted constraint set, each path from the BDD root to the `true` terminal represents one way that the constraint set can be satisfied. (This is also one of the clauses in the DNF representation of the constraint set's boolean formula.) Each of those paths is the conjunction of the individual constraints of each internal node that we traverse as we walk that path, giving a single lower/upper bound for the path. We use the upper bound as the "best" (i.e. "closest to `object`") type for that path. If there are multiple paths in the BDD, they technically represent independent possible specializations. If there's a single specialization that satisfies all of them, we will return that as the specialization. If not, then the constraint set is ambiguous. (This happens most often with constrained typevars.) We could in the future turn _each_ of the paths into separate specializations, but it's not clear what we would do with that, so instead we just report the ambiguity as a specialization failure.
6.8 KiB
Generic type aliases: PEP 695 syntax
[environment]
python-version = "3.13"
Defining a generic alias
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 ty_extensions import generic_context
type SingleTypevar[T] = ...
type MultipleTypevars[T, S] = ...
type SingleParamSpec[**P] = ...
type TypeVarAndParamSpec[T, **P] = ...
type SingleTypeVarTuple[*Ts] = ...
type TypeVarAndTypeVarTuple[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 `ParamSpec`/`TypeVarTuple` properly
# (these should include the `ParamSpec`s and `TypeVarTuple`s in their generic contexts)
# revealed: ty_extensions.GenericContext[]
reveal_type(generic_context(SingleParamSpec))
# revealed: ty_extensions.GenericContext[T@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] = ...
Specializing type aliases explicitly
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: [too-many-positional-arguments] "Too many positional arguments: expected 1, got 2"
reveal_type(C[int, int]) # revealed: Unknown
And non-generic types cannot be specialized:
type B = ...
# error: [non-subscriptable] "Cannot subscript non-generic type alias"
reveal_type(B[int]) # revealed: Unknown
# error: [non-subscriptable] "Cannot subscript non-generic type alias"
def _(b: B[int]): ...
If the type variable has an upper bound, the specialized type must satisfy that bound:
type Bounded[T: int] = ...
type BoundedByUnion[T: int | str] = ...
class IntSubclass(int): ...
reveal_type(Bounded[int]) # revealed: Bounded[int]
reveal_type(Bounded[IntSubclass]) # revealed: Bounded[IntSubclass]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `str`"
reveal_type(Bounded[str]) # revealed: Unknown
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument is incorrect: Expected `int`, found `int | str`"
reveal_type(Bounded[int | str]) # revealed: Unknown
reveal_type(BoundedByUnion[int]) # revealed: BoundedByUnion[int]
reveal_type(BoundedByUnion[IntSubclass]) # revealed: BoundedByUnion[IntSubclass]
reveal_type(BoundedByUnion[str]) # revealed: BoundedByUnion[str]
reveal_type(BoundedByUnion[int | str]) # revealed: BoundedByUnion[int | str]
If the type variable is constrained, the specialized type must satisfy those constraints:
type Constrained[T: (int, str)] = ...
reveal_type(Constrained[int]) # revealed: Constrained[int]
# TODO: error: [invalid-argument-type]
# TODO: revealed: Constrained[Unknown]
reveal_type(Constrained[IntSubclass]) # revealed: Constrained[IntSubclass]
reveal_type(Constrained[str]) # revealed: Constrained[str]
# TODO: error: [invalid-argument-type]
# TODO: revealed: Unknown
reveal_type(Constrained[int | str]) # revealed: Constrained[int | str]
# TODO: update this diagnostic to talk about type parameters and specializations
# error: [invalid-argument-type] "Argument is incorrect: Expected `int | str`, found `object`"
reveal_type(Constrained[object]) # revealed: Unknown
If the type variable has a default, it can be omitted:
type WithDefault[T, U = int] = ...
reveal_type(WithDefault[str, str]) # revealed: WithDefault[str, str]
reveal_type(WithDefault[str]) # revealed: 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]
Aliases are not callable
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
Recursive Truthiness
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
Recursive generic type aliases
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]