Type aliases

(See PEP 613 for the introduction of TypeAlias, and PEP 695 for the type statement.)

Type aliases may be defined by simple variable assignments:

Url = str

def retry(url: Url, retry_count: int) -> None: ...

Or by using typing.TypeAlias:

from typing import TypeAlias

Url: TypeAlias = str

def retry(url: Url, retry_count: int) -> None: ...

Or by using the type statement (Python 3.12 and higher):

type Url = str

def retry(url: Url, retry_count: int) -> None: ...

Note that we recommend capitalizing alias names, since they represent user-defined types, which (like user-defined classes) are typically spelled that way.

Type aliases may be as complex as type hints in annotations – anything that is acceptable as a type hint is acceptable in a type alias:

from typing import TypeVar
from collections.abc import Iterable

T = TypeVar('T', bound=float)
Vector = Iterable[tuple[T, T]]

def inproduct(v: Vector[T]) -> T:
    return sum(x*y for x, y in v)
def dilate(v: Vector[T], scale: T) -> Vector[T]:
    return ((x * scale, y * scale) for x, y in v)
vec: Vector[float] = []

This is equivalent to:

from typing import TypeVar
from collections.abc import Iterable

T = TypeVar('T', bound=float)

def inproduct(v: Iterable[tuple[T, T]]) -> T:
    return sum(x*y for x, y in v)
def dilate(v: Iterable[tuple[T, T]], scale: T) -> Iterable[tuple[T, T]]:
    return ((x * scale, y * scale) for x, y in v)
vec: Iterable[tuple[float, float]] = []

TypeAlias

The explicit alias declaration syntax with TypeAlias clearly differentiates between the three possible kinds of assignments: typed global expressions, untyped global expressions, and type aliases. This avoids the existence of assignments that break type checking when an annotation is added, and avoids classifying the nature of the assignment based on the type of the value.

Implicit syntax (pre-existing):

x = 1  # untyped global expression
x: int = 1  # typed global expression

x = int  # type alias
x: type[int] = int  # typed global expression

Explicit syntax:

x = 1  # untyped global expression
x: int = 1  # typed global expression

x = int  # untyped global expression (see note below)
x: type[int] = int  # typed global expression

x: TypeAlias = int  # type alias
x: TypeAlias = "MyClass"  # type alias

Note: The examples above illustrate implicit and explicit alias declarations in isolation. For the sake of backwards compatibility, type checkers should support both simultaneously, meaning an untyped global expression x = int will still be considered a valid type alias.

type statement

Type aliases may also be defined using the type statement (Python 3.12 and higher).

The type statement allows the creation of explicitly generic type aliases:

type ListOrSet[T] = list[T] | set[T]

Type parameters declared as part of a generic type alias are valid only when evaluating the right-hand side of the type alias.

As with typing.TypeAlias, type checkers should restrict the right-hand expression to expression forms that are allowed within type annotations. The use of more complex expression forms (call expressions, ternary operators, arithmetic operators, comparison operators, etc.) should be flagged as an error.

Type alias expressions are not allowed to use traditional type variables (i.e. those allocated with an explicit TypeVar constructor call). Type checkers should generate an error in this case.

T = TypeVar("T")
type MyList = list[T]  # Type checker error: traditional type variable usage

NewType

There are also situations where a programmer might want to avoid logical errors by creating simple classes. For example:

class UserId(int):
    pass

def get_by_user_id(user_id: UserId):
    ...

However, this approach introduces a runtime overhead. To avoid this, typing.py provides a helper function NewType that creates simple unique types with almost zero runtime overhead. For a static type checker Derived = NewType('Derived', Base) is roughly equivalent to a definition:

class Derived(Base):
    def __init__(self, _x: Base) -> None:
        ...

While at runtime, NewType('Derived', Base) returns a dummy function that simply returns its argument. Type checkers require explicit casts from int where UserId is expected, while implicitly casting from UserId where int is expected. Examples:

UserId = NewType('UserId', int)

def name_by_id(user_id: UserId) -> str:
    ...

UserId('user')          # Fails type check

name_by_id(42)          # Fails type check
name_by_id(UserId(42))  # OK

num = UserId(5) + 1     # type: int

NewType accepts exactly two arguments: a name for the new unique type, and a base class. The latter should be a proper class (i.e., not a type construct like Union, etc.), or another unique type created by calling NewType. The function returned by NewType accepts only one argument; this is equivalent to supporting only one constructor accepting an instance of the base class (see above). Example:

class PacketId:
    def __init__(self, major: int, minor: int) -> None:
        self._major = major
        self._minor = minor

TcpPacketId = NewType('TcpPacketId', PacketId)

packet = PacketId(100, 100)
tcp_packet = TcpPacketId(packet)  # OK

tcp_packet = TcpPacketId(127, 0)  # Fails in type checker and at runtime

Both isinstance and issubclass, as well as subclassing will fail for NewType('Derived', Base) since function objects don’t support these operations.