mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-28 04:45:01 +00:00
Rename Red Knot (#17820)
This commit is contained in:
parent
e6a798b962
commit
b51c4f82ea
1564 changed files with 1598 additions and 1578 deletions
|
@ -0,0 +1,35 @@
|
|||
# Generic builtins
|
||||
|
||||
## Variadic keyword arguments with a custom `dict`
|
||||
|
||||
When we define `dict` in a custom typeshed, we must take care to define it as a generic class in the
|
||||
same way as in the real typeshed.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
typeshed = "/typeshed"
|
||||
```
|
||||
|
||||
`/typeshed/stdlib/builtins.pyi`:
|
||||
|
||||
```pyi
|
||||
class object: ...
|
||||
class int: ...
|
||||
class dict[K, V, Extra]: ...
|
||||
```
|
||||
|
||||
`/typeshed/stdlib/typing_extensions.pyi`:
|
||||
|
||||
```pyi
|
||||
def reveal_type(obj, /): ...
|
||||
```
|
||||
|
||||
If we don't, then we won't be able to infer the types of variadic keyword arguments correctly.
|
||||
|
||||
```py
|
||||
def f(**kwargs):
|
||||
reveal_type(kwargs) # revealed: Unknown
|
||||
|
||||
def g(**kwargs: int):
|
||||
reveal_type(kwargs) # revealed: Unknown
|
||||
```
|
|
@ -0,0 +1,443 @@
|
|||
# Generic classes: Legacy syntax
|
||||
|
||||
## Defining a generic class
|
||||
|
||||
At its simplest, to define a generic class using the legacy syntax, you inherit from the
|
||||
`typing.Generic` special form, which is "specialized" with the generic class's type variables.
|
||||
|
||||
```py
|
||||
from ty_extensions import generic_context
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
S = TypeVar("S")
|
||||
|
||||
class SingleTypevar(Generic[T]): ...
|
||||
class MultipleTypevars(Generic[T, S]): ...
|
||||
|
||||
reveal_type(generic_context(SingleTypevar)) # revealed: tuple[T]
|
||||
reveal_type(generic_context(MultipleTypevars)) # revealed: tuple[T, S]
|
||||
```
|
||||
|
||||
You cannot use the same typevar more than once.
|
||||
|
||||
```py
|
||||
# TODO: error
|
||||
class RepeatedTypevar(Generic[T, T]): ...
|
||||
```
|
||||
|
||||
You can only specialize `typing.Generic` with typevars (TODO: or param specs or typevar tuples).
|
||||
|
||||
```py
|
||||
# error: [invalid-argument-type] "`Literal[int]` is not a valid argument to `typing.Generic`"
|
||||
class GenericOfType(Generic[int]): ...
|
||||
```
|
||||
|
||||
You can also define a generic class by inheriting from some _other_ generic class, and specializing
|
||||
it with typevars.
|
||||
|
||||
```py
|
||||
class InheritedGeneric(MultipleTypevars[T, S]): ...
|
||||
class InheritedGenericPartiallySpecialized(MultipleTypevars[T, int]): ...
|
||||
class InheritedGenericFullySpecialized(MultipleTypevars[str, int]): ...
|
||||
|
||||
reveal_type(generic_context(InheritedGeneric)) # revealed: tuple[T, S]
|
||||
reveal_type(generic_context(InheritedGenericPartiallySpecialized)) # revealed: tuple[T]
|
||||
reveal_type(generic_context(InheritedGenericFullySpecialized)) # revealed: None
|
||||
```
|
||||
|
||||
If you don't specialize a generic base class, we use the default specialization, which maps each
|
||||
typevar to its default value or `Any`. Since that base class is fully specialized, it does not make
|
||||
the inheriting class generic.
|
||||
|
||||
```py
|
||||
class InheritedGenericDefaultSpecialization(MultipleTypevars): ...
|
||||
|
||||
reveal_type(generic_context(InheritedGenericDefaultSpecialization)) # revealed: None
|
||||
```
|
||||
|
||||
When inheriting from a generic class, you can optionally inherit from `typing.Generic` as well. But
|
||||
if you do, you have to mention all of the typevars that you use in your other base classes.
|
||||
|
||||
```py
|
||||
class ExplicitInheritedGeneric(MultipleTypevars[T, S], Generic[T, S]): ...
|
||||
|
||||
# error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes"
|
||||
class ExplicitInheritedGenericMissingTypevar(MultipleTypevars[T, S], Generic[T]): ...
|
||||
class ExplicitInheritedGenericPartiallySpecialized(MultipleTypevars[T, int], Generic[T]): ...
|
||||
class ExplicitInheritedGenericPartiallySpecializedExtraTypevar(MultipleTypevars[T, int], Generic[T, S]): ...
|
||||
|
||||
# error: [invalid-generic-class] "`Generic` base class must include all type variables used in other base classes"
|
||||
class ExplicitInheritedGenericPartiallySpecializedMissingTypevar(MultipleTypevars[T, int], Generic[S]): ...
|
||||
|
||||
reveal_type(generic_context(ExplicitInheritedGeneric)) # revealed: tuple[T, S]
|
||||
reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecialized)) # revealed: tuple[T]
|
||||
reveal_type(generic_context(ExplicitInheritedGenericPartiallySpecializedExtraTypevar)) # revealed: tuple[T, S]
|
||||
```
|
||||
|
||||
## Specializing generic classes explicitly
|
||||
|
||||
The type parameter can be specified explicitly:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
x: T
|
||||
|
||||
reveal_type(C[int]()) # revealed: C[int]
|
||||
```
|
||||
|
||||
The specialization must match the generic types:
|
||||
|
||||
```py
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2"
|
||||
reveal_type(C[int, int]()) # revealed: Unknown
|
||||
```
|
||||
|
||||
If the type variable has an upper bound, the specialized type must satisfy that bound:
|
||||
|
||||
```py
|
||||
from typing import Union
|
||||
|
||||
BoundedT = TypeVar("BoundedT", bound=int)
|
||||
BoundedByUnionT = TypeVar("BoundedByUnionT", bound=Union[int, str])
|
||||
|
||||
class Bounded(Generic[BoundedT]): ...
|
||||
class BoundedByUnion(Generic[BoundedByUnionT]): ...
|
||||
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 to this function 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 to this function 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:
|
||||
|
||||
```py
|
||||
ConstrainedT = TypeVar("ConstrainedT", int, str)
|
||||
|
||||
class Constrained(Generic[ConstrainedT]): ...
|
||||
|
||||
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 to this function is incorrect: Expected `int | str`, found `object`"
|
||||
reveal_type(Constrained[object]()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Inferring generic class parameters
|
||||
|
||||
We can infer the type parameter from a type context:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
x: T
|
||||
|
||||
c: C[int] = C()
|
||||
# TODO: revealed: C[int]
|
||||
reveal_type(c) # revealed: C[Unknown]
|
||||
```
|
||||
|
||||
The typevars of a fully specialized generic class should no longer be visible:
|
||||
|
||||
```py
|
||||
# TODO: revealed: int
|
||||
reveal_type(c.x) # revealed: Unknown
|
||||
```
|
||||
|
||||
If the type parameter is not specified explicitly, and there are no constraints that let us infer a
|
||||
specific type, we infer the typevar's default type:
|
||||
|
||||
```py
|
||||
DefaultT = TypeVar("DefaultT", default=int)
|
||||
|
||||
class D(Generic[DefaultT]): ...
|
||||
|
||||
reveal_type(D()) # revealed: D[int]
|
||||
```
|
||||
|
||||
If a typevar does not provide a default, we use `Unknown`:
|
||||
|
||||
```py
|
||||
reveal_type(C()) # revealed: C[Unknown]
|
||||
```
|
||||
|
||||
## Inferring generic class parameters from constructors
|
||||
|
||||
If the type of a constructor parameter is a class typevar, we can use that to infer the type
|
||||
parameter. The types inferred from a type context and from a constructor parameter must be
|
||||
consistent with each other.
|
||||
|
||||
### `__new__` only
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
def __new__(cls, x: T) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
```
|
||||
|
||||
### `__init__` only
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
```
|
||||
|
||||
### Identical `__new__` and `__init__` signatures
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
def __new__(cls, x: T) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
```
|
||||
|
||||
### Compatible `__new__` and `__init__` signatures
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
def __new__(cls, *args, **kwargs) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
|
||||
class D(Generic[T]):
|
||||
def __new__(cls, x: T) -> "D[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None: ...
|
||||
|
||||
reveal_type(D(1)) # revealed: D[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `D[Literal["five"]]` is not assignable to `D[int]`"
|
||||
wrong_innards: D[int] = D("five")
|
||||
```
|
||||
|
||||
### Both present, `__new__` inherited from a generic base class
|
||||
|
||||
If either method comes from a generic base class, we don't currently use its inferred specialization
|
||||
to specialize the class.
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
V = TypeVar("V")
|
||||
|
||||
class C(Generic[T, U]):
|
||||
def __new__(cls, *args, **kwargs) -> "C[T, U]":
|
||||
return object.__new__(cls)
|
||||
|
||||
class D(C[V, int]):
|
||||
def __init__(self, x: V) -> None: ...
|
||||
|
||||
reveal_type(D(1)) # revealed: D[Literal[1]]
|
||||
```
|
||||
|
||||
### `__init__` is itself generic
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
S = TypeVar("S")
|
||||
T = TypeVar("T")
|
||||
|
||||
class C(Generic[T]):
|
||||
def __init__(self, x: T, y: S) -> None: ...
|
||||
|
||||
reveal_type(C(1, 1)) # revealed: C[Literal[1]]
|
||||
reveal_type(C(1, "string")) # revealed: C[Literal[1]]
|
||||
reveal_type(C(1, True)) # revealed: C[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five", 1)
|
||||
```
|
||||
|
||||
## Generic subclass
|
||||
|
||||
When a generic subclass fills its superclass's type parameter with one of its own, the actual types
|
||||
propagate through:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Base(Generic[T]):
|
||||
x: T | None = None
|
||||
|
||||
class ExplicitlyGenericSub(Base[T], Generic[T]): ...
|
||||
class ImplicitlyGenericSub(Base[T]): ...
|
||||
|
||||
reveal_type(Base[int].x) # revealed: int | None
|
||||
reveal_type(ExplicitlyGenericSub[int].x) # revealed: int | None
|
||||
reveal_type(ImplicitlyGenericSub[int].x) # revealed: int | None
|
||||
```
|
||||
|
||||
## Generic methods
|
||||
|
||||
Generic classes can contain methods that are themselves generic. The generic methods can refer to
|
||||
the typevars of the enclosing generic class, and introduce new (distinct) typevars that are only in
|
||||
scope for the method.
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
U = TypeVar("U")
|
||||
|
||||
class C(Generic[T]):
|
||||
def method(self, u: U) -> U:
|
||||
return u
|
||||
|
||||
c: C[int] = C[int]()
|
||||
reveal_type(c.method("string")) # revealed: Literal["string"]
|
||||
```
|
||||
|
||||
## Cyclic class definitions
|
||||
|
||||
### F-bounded quantification
|
||||
|
||||
A class can use itself as the type parameter of one of its superclasses. (This is also known as the
|
||||
[curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].)
|
||||
|
||||
#### In a stub file
|
||||
|
||||
Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself).
|
||||
|
||||
```pyi
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Base(Generic[T]): ...
|
||||
class Sub(Base[Sub]): ...
|
||||
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
```
|
||||
|
||||
#### With string forward references
|
||||
|
||||
A similar case can work in a non-stub file, if forward references are stringified:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Base(Generic[T]): ...
|
||||
class Sub(Base["Sub"]): ...
|
||||
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
```
|
||||
|
||||
#### Without string forward references
|
||||
|
||||
In a non-stub file, without stringified forward references, this raises a `NameError`:
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Base(Generic[T]): ...
|
||||
|
||||
# error: [unresolved-reference]
|
||||
class Sub(Base[Sub]): ...
|
||||
```
|
||||
|
||||
### Cyclic inheritance as a generic parameter
|
||||
|
||||
```pyi
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
class Derived(list[Derived[T]], Generic[T]): ...
|
||||
```
|
||||
|
||||
### Direct cyclic inheritance
|
||||
|
||||
Inheritance that would result in a cyclic MRO is detected as an error.
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# error: [unresolved-reference]
|
||||
class C(C, Generic[T]): ...
|
||||
|
||||
# error: [unresolved-reference]
|
||||
class D(D[int], Generic[T]): ...
|
||||
```
|
||||
|
||||
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
|
||||
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification
|
|
@ -0,0 +1,272 @@
|
|||
# Generic functions: Legacy syntax
|
||||
|
||||
## Typevar must be used at least twice
|
||||
|
||||
If you're only using a typevar for a single parameter, you don't need the typevar — just use
|
||||
`object` (or the typevar's upper bound):
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# TODO: error, should be (x: object)
|
||||
def typevar_not_needed(x: T) -> None:
|
||||
pass
|
||||
|
||||
BoundedT = TypeVar("BoundedT", bound=int)
|
||||
|
||||
# TODO: error, should be (x: int)
|
||||
def bounded_typevar_not_needed(x: BoundedT) -> None:
|
||||
pass
|
||||
```
|
||||
|
||||
Typevars are only needed if you use them more than once. For instance, to specify that two
|
||||
parameters must both have the same type:
|
||||
|
||||
```py
|
||||
def two_params(x: T, y: T) -> T:
|
||||
return x
|
||||
```
|
||||
|
||||
or to specify that a return value is the same as a parameter:
|
||||
|
||||
```py
|
||||
def return_value(x: T) -> T:
|
||||
return x
|
||||
```
|
||||
|
||||
Each typevar must also appear _somewhere_ in the parameter list:
|
||||
|
||||
```py
|
||||
def absurd() -> T:
|
||||
# There's no way to construct a T!
|
||||
raise ValueError("absurd")
|
||||
```
|
||||
|
||||
## Inferring generic function parameter types
|
||||
|
||||
If the type of a generic function parameter is a typevar, then we can infer what type that typevar
|
||||
is bound to at each call site.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def f(x: T) -> T:
|
||||
return x
|
||||
|
||||
reveal_type(f(1)) # revealed: Literal[1]
|
||||
reveal_type(f(1.0)) # revealed: float
|
||||
reveal_type(f(True)) # revealed: Literal[True]
|
||||
reveal_type(f("string")) # revealed: Literal["string"]
|
||||
```
|
||||
|
||||
## Inferring “deep” generic parameter types
|
||||
|
||||
The matching up of call arguments and discovery of constraints on typevars can be a recursive
|
||||
process for arbitrarily-nested generic types in parameters.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def f(x: list[T]) -> T:
|
||||
return x[0]
|
||||
|
||||
# TODO: revealed: float
|
||||
reveal_type(f([1.0, 2.0])) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Inferring a bound typevar
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
T = TypeVar("T", bound=int)
|
||||
|
||||
def f(x: T) -> T:
|
||||
return x
|
||||
|
||||
reveal_type(f(1)) # revealed: Literal[1]
|
||||
reveal_type(f(True)) # revealed: Literal[True]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(f("string")) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Inferring a constrained typevar
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
T = TypeVar("T", int, None)
|
||||
|
||||
def f(x: T) -> T:
|
||||
return x
|
||||
|
||||
reveal_type(f(1)) # revealed: int
|
||||
reveal_type(f(True)) # revealed: int
|
||||
reveal_type(f(None)) # revealed: None
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(f("string")) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Typevar constraints
|
||||
|
||||
If a type parameter has an upper bound, that upper bound constrains which types can be used for that
|
||||
typevar. This effectively adds the upper bound as an intersection to every appearance of the typevar
|
||||
in the function.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T", bound=int)
|
||||
|
||||
def good_param(x: T) -> None:
|
||||
reveal_type(x) # revealed: T
|
||||
```
|
||||
|
||||
If the function is annotated as returning the typevar, this means that the upper bound is _not_
|
||||
assignable to that typevar, since return types are contravariant. In `bad`, we can infer that
|
||||
`x + 1` has type `int`. But `T` might be instantiated with a narrower type than `int`, and so the
|
||||
return value is not guaranteed to be compatible for all `T: int`.
|
||||
|
||||
```py
|
||||
def good_return(x: T) -> T:
|
||||
return x
|
||||
|
||||
def bad_return(x: T) -> T:
|
||||
# error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `int`"
|
||||
return x + 1
|
||||
```
|
||||
|
||||
## All occurrences of the same typevar have the same type
|
||||
|
||||
If a typevar appears multiple times in a function signature, all occurrences have the same type.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
S = TypeVar("S")
|
||||
|
||||
def different_types(cond: bool, t: T, s: S) -> T:
|
||||
if cond:
|
||||
return t
|
||||
else:
|
||||
# error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `S`"
|
||||
return s
|
||||
|
||||
def same_types(cond: bool, t1: T, t2: T) -> T:
|
||||
if cond:
|
||||
return t1
|
||||
else:
|
||||
return t2
|
||||
```
|
||||
|
||||
## All occurrences of the same constrained typevar have the same type
|
||||
|
||||
The above is true even when the typevars are constrained. Here, both `int` and `str` have `__add__`
|
||||
methods that are compatible with the return type, so the `return` expression is always well-typed:
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T", int, str)
|
||||
|
||||
def same_constrained_types(t1: T, t2: T) -> T:
|
||||
# TODO: no error
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `T`"
|
||||
return t1 + t2
|
||||
```
|
||||
|
||||
This is _not_ the same as a union type, because of this additional constraint that the two
|
||||
occurrences have the same type. In `unions_are_different`, `t1` and `t2` might have different types,
|
||||
and an `int` and a `str` cannot be added together:
|
||||
|
||||
```py
|
||||
def unions_are_different(t1: int | str, t2: int | str) -> int | str:
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`"
|
||||
return t1 + t2
|
||||
```
|
||||
|
||||
## Typevar inference is a unification problem
|
||||
|
||||
When inferring typevar assignments in a generic function call, we cannot simply solve constraints
|
||||
eagerly for each parameter in turn. We must solve a unification problem involving all of the
|
||||
parameters simultaneously.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def two_params(x: T, y: T) -> T:
|
||||
return x
|
||||
|
||||
reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"]
|
||||
reveal_type(two_params("a", 1)) # revealed: Literal["a", 1]
|
||||
```
|
||||
|
||||
When one of the parameters is a union, we attempt to find the smallest specialization that satisfies
|
||||
all of the constraints.
|
||||
|
||||
```py
|
||||
def union_param(x: T | None) -> T:
|
||||
if x is None:
|
||||
raise ValueError
|
||||
return x
|
||||
|
||||
reveal_type(union_param("a")) # revealed: Literal["a"]
|
||||
reveal_type(union_param(1)) # revealed: Literal[1]
|
||||
reveal_type(union_param(None)) # revealed: Unknown
|
||||
```
|
||||
|
||||
```py
|
||||
def union_and_nonunion_params(x: T | int, y: T) -> T:
|
||||
return y
|
||||
|
||||
reveal_type(union_and_nonunion_params(1, "a")) # revealed: Literal["a"]
|
||||
reveal_type(union_and_nonunion_params("a", "a")) # revealed: Literal["a"]
|
||||
reveal_type(union_and_nonunion_params(1, 1)) # revealed: Literal[1]
|
||||
reveal_type(union_and_nonunion_params(3, 1)) # revealed: Literal[1]
|
||||
reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1]
|
||||
```
|
||||
|
||||
```py
|
||||
S = TypeVar("S")
|
||||
|
||||
def tuple_param(x: T | S, y: tuple[T, S]) -> tuple[T, S]:
|
||||
return y
|
||||
|
||||
reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
|
||||
reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
|
||||
```
|
||||
|
||||
## Inferring nested generic function calls
|
||||
|
||||
We can infer type assignments in nested calls to multiple generic functions. If they use the same
|
||||
type variable, we do not confuse the two; `T@f` and `T@g` have separate types in each example below.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
def f(x: T) -> tuple[T, int]:
|
||||
return (x, 1)
|
||||
|
||||
def g(x: T) -> T | None:
|
||||
return x
|
||||
|
||||
reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int]
|
||||
reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None
|
||||
```
|
|
@ -0,0 +1,126 @@
|
|||
# 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` called `TypeVar`.
|
||||
|
||||
This was the only way to create type variables prior to PEP 695/Python 3.12. It is still available
|
||||
in newer Python releases.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
reveal_type(type(T)) # revealed: Literal[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).
|
||||
|
||||
```py
|
||||
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"
|
||||
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.
|
||||
|
||||
```py
|
||||
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.
|
||||
|
||||
```py
|
||||
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.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
```py
|
||||
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
|
||||
```
|
||||
|
||||
### Type variables with an upper bound
|
||||
|
||||
```py
|
||||
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
|
||||
|
||||
```py
|
||||
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.
|
||||
|
||||
```py
|
||||
from typing import TypeVar
|
||||
|
||||
# TODO: error: [invalid-type-variable-constraints]
|
||||
T = TypeVar("T", int)
|
||||
```
|
||||
|
||||
[generics]: https://typing.python.org/en/latest/spec/generics.html
|
|
@ -0,0 +1,367 @@
|
|||
# Generic classes: PEP 695 syntax
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
## Defining a generic class
|
||||
|
||||
At its simplest, to define a generic class using PEP 695 syntax, you add a list of typevars after
|
||||
the class name.
|
||||
|
||||
```py
|
||||
from ty_extensions import generic_context
|
||||
|
||||
class SingleTypevar[T]: ...
|
||||
class MultipleTypevars[T, S]: ...
|
||||
|
||||
reveal_type(generic_context(SingleTypevar)) # revealed: tuple[T]
|
||||
reveal_type(generic_context(MultipleTypevars)) # revealed: tuple[T, S]
|
||||
```
|
||||
|
||||
You cannot use the same typevar more than once.
|
||||
|
||||
```py
|
||||
# error: [invalid-syntax] "duplicate type parameter"
|
||||
class RepeatedTypevar[T, T]: ...
|
||||
```
|
||||
|
||||
You can only use typevars (TODO: or param specs or typevar tuples) in the class's generic context.
|
||||
|
||||
```py
|
||||
# TODO: error
|
||||
class GenericOfType[int]: ...
|
||||
```
|
||||
|
||||
You can also define a generic class by inheriting from some _other_ generic class, and specializing
|
||||
it with typevars. With PEP 695 syntax, you must explicitly list all of the typevars that you use in
|
||||
your base classes.
|
||||
|
||||
```py
|
||||
class InheritedGeneric[U, V](MultipleTypevars[U, V]): ...
|
||||
class InheritedGenericPartiallySpecialized[U](MultipleTypevars[U, int]): ...
|
||||
class InheritedGenericFullySpecialized(MultipleTypevars[str, int]): ...
|
||||
|
||||
reveal_type(generic_context(InheritedGeneric)) # revealed: tuple[U, V]
|
||||
reveal_type(generic_context(InheritedGenericPartiallySpecialized)) # revealed: tuple[U]
|
||||
reveal_type(generic_context(InheritedGenericFullySpecialized)) # revealed: None
|
||||
```
|
||||
|
||||
If you don't specialize a generic base class, we use the default specialization, which maps each
|
||||
typevar to its default value or `Any`. Since that base class is fully specialized, it does not make
|
||||
the inheriting class generic.
|
||||
|
||||
```py
|
||||
class InheritedGenericDefaultSpecialization(MultipleTypevars): ...
|
||||
|
||||
reveal_type(generic_context(InheritedGenericDefaultSpecialization)) # revealed: None
|
||||
```
|
||||
|
||||
You cannot use PEP-695 syntax and the legacy syntax in the same class definition.
|
||||
|
||||
```py
|
||||
from typing import Generic, TypeVar
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
# error: [invalid-generic-class] "Cannot both inherit from `Generic` and use PEP 695 type variables"
|
||||
class BothGenericSyntaxes[U](Generic[T]): ...
|
||||
```
|
||||
|
||||
## Specializing generic classes explicitly
|
||||
|
||||
The type parameter can be specified explicitly:
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
reveal_type(C[int]()) # revealed: C[int]
|
||||
```
|
||||
|
||||
The specialization must match the generic types:
|
||||
|
||||
```py
|
||||
# error: [too-many-positional-arguments] "Too many positional arguments to class `C`: expected 1, got 2"
|
||||
reveal_type(C[int, int]()) # revealed: Unknown
|
||||
```
|
||||
|
||||
If the type variable has an upper bound, the specialized type must satisfy that bound:
|
||||
|
||||
```py
|
||||
class Bounded[T: int]: ...
|
||||
class 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 to this function 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 to this function 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:
|
||||
|
||||
```py
|
||||
class 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 to this function is incorrect: Expected `int | str`, found `object`"
|
||||
reveal_type(Constrained[object]()) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Inferring generic class parameters
|
||||
|
||||
We can infer the type parameter from a type context:
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
x: T
|
||||
|
||||
c: C[int] = C()
|
||||
# TODO: revealed: C[int]
|
||||
reveal_type(c) # revealed: C[Unknown]
|
||||
```
|
||||
|
||||
The typevars of a fully specialized generic class should no longer be visible:
|
||||
|
||||
```py
|
||||
# TODO: revealed: int
|
||||
reveal_type(c.x) # revealed: Unknown
|
||||
```
|
||||
|
||||
If the type parameter is not specified explicitly, and there are no constraints that let us infer a
|
||||
specific type, we infer the typevar's default type:
|
||||
|
||||
```py
|
||||
class D[T = int]: ...
|
||||
|
||||
reveal_type(D()) # revealed: D[int]
|
||||
```
|
||||
|
||||
If a typevar does not provide a default, we use `Unknown`:
|
||||
|
||||
```py
|
||||
reveal_type(C()) # revealed: C[Unknown]
|
||||
```
|
||||
|
||||
## Inferring generic class parameters from constructors
|
||||
|
||||
If the type of a constructor parameter is a class typevar, we can use that to infer the type
|
||||
parameter. The types inferred from a type context and from a constructor parameter must be
|
||||
consistent with each other.
|
||||
|
||||
### `__new__` only
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
def __new__(cls, x: T) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
```
|
||||
|
||||
### `__init__` only
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
```
|
||||
|
||||
### Identical `__new__` and `__init__` signatures
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
def __new__(cls, x: T) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
```
|
||||
|
||||
### Compatible `__new__` and `__init__` signatures
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
def __new__(cls, *args, **kwargs) -> "C[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, x: T) -> None: ...
|
||||
|
||||
reveal_type(C(1)) # revealed: C[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five")
|
||||
|
||||
class D[T]:
|
||||
def __new__(cls, x: T) -> "D[T]":
|
||||
return object.__new__(cls)
|
||||
|
||||
def __init__(self, *args, **kwargs) -> None: ...
|
||||
|
||||
reveal_type(D(1)) # revealed: D[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `D[Literal["five"]]` is not assignable to `D[int]`"
|
||||
wrong_innards: D[int] = D("five")
|
||||
```
|
||||
|
||||
### Both present, `__new__` inherited from a generic base class
|
||||
|
||||
If either method comes from a generic base class, we don't currently use its inferred specialization
|
||||
to specialize the class.
|
||||
|
||||
```py
|
||||
class C[T, U]:
|
||||
def __new__(cls, *args, **kwargs) -> "C[T, U]":
|
||||
return object.__new__(cls)
|
||||
|
||||
class D[V](C[V, int]):
|
||||
def __init__(self, x: V) -> None: ...
|
||||
|
||||
reveal_type(D(1)) # revealed: D[Literal[1]]
|
||||
```
|
||||
|
||||
### `__init__` is itself generic
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
def __init__[S](self, x: T, y: S) -> None: ...
|
||||
|
||||
reveal_type(C(1, 1)) # revealed: C[Literal[1]]
|
||||
reveal_type(C(1, "string")) # revealed: C[Literal[1]]
|
||||
reveal_type(C(1, True)) # revealed: C[Literal[1]]
|
||||
|
||||
# error: [invalid-assignment] "Object of type `C[Literal["five"]]` is not assignable to `C[int]`"
|
||||
wrong_innards: C[int] = C("five", 1)
|
||||
```
|
||||
|
||||
## Generic subclass
|
||||
|
||||
When a generic subclass fills its superclass's type parameter with one of its own, the actual types
|
||||
propagate through:
|
||||
|
||||
```py
|
||||
class Base[T]:
|
||||
x: T | None = None
|
||||
|
||||
class Sub[U](Base[U]): ...
|
||||
|
||||
reveal_type(Base[int].x) # revealed: int | None
|
||||
reveal_type(Sub[int].x) # revealed: int | None
|
||||
```
|
||||
|
||||
## Generic methods
|
||||
|
||||
Generic classes can contain methods that are themselves generic. The generic methods can refer to
|
||||
the typevars of the enclosing generic class, and introduce new (distinct) typevars that are only in
|
||||
scope for the method.
|
||||
|
||||
```py
|
||||
class C[T]:
|
||||
def method[U](self, u: U) -> U:
|
||||
return u
|
||||
# error: [unresolved-reference]
|
||||
def cannot_use_outside_of_method(self, u: U): ...
|
||||
|
||||
# TODO: error
|
||||
def cannot_shadow_class_typevar[T](self, t: T): ...
|
||||
|
||||
c: C[int] = C[int]()
|
||||
reveal_type(c.method("string")) # revealed: Literal["string"]
|
||||
```
|
||||
|
||||
## Cyclic class definitions
|
||||
|
||||
### F-bounded quantification
|
||||
|
||||
A class can use itself as the type parameter of one of its superclasses. (This is also known as the
|
||||
[curiously recurring template pattern][crtp] or [F-bounded quantification][f-bound].)
|
||||
|
||||
#### In a stub file
|
||||
|
||||
Here, `Sub` is not a generic class, since it fills its superclass's type parameter (with itself).
|
||||
|
||||
```pyi
|
||||
class Base[T]: ...
|
||||
class Sub(Base[Sub]): ...
|
||||
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
```
|
||||
|
||||
#### With string forward references
|
||||
|
||||
A similar case can work in a non-stub file, if forward references are stringified:
|
||||
|
||||
```py
|
||||
class Base[T]: ...
|
||||
class Sub(Base["Sub"]): ...
|
||||
|
||||
reveal_type(Sub) # revealed: Literal[Sub]
|
||||
```
|
||||
|
||||
#### Without string forward references
|
||||
|
||||
In a non-stub file, without stringified forward references, this raises a `NameError`:
|
||||
|
||||
```py
|
||||
class Base[T]: ...
|
||||
|
||||
# error: [unresolved-reference]
|
||||
class Sub(Base[Sub]): ...
|
||||
```
|
||||
|
||||
### Cyclic inheritance as a generic parameter
|
||||
|
||||
```pyi
|
||||
class Derived[T](list[Derived[T]]): ...
|
||||
```
|
||||
|
||||
### Direct cyclic inheritance
|
||||
|
||||
Inheritance that would result in a cyclic MRO is detected as an error.
|
||||
|
||||
```py
|
||||
# error: [cyclic-class-definition]
|
||||
class C[T](C): ...
|
||||
|
||||
# error: [cyclic-class-definition]
|
||||
class D[T](D[int]): ...
|
||||
```
|
||||
|
||||
[crtp]: https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern
|
||||
[f-bound]: https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification
|
|
@ -0,0 +1,289 @@
|
|||
# Generic functions: PEP 695 syntax
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
## Typevar must be used at least twice
|
||||
|
||||
If you're only using a typevar for a single parameter, you don't need the typevar — just use
|
||||
`object` (or the typevar's upper bound):
|
||||
|
||||
```py
|
||||
# TODO: error, should be (x: object)
|
||||
def typevar_not_needed[T](x: T) -> None:
|
||||
pass
|
||||
|
||||
# TODO: error, should be (x: int)
|
||||
def bounded_typevar_not_needed[T: int](x: T) -> None:
|
||||
pass
|
||||
```
|
||||
|
||||
Typevars are only needed if you use them more than once. For instance, to specify that two
|
||||
parameters must both have the same type:
|
||||
|
||||
```py
|
||||
def two_params[T](x: T, y: T) -> T:
|
||||
return x
|
||||
```
|
||||
|
||||
or to specify that a return value is the same as a parameter:
|
||||
|
||||
```py
|
||||
def return_value[T](x: T) -> T:
|
||||
return x
|
||||
```
|
||||
|
||||
Each typevar must also appear _somewhere_ in the parameter list:
|
||||
|
||||
```py
|
||||
def absurd[T]() -> T:
|
||||
# There's no way to construct a T!
|
||||
raise ValueError("absurd")
|
||||
```
|
||||
|
||||
## Inferring generic function parameter types
|
||||
|
||||
If the type of a generic function parameter is a typevar, then we can infer what type that typevar
|
||||
is bound to at each call site.
|
||||
|
||||
```py
|
||||
def f[T](x: T) -> T:
|
||||
return x
|
||||
|
||||
reveal_type(f(1)) # revealed: Literal[1]
|
||||
reveal_type(f(1.0)) # revealed: float
|
||||
reveal_type(f(True)) # revealed: Literal[True]
|
||||
reveal_type(f("string")) # revealed: Literal["string"]
|
||||
```
|
||||
|
||||
## Inferring “deep” generic parameter types
|
||||
|
||||
The matching up of call arguments and discovery of constraints on typevars can be a recursive
|
||||
process for arbitrarily-nested generic types in parameters.
|
||||
|
||||
```py
|
||||
def f[T](x: list[T]) -> T:
|
||||
return x[0]
|
||||
|
||||
# TODO: revealed: float
|
||||
reveal_type(f([1.0, 2.0])) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Inferring a bound typevar
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
def f[T: int](x: T) -> T:
|
||||
return x
|
||||
|
||||
reveal_type(f(1)) # revealed: Literal[1]
|
||||
reveal_type(f(True)) # revealed: Literal[True]
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(f("string")) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Inferring a constrained typevar
|
||||
|
||||
<!-- snapshot-diagnostics -->
|
||||
|
||||
```py
|
||||
from typing_extensions import reveal_type
|
||||
|
||||
def f[T: (int, None)](x: T) -> T:
|
||||
return x
|
||||
|
||||
reveal_type(f(1)) # revealed: int
|
||||
reveal_type(f(True)) # revealed: int
|
||||
reveal_type(f(None)) # revealed: None
|
||||
# error: [invalid-argument-type]
|
||||
reveal_type(f("string")) # revealed: Unknown
|
||||
```
|
||||
|
||||
## Typevar constraints
|
||||
|
||||
If a type parameter has an upper bound, that upper bound constrains which types can be used for that
|
||||
typevar. This effectively adds the upper bound as an intersection to every appearance of the typevar
|
||||
in the function.
|
||||
|
||||
```py
|
||||
def good_param[T: int](x: T) -> None:
|
||||
reveal_type(x) # revealed: T
|
||||
```
|
||||
|
||||
If the function is annotated as returning the typevar, this means that the upper bound is _not_
|
||||
assignable to that typevar, since return types are contravariant. In `bad`, we can infer that
|
||||
`x + 1` has type `int`. But `T` might be instantiated with a narrower type than `int`, and so the
|
||||
return value is not guaranteed to be compatible for all `T: int`.
|
||||
|
||||
```py
|
||||
def good_return[T: int](x: T) -> T:
|
||||
return x
|
||||
|
||||
def bad_return[T: int](x: T) -> T:
|
||||
# error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `int`"
|
||||
return x + 1
|
||||
```
|
||||
|
||||
## All occurrences of the same typevar have the same type
|
||||
|
||||
If a typevar appears multiple times in a function signature, all occurrences have the same type.
|
||||
|
||||
```py
|
||||
def different_types[T, S](cond: bool, t: T, s: S) -> T:
|
||||
if cond:
|
||||
return t
|
||||
else:
|
||||
# error: [invalid-return-type] "Return type does not match returned value: Expected `T`, found `S`"
|
||||
return s
|
||||
|
||||
def same_types[T](cond: bool, t1: T, t2: T) -> T:
|
||||
if cond:
|
||||
return t1
|
||||
else:
|
||||
return t2
|
||||
```
|
||||
|
||||
## All occurrences of the same constrained typevar have the same type
|
||||
|
||||
The above is true even when the typevars are constrained. Here, both `int` and `str` have `__add__`
|
||||
methods that are compatible with the return type, so the `return` expression is always well-typed:
|
||||
|
||||
```py
|
||||
def same_constrained_types[T: (int, str)](t1: T, t2: T) -> T:
|
||||
# TODO: no error
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `T` and `T`"
|
||||
return t1 + t2
|
||||
```
|
||||
|
||||
This is _not_ the same as a union type, because of this additional constraint that the two
|
||||
occurrences have the same type. In `unions_are_different`, `t1` and `t2` might have different types,
|
||||
and an `int` and a `str` cannot be added together:
|
||||
|
||||
```py
|
||||
def unions_are_different(t1: int | str, t2: int | str) -> int | str:
|
||||
# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `int | str` and `int | str`"
|
||||
return t1 + t2
|
||||
```
|
||||
|
||||
## Typevar inference is a unification problem
|
||||
|
||||
When inferring typevar assignments in a generic function call, we cannot simply solve constraints
|
||||
eagerly for each parameter in turn. We must solve a unification problem involving all of the
|
||||
parameters simultaneously.
|
||||
|
||||
```py
|
||||
def two_params[T](x: T, y: T) -> T:
|
||||
return x
|
||||
|
||||
reveal_type(two_params("a", "b")) # revealed: Literal["a", "b"]
|
||||
reveal_type(two_params("a", 1)) # revealed: Literal["a", 1]
|
||||
```
|
||||
|
||||
When one of the parameters is a union, we attempt to find the smallest specialization that satisfies
|
||||
all of the constraints.
|
||||
|
||||
```py
|
||||
def union_param[T](x: T | None) -> T:
|
||||
if x is None:
|
||||
raise ValueError
|
||||
return x
|
||||
|
||||
reveal_type(union_param("a")) # revealed: Literal["a"]
|
||||
reveal_type(union_param(1)) # revealed: Literal[1]
|
||||
reveal_type(union_param(None)) # revealed: Unknown
|
||||
```
|
||||
|
||||
```py
|
||||
def union_and_nonunion_params[T](x: T | int, y: T) -> T:
|
||||
return y
|
||||
|
||||
reveal_type(union_and_nonunion_params(1, "a")) # revealed: Literal["a"]
|
||||
reveal_type(union_and_nonunion_params("a", "a")) # revealed: Literal["a"]
|
||||
reveal_type(union_and_nonunion_params(1, 1)) # revealed: Literal[1]
|
||||
reveal_type(union_and_nonunion_params(3, 1)) # revealed: Literal[1]
|
||||
reveal_type(union_and_nonunion_params("a", 1)) # revealed: Literal["a", 1]
|
||||
```
|
||||
|
||||
```py
|
||||
def tuple_param[T, S](x: T | S, y: tuple[T, S]) -> tuple[T, S]:
|
||||
return y
|
||||
|
||||
reveal_type(tuple_param("a", ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
|
||||
reveal_type(tuple_param(1, ("a", 1))) # revealed: tuple[Literal["a"], Literal[1]]
|
||||
```
|
||||
|
||||
## Inferring nested generic function calls
|
||||
|
||||
We can infer type assignments in nested calls to multiple generic functions. If they use the same
|
||||
type variable, we do not confuse the two; `T@f` and `T@g` have separate types in each example below.
|
||||
|
||||
```py
|
||||
def f[T](x: T) -> tuple[T, int]:
|
||||
return (x, 1)
|
||||
|
||||
def g[T](x: T) -> T | None:
|
||||
return x
|
||||
|
||||
reveal_type(f(g("a"))) # revealed: tuple[Literal["a"] | None, int]
|
||||
reveal_type(g(f("a"))) # revealed: tuple[Literal["a"], int] | None
|
||||
```
|
||||
|
||||
## Protocols as TypeVar bounds
|
||||
|
||||
Protocol types can be used as TypeVar bounds, just like nominal types.
|
||||
|
||||
```py
|
||||
from typing import Any, Protocol
|
||||
from ty_extensions import static_assert, is_assignable_to
|
||||
|
||||
class SupportsClose(Protocol):
|
||||
def close(self) -> None: ...
|
||||
|
||||
class ClosableFullyStaticProtocol(Protocol):
|
||||
x: int
|
||||
def close(self) -> None: ...
|
||||
|
||||
class ClosableNonFullyStaticProtocol(Protocol):
|
||||
x: Any
|
||||
def close(self) -> None: ...
|
||||
|
||||
class ClosableFullyStaticNominal:
|
||||
x: int
|
||||
def close(self) -> None: ...
|
||||
|
||||
class ClosableNonFullyStaticNominal:
|
||||
x: int
|
||||
def close(self) -> None: ...
|
||||
|
||||
class NotClosableProtocol(Protocol): ...
|
||||
class NotClosableNominal: ...
|
||||
|
||||
def close_and_return[T: SupportsClose](x: T) -> T:
|
||||
x.close()
|
||||
return x
|
||||
|
||||
def f(
|
||||
a: SupportsClose,
|
||||
b: ClosableFullyStaticProtocol,
|
||||
c: ClosableNonFullyStaticProtocol,
|
||||
d: ClosableFullyStaticNominal,
|
||||
e: ClosableNonFullyStaticNominal,
|
||||
f: NotClosableProtocol,
|
||||
g: NotClosableNominal,
|
||||
):
|
||||
reveal_type(close_and_return(a)) # revealed: SupportsClose
|
||||
reveal_type(close_and_return(b)) # revealed: ClosableFullyStaticProtocol
|
||||
reveal_type(close_and_return(c)) # revealed: ClosableNonFullyStaticProtocol
|
||||
reveal_type(close_and_return(d)) # revealed: ClosableFullyStaticNominal
|
||||
reveal_type(close_and_return(e)) # revealed: ClosableNonFullyStaticNominal
|
||||
|
||||
# error: [invalid-argument-type] "does not satisfy upper bound"
|
||||
reveal_type(close_and_return(f)) # revealed: Unknown
|
||||
# error: [invalid-argument-type] "does not satisfy upper bound"
|
||||
reveal_type(close_and_return(g)) # revealed: Unknown
|
||||
```
|
|
@ -0,0 +1,630 @@
|
|||
# PEP 695 Generics
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
[PEP 695] and Python 3.12 introduced new, more ergonomic syntax for type variables.
|
||||
|
||||
## Type variables
|
||||
|
||||
### Defining PEP 695 type variables
|
||||
|
||||
PEP 695 introduces a new syntax for defining type variables. The resulting type variables are
|
||||
instances of `typing.TypeVar`, just like legacy type variables.
|
||||
|
||||
```py
|
||||
def f[T]():
|
||||
reveal_type(type(T)) # revealed: Literal[TypeVar]
|
||||
reveal_type(T) # revealed: typing.TypeVar
|
||||
reveal_type(T.__name__) # revealed: Literal["T"]
|
||||
```
|
||||
|
||||
### Type variables with a default
|
||||
|
||||
Note that the `__default__` property is only available in Python ≥3.13.
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.13"
|
||||
```
|
||||
|
||||
```py
|
||||
def f[T = int]():
|
||||
reveal_type(T.__default__) # revealed: int
|
||||
reveal_type(T.__bound__) # revealed: None
|
||||
reveal_type(T.__constraints__) # revealed: tuple[()]
|
||||
|
||||
def g[S]():
|
||||
reveal_type(S.__default__) # revealed: NoDefault
|
||||
```
|
||||
|
||||
### Type variables with an upper bound
|
||||
|
||||
```py
|
||||
def f[T: int]():
|
||||
reveal_type(T.__bound__) # revealed: int
|
||||
reveal_type(T.__constraints__) # revealed: tuple[()]
|
||||
|
||||
def g[S]():
|
||||
reveal_type(S.__bound__) # revealed: None
|
||||
```
|
||||
|
||||
### Type variables with constraints
|
||||
|
||||
```py
|
||||
def f[T: (int, str)]():
|
||||
reveal_type(T.__constraints__) # revealed: tuple[int, str]
|
||||
reveal_type(T.__bound__) # revealed: None
|
||||
|
||||
def g[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.
|
||||
|
||||
```py
|
||||
# error: [invalid-type-variable-constraints] "TypeVar must have at least two constrained types"
|
||||
def f[T: (int,)]():
|
||||
pass
|
||||
```
|
||||
|
||||
## Invalid uses
|
||||
|
||||
Note that many of the invalid uses of legacy typevars do not apply to PEP 695 typevars, since the
|
||||
PEP 695 syntax is only allowed places where typevars are allowed.
|
||||
|
||||
## Displaying typevars
|
||||
|
||||
We use a suffix when displaying the typevars of a generic function or class. This helps distinguish
|
||||
different uses of the same typevar.
|
||||
|
||||
```py
|
||||
def f[T](x: T, y: T) -> None:
|
||||
# TODO: revealed: T@f
|
||||
reveal_type(x) # revealed: T
|
||||
|
||||
class C[T]:
|
||||
def m(self, x: T) -> None:
|
||||
# TODO: revealed: T@c
|
||||
reveal_type(x) # revealed: T
|
||||
```
|
||||
|
||||
## Fully static typevars
|
||||
|
||||
We consider a typevar to be fully static unless it has a non-fully-static bound or constraint. This
|
||||
is true even though a fully static typevar might be specialized to a gradual form like `Any`. (This
|
||||
is similar to how you can assign an expression whose type is not fully static to a target whose type
|
||||
is.)
|
||||
|
||||
```py
|
||||
from ty_extensions import is_fully_static, static_assert
|
||||
from typing import Any
|
||||
|
||||
def unbounded_unconstrained[T](t: T) -> None:
|
||||
static_assert(is_fully_static(T))
|
||||
|
||||
def bounded[T: int](t: T) -> None:
|
||||
static_assert(is_fully_static(T))
|
||||
|
||||
def bounded_by_gradual[T: Any](t: T) -> None:
|
||||
static_assert(not is_fully_static(T))
|
||||
|
||||
def constrained[T: (int, str)](t: T) -> None:
|
||||
static_assert(is_fully_static(T))
|
||||
|
||||
def constrained_by_gradual[T: (int, Any)](t: T) -> None:
|
||||
static_assert(not is_fully_static(T))
|
||||
```
|
||||
|
||||
## Subtyping and assignability
|
||||
|
||||
(Note: for simplicity, all of the prose in this section refers to _subtyping_ involving fully static
|
||||
typevars. Unless otherwise noted, all of the claims also apply to _assignability_ involving gradual
|
||||
typevars.)
|
||||
|
||||
We can make no assumption about what type an unbounded, unconstrained, fully static typevar will be
|
||||
specialized to. Properties are true of the typevar only if they are true for every valid
|
||||
specialization. Thus, the typevar is a subtype of itself and of `object`, but not of any other type
|
||||
(including other typevars).
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_subtype_of, static_assert
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
class Sub(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def unbounded_unconstrained[T, U](t: T, u: U) -> None:
|
||||
static_assert(is_assignable_to(T, T))
|
||||
static_assert(is_assignable_to(T, object))
|
||||
static_assert(not is_assignable_to(T, Super))
|
||||
static_assert(is_assignable_to(U, U))
|
||||
static_assert(is_assignable_to(U, object))
|
||||
static_assert(not is_assignable_to(U, Super))
|
||||
static_assert(not is_assignable_to(T, U))
|
||||
static_assert(not is_assignable_to(U, T))
|
||||
|
||||
static_assert(is_subtype_of(T, T))
|
||||
static_assert(is_subtype_of(T, object))
|
||||
static_assert(not is_subtype_of(T, Super))
|
||||
static_assert(is_subtype_of(U, U))
|
||||
static_assert(is_subtype_of(U, object))
|
||||
static_assert(not is_subtype_of(U, Super))
|
||||
static_assert(not is_subtype_of(T, U))
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
```
|
||||
|
||||
A bounded typevar is assignable to its bound, and a bounded, fully static typevar is a subtype of
|
||||
its bound. (A typevar with a non-fully-static bound is itself non-fully-static, and therefore does
|
||||
not participate in subtyping.) A fully static bound is not assignable to, nor a subtype of, the
|
||||
typevar, since the typevar might be specialized to a smaller type. (This is true even if the bound
|
||||
is a final class, since the typevar can still be specialized to `Never`.)
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
from typing_extensions import final
|
||||
|
||||
def bounded[T: Super](t: T) -> None:
|
||||
static_assert(is_assignable_to(T, Super))
|
||||
static_assert(not is_assignable_to(T, Sub))
|
||||
static_assert(not is_assignable_to(Super, T))
|
||||
static_assert(not is_assignable_to(Sub, T))
|
||||
|
||||
static_assert(is_subtype_of(T, Super))
|
||||
static_assert(not is_subtype_of(T, Sub))
|
||||
static_assert(not is_subtype_of(Super, T))
|
||||
static_assert(not is_subtype_of(Sub, T))
|
||||
|
||||
def bounded_by_gradual[T: Any](t: T) -> None:
|
||||
static_assert(is_assignable_to(T, Any))
|
||||
static_assert(is_assignable_to(Any, T))
|
||||
static_assert(is_assignable_to(T, Super))
|
||||
static_assert(not is_assignable_to(Super, T))
|
||||
static_assert(is_assignable_to(T, Sub))
|
||||
static_assert(not is_assignable_to(Sub, T))
|
||||
|
||||
static_assert(not is_subtype_of(T, Any))
|
||||
static_assert(not is_subtype_of(Any, T))
|
||||
static_assert(not is_subtype_of(T, Super))
|
||||
static_assert(not is_subtype_of(Super, T))
|
||||
static_assert(not is_subtype_of(T, Sub))
|
||||
static_assert(not is_subtype_of(Sub, T))
|
||||
|
||||
@final
|
||||
class FinalClass: ...
|
||||
|
||||
def bounded_final[T: FinalClass](t: T) -> None:
|
||||
static_assert(is_assignable_to(T, FinalClass))
|
||||
static_assert(not is_assignable_to(FinalClass, T))
|
||||
|
||||
static_assert(is_subtype_of(T, FinalClass))
|
||||
static_assert(not is_subtype_of(FinalClass, T))
|
||||
```
|
||||
|
||||
Two distinct fully static typevars are not subtypes of each other, even if they have the same
|
||||
bounds, since there is (still) no guarantee that they will be specialized to the same type. This is
|
||||
true even if both typevars are bounded by the same final class, since you can specialize the
|
||||
typevars to `Never` in addition to that final class.
|
||||
|
||||
```py
|
||||
def two_bounded[T: Super, U: Super](t: T, u: U) -> None:
|
||||
static_assert(not is_assignable_to(T, U))
|
||||
static_assert(not is_assignable_to(U, T))
|
||||
|
||||
static_assert(not is_subtype_of(T, U))
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
|
||||
def two_final_bounded[T: FinalClass, U: FinalClass](t: T, u: U) -> None:
|
||||
static_assert(not is_assignable_to(T, U))
|
||||
static_assert(not is_assignable_to(U, T))
|
||||
|
||||
static_assert(not is_subtype_of(T, U))
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
```
|
||||
|
||||
A constrained fully static typevar is assignable to the union of its constraints, but not to any of
|
||||
the constraints individually. None of the constraints are subtypes of the typevar, though the
|
||||
intersection of all of its constraints is a subtype of the typevar.
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection
|
||||
|
||||
def constrained[T: (Base, Unrelated)](t: T) -> None:
|
||||
static_assert(not is_assignable_to(T, Super))
|
||||
static_assert(not is_assignable_to(T, Base))
|
||||
static_assert(not is_assignable_to(T, Sub))
|
||||
static_assert(not is_assignable_to(T, Unrelated))
|
||||
static_assert(is_assignable_to(T, Super | Unrelated))
|
||||
static_assert(is_assignable_to(T, Base | Unrelated))
|
||||
static_assert(not is_assignable_to(T, Sub | Unrelated))
|
||||
static_assert(not is_assignable_to(Super, T))
|
||||
static_assert(not is_assignable_to(Unrelated, T))
|
||||
static_assert(not is_assignable_to(Super | Unrelated, T))
|
||||
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
|
||||
|
||||
static_assert(not is_subtype_of(T, Super))
|
||||
static_assert(not is_subtype_of(T, Base))
|
||||
static_assert(not is_subtype_of(T, Sub))
|
||||
static_assert(not is_subtype_of(T, Unrelated))
|
||||
static_assert(is_subtype_of(T, Super | Unrelated))
|
||||
static_assert(is_subtype_of(T, Base | Unrelated))
|
||||
static_assert(not is_subtype_of(T, Sub | Unrelated))
|
||||
static_assert(not is_subtype_of(Super, T))
|
||||
static_assert(not is_subtype_of(Unrelated, T))
|
||||
static_assert(not is_subtype_of(Super | Unrelated, T))
|
||||
static_assert(is_subtype_of(Intersection[Base, Unrelated], T))
|
||||
|
||||
def constrained_by_gradual[T: (Base, Any)](t: T) -> None:
|
||||
static_assert(is_assignable_to(T, Super))
|
||||
static_assert(is_assignable_to(T, Base))
|
||||
static_assert(not is_assignable_to(T, Sub))
|
||||
static_assert(not is_assignable_to(T, Unrelated))
|
||||
static_assert(is_assignable_to(T, Any))
|
||||
static_assert(is_assignable_to(T, Super | Any))
|
||||
static_assert(is_assignable_to(T, Super | Unrelated))
|
||||
static_assert(not is_assignable_to(Super, T))
|
||||
static_assert(is_assignable_to(Base, T))
|
||||
static_assert(not is_assignable_to(Unrelated, T))
|
||||
static_assert(is_assignable_to(Any, T))
|
||||
static_assert(not is_assignable_to(Super | Any, T))
|
||||
static_assert(is_assignable_to(Base | Any, T))
|
||||
static_assert(not is_assignable_to(Super | Unrelated, T))
|
||||
static_assert(is_assignable_to(Intersection[Base, Unrelated], T))
|
||||
static_assert(is_assignable_to(Intersection[Base, Any], T))
|
||||
|
||||
static_assert(not is_subtype_of(T, Super))
|
||||
static_assert(not is_subtype_of(T, Base))
|
||||
static_assert(not is_subtype_of(T, Sub))
|
||||
static_assert(not is_subtype_of(T, Unrelated))
|
||||
static_assert(not is_subtype_of(T, Any))
|
||||
static_assert(not is_subtype_of(T, Super | Any))
|
||||
static_assert(not is_subtype_of(T, Super | Unrelated))
|
||||
static_assert(not is_subtype_of(Super, T))
|
||||
static_assert(not is_subtype_of(Base, T))
|
||||
static_assert(not is_subtype_of(Unrelated, T))
|
||||
static_assert(not is_subtype_of(Any, T))
|
||||
static_assert(not is_subtype_of(Super | Any, T))
|
||||
static_assert(not is_subtype_of(Base | Any, T))
|
||||
static_assert(not is_subtype_of(Super | Unrelated, T))
|
||||
static_assert(not is_subtype_of(Intersection[Base, Unrelated], T))
|
||||
static_assert(not is_subtype_of(Intersection[Base, Any], T))
|
||||
```
|
||||
|
||||
Two distinct fully static typevars are not subtypes of each other, even if they have the same
|
||||
constraints, and even if any of the constraints are final. There must always be at least two
|
||||
distinct constraints, meaning that there is (still) no guarantee that they will be specialized to
|
||||
the same type.
|
||||
|
||||
```py
|
||||
def two_constrained[T: (int, str), U: (int, str)](t: T, u: U) -> None:
|
||||
static_assert(not is_assignable_to(T, U))
|
||||
static_assert(not is_assignable_to(U, T))
|
||||
|
||||
static_assert(not is_subtype_of(T, U))
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
|
||||
@final
|
||||
class AnotherFinalClass: ...
|
||||
|
||||
def two_final_constrained[T: (FinalClass, AnotherFinalClass), U: (FinalClass, AnotherFinalClass)](t: T, u: U) -> None:
|
||||
static_assert(not is_assignable_to(T, U))
|
||||
static_assert(not is_assignable_to(U, T))
|
||||
|
||||
static_assert(not is_subtype_of(T, U))
|
||||
static_assert(not is_subtype_of(U, T))
|
||||
```
|
||||
|
||||
## Singletons and single-valued types
|
||||
|
||||
(Note: for simplicity, all of the prose in this section refers to _singleton_ types, but all of the
|
||||
claims also apply to _single-valued_ types.)
|
||||
|
||||
An unbounded, unconstrained typevar is not a singleton, because it can be specialized to a
|
||||
non-singleton type.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_singleton, is_single_valued, static_assert
|
||||
|
||||
def unbounded_unconstrained[T](t: T) -> None:
|
||||
static_assert(not is_singleton(T))
|
||||
static_assert(not is_single_valued(T))
|
||||
```
|
||||
|
||||
A bounded typevar is not a singleton, even if its bound is a singleton, since it can still be
|
||||
specialized to `Never`.
|
||||
|
||||
```py
|
||||
def bounded[T: None](t: T) -> None:
|
||||
static_assert(not is_singleton(T))
|
||||
static_assert(not is_single_valued(T))
|
||||
```
|
||||
|
||||
A constrained typevar is a singleton if all of its constraints are singletons. (Note that you cannot
|
||||
specialize a constrained typevar to a subtype of a constraint.)
|
||||
|
||||
```py
|
||||
from typing_extensions import Literal
|
||||
|
||||
def constrained_non_singletons[T: (int, str)](t: T) -> None:
|
||||
static_assert(not is_singleton(T))
|
||||
static_assert(not is_single_valued(T))
|
||||
|
||||
def constrained_singletons[T: (Literal[True], Literal[False])](t: T) -> None:
|
||||
static_assert(is_singleton(T))
|
||||
|
||||
def constrained_single_valued[T: (Literal[True], tuple[()])](t: T) -> None:
|
||||
static_assert(is_single_valued(T))
|
||||
```
|
||||
|
||||
## Unions involving typevars
|
||||
|
||||
The union of an unbounded unconstrained typevar with any other type cannot be simplified, since
|
||||
there is no guarantee what type the typevar will be specialized to.
|
||||
|
||||
```py
|
||||
from typing import Any
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
class Sub(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def unbounded_unconstrained[T](t: T) -> None:
|
||||
def _(x: T | Super) -> None:
|
||||
reveal_type(x) # revealed: T | Super
|
||||
|
||||
def _(x: T | Base) -> None:
|
||||
reveal_type(x) # revealed: T | Base
|
||||
|
||||
def _(x: T | Sub) -> None:
|
||||
reveal_type(x) # revealed: T | Sub
|
||||
|
||||
def _(x: T | Unrelated) -> None:
|
||||
reveal_type(x) # revealed: T | Unrelated
|
||||
|
||||
def _(x: T | Any) -> None:
|
||||
reveal_type(x) # revealed: T | Any
|
||||
```
|
||||
|
||||
The union of a bounded typevar with its bound is that bound. (The typevar is guaranteed to be
|
||||
specialized to a subtype of the bound.) The union of a bounded typevar with a subtype of its bound
|
||||
cannot be simplified. (The typevar might be specialized to a different subtype of the bound.)
|
||||
|
||||
```py
|
||||
def bounded[T: Base](t: T) -> None:
|
||||
def _(x: T | Super) -> None:
|
||||
reveal_type(x) # revealed: Super
|
||||
|
||||
def _(x: T | Base) -> None:
|
||||
reveal_type(x) # revealed: Base
|
||||
|
||||
def _(x: T | Sub) -> None:
|
||||
reveal_type(x) # revealed: T | Sub
|
||||
|
||||
def _(x: T | Unrelated) -> None:
|
||||
reveal_type(x) # revealed: T | Unrelated
|
||||
|
||||
def _(x: T | Any) -> None:
|
||||
reveal_type(x) # revealed: T | Any
|
||||
```
|
||||
|
||||
The union of a constrained typevar with a type depends on how that type relates to the constraints.
|
||||
If all of the constraints are a subtype of that type, the union simplifies to that type. Inversely,
|
||||
if the type is a subtype of every constraint, the union simplifies to the typevar. Otherwise, the
|
||||
union cannot be simplified.
|
||||
|
||||
```py
|
||||
def constrained[T: (Base, Sub)](t: T) -> None:
|
||||
def _(x: T | Super) -> None:
|
||||
reveal_type(x) # revealed: Super
|
||||
|
||||
def _(x: T | Base) -> None:
|
||||
reveal_type(x) # revealed: Base
|
||||
|
||||
def _(x: T | Sub) -> None:
|
||||
reveal_type(x) # revealed: T
|
||||
|
||||
def _(x: T | Unrelated) -> None:
|
||||
reveal_type(x) # revealed: T | Unrelated
|
||||
|
||||
def _(x: T | Any) -> None:
|
||||
reveal_type(x) # revealed: T | Any
|
||||
```
|
||||
|
||||
## Intersections involving typevars
|
||||
|
||||
The intersection of an unbounded unconstrained typevar with any other type cannot be simplified,
|
||||
since there is no guarantee what type the typevar will be specialized to.
|
||||
|
||||
```py
|
||||
from ty_extensions import Intersection
|
||||
from typing import Any
|
||||
|
||||
class Super: ...
|
||||
class Base(Super): ...
|
||||
class Sub(Base): ...
|
||||
class Unrelated: ...
|
||||
|
||||
def unbounded_unconstrained[T](t: T) -> None:
|
||||
def _(x: Intersection[T, Super]) -> None:
|
||||
reveal_type(x) # revealed: T & Super
|
||||
|
||||
def _(x: Intersection[T, Base]) -> None:
|
||||
reveal_type(x) # revealed: T & Base
|
||||
|
||||
def _(x: Intersection[T, Sub]) -> None:
|
||||
reveal_type(x) # revealed: T & Sub
|
||||
|
||||
def _(x: Intersection[T, Unrelated]) -> None:
|
||||
reveal_type(x) # revealed: T & Unrelated
|
||||
|
||||
def _(x: Intersection[T, Any]) -> None:
|
||||
reveal_type(x) # revealed: T & Any
|
||||
```
|
||||
|
||||
The intersection of a bounded typevar with its bound or a supertype of its bound is the typevar
|
||||
itself. (The typevar might be specialized to a subtype of the bound.) The intersection of a bounded
|
||||
typevar with a subtype of its bound cannot be simplified. (The typevar might be specialized to a
|
||||
different subtype of the bound.) The intersection of a bounded typevar with a type that is disjoint
|
||||
from its bound is `Never`.
|
||||
|
||||
```py
|
||||
def bounded[T: Base](t: T) -> None:
|
||||
def _(x: Intersection[T, Super]) -> None:
|
||||
reveal_type(x) # revealed: T
|
||||
|
||||
def _(x: Intersection[T, Base]) -> None:
|
||||
reveal_type(x) # revealed: T
|
||||
|
||||
def _(x: Intersection[T, Sub]) -> None:
|
||||
reveal_type(x) # revealed: T & Sub
|
||||
|
||||
def _(x: Intersection[T, None]) -> None:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
def _(x: Intersection[T, Any]) -> None:
|
||||
reveal_type(x) # revealed: T & Any
|
||||
```
|
||||
|
||||
Constrained typevars can be modeled using a hypothetical `OneOf` connector, where the typevar must
|
||||
be specialized to _one_ of its constraints. The typevar is not the _union_ of those constraints,
|
||||
since that would allow the typevar to take on values from _multiple_ constraints simultaneously. The
|
||||
`OneOf` connector would not be a “type” according to a strict reading of the typing spec, since it
|
||||
would not represent a single set of runtime objects; it would instead represent a _set of_ sets of
|
||||
runtime objects. This is one reason we have not actually added this connector to our data model yet.
|
||||
Nevertheless, describing constrained typevars this way helps explain how we simplify intersections
|
||||
involving them.
|
||||
|
||||
This means that when intersecting a constrained typevar with a type `T`, constraints that are
|
||||
supertypes of `T` can be simplified to `T`, since intersection distributes over `OneOf`. Moreover,
|
||||
constraints that are disjoint from `T` are no longer valid specializations of the typevar, since
|
||||
`Never` is an identity for `OneOf`. After these simplifications, if only one constraint remains, we
|
||||
can simplify the intersection as a whole to that constraint.
|
||||
|
||||
```py
|
||||
def constrained[T: (Base, Sub, Unrelated)](t: T) -> None:
|
||||
def _(x: Intersection[T, Base]) -> None:
|
||||
# With OneOf this would be OneOf[Base, Sub]
|
||||
reveal_type(x) # revealed: T & Base
|
||||
|
||||
def _(x: Intersection[T, Unrelated]) -> None:
|
||||
reveal_type(x) # revealed: Unrelated
|
||||
|
||||
def _(x: Intersection[T, Sub]) -> None:
|
||||
reveal_type(x) # revealed: Sub
|
||||
|
||||
def _(x: Intersection[T, None]) -> None:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
def _(x: Intersection[T, Any]) -> None:
|
||||
reveal_type(x) # revealed: T & Any
|
||||
```
|
||||
|
||||
We can simplify the intersection similarly when removing a type from a constrained typevar, since
|
||||
this is modeled internally as an intersection with a negation.
|
||||
|
||||
```py
|
||||
from ty_extensions import Not
|
||||
|
||||
def remove_constraint[T: (int, str, bool)](t: T) -> None:
|
||||
def _(x: Intersection[T, Not[int]]) -> None:
|
||||
reveal_type(x) # revealed: str & ~int
|
||||
|
||||
def _(x: Intersection[T, Not[str]]) -> None:
|
||||
# With OneOf this would be OneOf[int, bool]
|
||||
reveal_type(x) # revealed: T & ~str
|
||||
|
||||
def _(x: Intersection[T, Not[bool]]) -> None:
|
||||
reveal_type(x) # revealed: T & ~bool
|
||||
|
||||
def _(x: Intersection[T, Not[int], Not[str]]) -> None:
|
||||
reveal_type(x) # revealed: Never
|
||||
|
||||
def _(x: Intersection[T, Not[None]]) -> None:
|
||||
reveal_type(x) # revealed: T
|
||||
|
||||
def _(x: Intersection[T, Not[Any]]) -> None:
|
||||
reveal_type(x) # revealed: T & Any
|
||||
```
|
||||
|
||||
The intersection of a typevar with any other type is assignable to (and if fully static, a subtype
|
||||
of) itself.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_subtype_of, static_assert, Not
|
||||
|
||||
def intersection_is_assignable[T](t: T) -> None:
|
||||
static_assert(is_assignable_to(Intersection[T, None], T))
|
||||
static_assert(is_assignable_to(Intersection[T, Not[None]], T))
|
||||
|
||||
static_assert(is_subtype_of(Intersection[T, None], T))
|
||||
static_assert(is_subtype_of(Intersection[T, Not[None]], T))
|
||||
```
|
||||
|
||||
## Narrowing
|
||||
|
||||
We can use narrowing expressions to eliminate some of the possibilities of a constrained typevar:
|
||||
|
||||
```py
|
||||
class P: ...
|
||||
class Q: ...
|
||||
class R: ...
|
||||
|
||||
def f[T: (P, Q)](t: T) -> None:
|
||||
if isinstance(t, P):
|
||||
reveal_type(t) # revealed: P
|
||||
p: P = t
|
||||
else:
|
||||
reveal_type(t) # revealed: Q & ~P
|
||||
q: Q = t
|
||||
|
||||
if isinstance(t, Q):
|
||||
reveal_type(t) # revealed: Q
|
||||
q: Q = t
|
||||
else:
|
||||
reveal_type(t) # revealed: P & ~Q
|
||||
p: P = t
|
||||
|
||||
def g[T: (P, Q, R)](t: T) -> None:
|
||||
if isinstance(t, P):
|
||||
reveal_type(t) # revealed: P
|
||||
p: P = t
|
||||
elif isinstance(t, Q):
|
||||
reveal_type(t) # revealed: Q & ~P
|
||||
q: Q = t
|
||||
else:
|
||||
reveal_type(t) # revealed: R & ~P & ~Q
|
||||
r: R = t
|
||||
|
||||
if isinstance(t, P):
|
||||
reveal_type(t) # revealed: P
|
||||
p: P = t
|
||||
elif isinstance(t, Q):
|
||||
reveal_type(t) # revealed: Q & ~P
|
||||
q: Q = t
|
||||
elif isinstance(t, R):
|
||||
reveal_type(t) # revealed: R & ~P & ~Q
|
||||
r: R = t
|
||||
else:
|
||||
reveal_type(t) # revealed: Never
|
||||
```
|
||||
|
||||
If the constraints are disjoint, simplification does eliminate the redundant negative:
|
||||
|
||||
```py
|
||||
def h[T: (P, None)](t: T) -> None:
|
||||
if t is None:
|
||||
reveal_type(t) # revealed: None
|
||||
p: None = t
|
||||
else:
|
||||
reveal_type(t) # revealed: P
|
||||
p: P = t
|
||||
```
|
||||
|
||||
[pep 695]: https://peps.python.org/pep-0695/
|
|
@ -0,0 +1,277 @@
|
|||
# Variance: PEP 695 syntax
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
Type variables have a property called _variance_ that affects the subtyping and assignability
|
||||
relations. Much more detail can be found in the [spec]. To summarize, each typevar is either
|
||||
**covariant**, **contravariant**, **invariant**, or **bivariant**. (Note that bivariance is not
|
||||
currently mentioned in the typing spec, but is a fourth case that we must consider.)
|
||||
|
||||
For all of the examples below, we will consider a typevar `T`, a generic class using that typevar
|
||||
`C[T]`, and two types `A` and `B`.
|
||||
|
||||
## Covariance
|
||||
|
||||
With a covariant typevar, subtyping is in "alignment": if `A <: B`, then `C[A] <: C[B]`.
|
||||
|
||||
Types that "produce" data on demand are covariant in their typevar. If you expect a sequence of
|
||||
`int`s, someone can safely provide a sequence of `bool`s, since each `bool` element that you would
|
||||
get from the sequence is a valid `int`.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
|
||||
from typing import Any
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
def receive(self) -> T:
|
||||
raise ValueError
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(C[B], C[A]))
|
||||
static_assert(not is_assignable_to(C[A], C[B]))
|
||||
static_assert(is_assignable_to(C[A], C[Any]))
|
||||
static_assert(is_assignable_to(C[B], C[Any]))
|
||||
static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
static_assert(is_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_equivalent_to(C[B], C[B]))
|
||||
static_assert(not is_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_equivalent_to(C[A], C[B]))
|
||||
static_assert(not is_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_gradual_equivalent_to(C[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||
```
|
||||
|
||||
## Contravariance
|
||||
|
||||
With a contravariant typevar, subtyping is in "opposition": if `A <: B`, then `C[B] <: C[A]`.
|
||||
|
||||
Types that "consume" data are contravariant in their typevar. If you expect a consumer that receives
|
||||
`bool`s, someone can safely provide a consumer that expects to receive `int`s, since each `bool`
|
||||
that you pass into the consumer is a valid `int`.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
|
||||
from typing import Any
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
def send(self, value: T): ...
|
||||
|
||||
static_assert(not is_assignable_to(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(C[A], C[B]))
|
||||
static_assert(is_assignable_to(C[A], C[Any]))
|
||||
static_assert(is_assignable_to(C[B], C[Any]))
|
||||
static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
static_assert(is_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_equivalent_to(C[B], C[B]))
|
||||
static_assert(not is_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_equivalent_to(C[A], C[B]))
|
||||
static_assert(not is_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_gradual_equivalent_to(C[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||
```
|
||||
|
||||
## Invariance
|
||||
|
||||
With an invariant typevar, _no_ specializations of the generic class are subtypes of each other.
|
||||
|
||||
This often occurs for types that are both producers _and_ consumers, like a mutable `list`.
|
||||
Iterating over the elements in a list would work with a covariant typevar, just like with the
|
||||
"producer" type above. Appending elements to a list would work with a contravariant typevar, just
|
||||
like with the "consumer" type above. However, a typevar cannot be both covariant and contravariant
|
||||
at the same time!
|
||||
|
||||
If you expect a mutable list of `int`s, it's not safe for someone to provide you with a mutable list
|
||||
of `bool`s, since you might try to add an element to the list: if you try to add an `int`, the list
|
||||
would no longer only contain elements that are subtypes of `bool`.
|
||||
|
||||
Conversely, if you expect a mutable list of `bool`s, it's not safe for someone to provide you with a
|
||||
mutable list of `int`s, since you might try to extract elements from the list: you expect every
|
||||
element that you extract to be a subtype of `bool`, but the list can contain any `int`.
|
||||
|
||||
In the end, if you expect a mutable list, you must always be given a list of exactly that type,
|
||||
since we can't know in advance which of the allowed methods you'll want to use.
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
|
||||
from typing import Any
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
def send(self, value: T): ...
|
||||
def receive(self) -> T:
|
||||
raise ValueError
|
||||
|
||||
static_assert(not is_assignable_to(C[B], C[A]))
|
||||
static_assert(not is_assignable_to(C[A], C[B]))
|
||||
static_assert(is_assignable_to(C[A], C[Any]))
|
||||
static_assert(is_assignable_to(C[B], C[Any]))
|
||||
static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
static_assert(not is_subtype_of(C[B], C[A]))
|
||||
static_assert(not is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
static_assert(is_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_equivalent_to(C[B], C[B]))
|
||||
static_assert(not is_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_equivalent_to(C[A], C[B]))
|
||||
static_assert(not is_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_gradual_equivalent_to(C[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[B]))
|
||||
static_assert(not is_gradual_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_gradual_equivalent_to(C[Any], C[B]))
|
||||
```
|
||||
|
||||
## Bivariance
|
||||
|
||||
With a bivariant typevar, _all_ specializations of the generic class are subtypes of (and in fact,
|
||||
equivalent to) each other.
|
||||
|
||||
This is a bit of pathological case, which really only happens when the class doesn't use the typevar
|
||||
at all. (If it did, it would have to be covariant, contravariant, or invariant, depending on _how_
|
||||
the typevar was used.)
|
||||
|
||||
```py
|
||||
from ty_extensions import is_assignable_to, is_equivalent_to, is_gradual_equivalent_to, is_subtype_of, static_assert, Unknown
|
||||
from typing import Any
|
||||
|
||||
class A: ...
|
||||
class B(A): ...
|
||||
|
||||
class C[T]:
|
||||
pass
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_assignable_to(C[A], C[B]))
|
||||
static_assert(is_assignable_to(C[A], C[Any]))
|
||||
static_assert(is_assignable_to(C[B], C[Any]))
|
||||
static_assert(is_assignable_to(C[Any], C[A]))
|
||||
static_assert(is_assignable_to(C[Any], C[B]))
|
||||
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_subtype_of(C[A], C[B]))
|
||||
static_assert(not is_subtype_of(C[A], C[Any]))
|
||||
static_assert(not is_subtype_of(C[B], C[Any]))
|
||||
static_assert(not is_subtype_of(C[Any], C[A]))
|
||||
static_assert(not is_subtype_of(C[Any], C[B]))
|
||||
|
||||
static_assert(is_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_equivalent_to(C[B], C[B]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_equivalent_to(C[A], C[B]))
|
||||
static_assert(not is_equivalent_to(C[A], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[B], C[Any]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[A]))
|
||||
static_assert(not is_equivalent_to(C[Any], C[B]))
|
||||
|
||||
static_assert(is_gradual_equivalent_to(C[A], C[A]))
|
||||
static_assert(is_gradual_equivalent_to(C[B], C[B]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Any]))
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[Unknown]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(C[B], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(C[A], C[B]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(C[A], C[Any]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(C[B], C[Any]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[A]))
|
||||
# TODO: no error
|
||||
# error: [static-assert-error]
|
||||
static_assert(is_gradual_equivalent_to(C[Any], C[B]))
|
||||
```
|
||||
|
||||
[spec]: https://typing.python.org/en/latest/spec/generics.html#variance
|
291
crates/ty_python_semantic/resources/mdtest/generics/scoping.md
Normal file
291
crates/ty_python_semantic/resources/mdtest/generics/scoping.md
Normal file
|
@ -0,0 +1,291 @@
|
|||
# Scoping rules for type variables
|
||||
|
||||
```toml
|
||||
[environment]
|
||||
python-version = "3.12"
|
||||
```
|
||||
|
||||
Most of these tests come from the [Scoping rules for type variables][scoping] section of the typing
|
||||
spec.
|
||||
|
||||
## Typevar used outside of generic function or class
|
||||
|
||||
Typevars may only be used in generic function or class definitions.
|
||||
|
||||
```py
|
||||
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.
|
||||
|
||||
```py
|
||||
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.
|
||||
|
||||
```py
|
||||
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.
|
||||
|
||||
```py
|
||||
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 this function 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](./call/methods.md) 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.
|
||||
|
||||
```py
|
||||
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[int]`>
|
||||
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.
|
||||
|
||||
```py
|
||||
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:
|
||||
|
||||
```py
|
||||
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:
|
||||
|
||||
```py
|
||||
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`:
|
||||
|
||||
```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
|
||||
|
||||
```py
|
||||
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
|
||||
|
||||
```py
|
||||
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
|
||||
|
||||
```py
|
||||
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
|
||||
|
||||
```py
|
||||
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.
|
||||
|
||||
```py
|
||||
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]
|
||||
```
|
||||
|
||||
[scoping]: https://typing.python.org/en/latest/spec/generics.html#scoping-rules-for-type-variables
|
Loading…
Add table
Add a link
Reference in a new issue