Reputation: 3947
What does T, *Ts, **P
mean when they are used in square brackets directly after a class or function name or with the type
keyword?
class ChildClass[T, *Ts, **P]: ...
def foo[T, *Ts, **P](arg: T) -> Callable[P, tuple[T, *Ts]]:
type vat[T, *Ts, **P] = Callable[P, tuple[T, *Ts]]
See complementary questions about ** and * for function parameters and arguments:
Upvotes: 4
Views: 147
Reputation: 3947
Python 3.12, in relation to PEP 695, introduced some grammar changes which bring some features from the typing library into the syntax of the language itself.
In short: Each of the 0-to-2-starred expressions T
, *Ts
, and **P
is a type parameter. They are used as a parameters for type parameter lists which can be used for functions, classes and the type
statement to create a TypeAliasType
.
A type parameter list, encapsulated by []
may follow the name of a class, function, or typevar after a type
statement and will create a generic. Type-checkers can infer the type of attributes and calls for example:
class ClassA[T]:
def method1(self) -> T:
...
def create[T, *Ts](arg: T, *args: *Ts) -> ClassA[tuple[T, *Ts]]:
...
a = create(1, 2, 3) # a inferred as ClassA[tuple[int, int, int]]
t = a.method1() # t inferred as tuple[int, int, int]
Inside a type parameter list the three different amounts of asterisks have the following meaning:
A non-starred identifier, e.g. T
is the equivalent to typing.TypeVar
and is interpreted as a single type.
An identifier prefixed with a single asterisk, e.g. *Ts
, is the equivalent to typing.TypeVarTuple and is interpreted as a tuple of types. The single-asterisk prefix *
before a TypeVarTuple
is equivalent to using typing.Unpack which can be seen similar to a normal unpacking just for types, e.g. tuple[int, *tuple[int, str]]
is equivalent to tuple[int, int, str]
. Using a TypeVarTuple
makes it variable what types are unpacked.
Two asterisk before an identifer, e.g. **P
, denote a ParamSpec, which in turn stands for the parameters of a callable.
The three different variants of 0-2 asterisk differentiate between these types.
A type parameter list for function and classes defines the listed type parameters in their local scope only, similar to typing.Self which refers to the type of the encapsulating class inside a class
' body.
The following example from the documentation illustrates most usages:
def overly_generic[
SimpleTypeVar,
TypeVarWithBound: int,
TypeVarWithConstraints: (str, bytes),
*SimpleTypeVarTuple,
**SimpleParamSpec,
](
a: SimpleTypeVar,
b: TypeVarWithBound,
c: Callable[SimpleParamSpec, TypeVarWithConstraints],
*d: SimpleTypeVarTuple,
): ...
Furthermore a ParamSpec
's args
and kwargs
attributes can also be used to annotate both and only both function parameters prefixed with *
and **
def usage_with_kwargs[
SimpleTypeVar,
**SimpleParamSpec,
](
c: Callable[SimpleParamSpec, SimpleTypeVar],
*c_args: SimpleParamSpec.args,
**c_kwargs: SimpleParamSpec.kwargs
): ...
As you might have noticed the syntax also allows for colons :
in a type parameter list, these address some of the other paramters of typing.TypeVar
, allowing to define an upper bound type of a type variable or constraints that state a type is either type A or B or ...
TypeVarWithBound: int # equivalent to TypeVar("TypeVarWithBound", bound=int)
TypeVarWithConstraints: (str, bytes), # equivalent to TypeVar("TypeVarWithConstraints", str, bytes)
The same holds for type parameter lists when used for class declarations and the type
statement, just that in Python versions <3.12 we have to use typing_extensions.TypeAliasType
or typing.Generic
, the former beeing the functional equivalent of the type
statement.
Quoting PEP 695:
Defining a generic class prior to python 3.12 looks something like this:
from typing import Generic, TypeVar
_T_co = TypeVar("_T_co", covariant=True, bound=str)
class ClassA(Generic[_T_co]):
def method1(self) -> _T_co:
...
With the new syntax, it looks like this, omitting the need to use TypeVar
s directly.
class ClassA[T: str]:
def method1(self) -> T:
...
For the type
statement it is similar:
from collections.abc import Callable
type call[T, **P, *Ts] = Callable[P, tuple[T, *Ts]]
is equivalent to
from typing import ParamSpec, TypeVarTuple, TypeVar, Unpack #, TypeAliasType # if Python >= 3.12
from typing_extensions import TypeAliasType # backport
from collections.abc import Callable
T = TypeVar('T')
P = ParamSpec('P')
Ts = TypeVarTuple("Ts")
call = TypeAliasType("call", Callable[P, tuple[T, Unpack[Ts]]], type_params=(T, P, Ts))
Upvotes: 3