Daraan
Daraan

Reputation: 3947

What do ** (double star/asterisk) and * (star/asterisk) inside square brakets mean for class and function declarations in Python 3.12+?

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

Answers (1)

Daraan
Daraan

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 TypeVars 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

Related Questions