Typing

Type aliases

from typing import List

Vector = List[float]

def scale(scalar: float, vector: Vector) -> Vector:
    return [scalar * num for num in vector]

NewType

from typing import NewType

UserId = NewType('UserId', int)

Callable

Frameworks expecting callback functions of specific signatures might be type hinted using:

Callable[[Arg1Type, Arg2Type], ReturnType]

For example:

from collections.abc import Callable

def feeder(get_next_item: Callable[[], str]) -> None:
    # Body

def async_query(on_success: Callable[[int], None],
                on_error: Callable[[int, Exception], None]) -> None:
    # Body

async def on_update(value: str) -> None:
    # Body

callback: Callable[[str], Awaitable[None]] = on_update

It is possible to declare the return type of a callable without specifying the call signature by substituting a literal ellipsis for the list of arguments in the type hint:

Callable[..., ReturnType]

Generics

Generics can be parameterized by using a factory available in typing called TypeVar.

from collections.abc import Sequence
from typing import TypeVar

T = TypeVar('T')      # Declare type variable

def first(l: Sequence[T]) -> T:   # Generic function
    return l[0]

Here the return type is “linked” to the parameter type: whatever you put into the function, the same thing comes out.

T = TypeVar('T')  # Can be anything
S = TypeVar('S', bound=str)  # Can be any subtype of str
A = TypeVar('A', str, bytes)  # Must be exactly str or bytes
U = TypeVar('U', bound=str|bytes)  # Can be any subtype of the union str|bytes
V = TypeVar('V', bound=SupportsAbs)  # Can be anything with an __abs__ method
X = TypeVar('X', covariant=True)  # Allowing Subclasses to be used
Y = TypeVar('Y', contravariant=True)  # Allowing Parent class to be used

More info about covariant and contravariant parameters in the PEP 484: https://peps.python.org/pep-0484/#covariance-and-contravariance

User-defined generic types

A user-defined class can be defined as a generic class. The type is stated when we instantiate the class.

from typing import Dict, Generic, TypeVar

T = TypeVar("T")

class Registry(Generic[T]):
    def __init__(self) -> None:
        self._store: Dict[str, T] = {}

    def set_item(self, k: str, v: T) -> None:
        self._store[k] = v

    def get_item(self, k: str) -> T:
        return self._store[k]

if __name__ == "__main__":
    family_name_reg = Registry[str]()
    family_age_reg = Registry[int]()

    family_name_reg.set_item("husband", "steve")
    family_name_reg.set_item("dad", "john")

    family_age_reg.set_item("steve", 30)

Nominal vs structural subtyping

from collections.abc import Iterator, Iterable

class Bucket:  # Note: no base classes
    ...
    def __len__(self) -> int: ...
    def __iter__(self) -> Iterator[int]: ...


def collect(items: Iterable[int]) -> int: ...

result = collect(Bucket())  # Passes type check

Bucket is implicitly considered a subtype of both Sized (from collections.abc import Sized) and Iterable[int] (from collections.abc import Iterable) by static type checkers thanks to the definition of methods __len__ and __iter__. This is known as structural subtyping (or static duck-typing).

MyPy

You can use mypy (conda install -c conda-forge mypy) to check the correct typing of your project/script.

Examples

Multiple possible input type:

from tying import Union
from numbers import Number

Union[Number, str]

Generator function (yield):

from collections.abc import Generator

def test(N: int) -> Generator[int]:
    for k in range(N):
        yield k

Iterable object:

from collections.abc import Iterable

def test(params: Iterable[int]):
    for p in params:
        print(p)

Sources: