Callables

Argument defaults

It may be useful to declare an argument as having a default without specifying the actual default value. For example:

def foo(x: AnyStr, y: AnyStr = ...) -> AnyStr: ...

What should the default value look like? Any of the options "", b"" or None fails to satisfy the type constraint.

In such cases the default value may be specified as a literal ellipsis, i.e. the above example is literally what you would write.

Annotating *args and **kwargs

Type annotation on variadic positional arguments (*args) and keyword arguments (**kwargs) refer to the types of individual arguments, not to the type of the entire collection (except if Unpack is used).

Therefore, the definition:

def foo(*args: str, **kwds: int): ...

is acceptable and it means that the function accepts an arbitrary number of positional arguments of type str and an arbitrary number of keyword arguments of type int. For example, all of the following represent function calls with valid types of arguments:

foo('a', 'b', 'c')
foo(x=1, y=2)
foo('', z=0)

In the body of function foo, the type of variable args is deduced as tuple[str, ...] and the type of variable kwds is dict[str, int].

Unpack for keyword arguments

typing.Unpack has two use cases in the type system:

  • As introduced by PEP 646, a backward-compatible form for certain operations involving variadic generics. See the section on TypeVarTuple for details.

  • As introduced by PEP 692, a way to annotate the **kwargs of a function.

This second usage is described in this section. The following example:

from typing import TypedDict, Unpack

class Movie(TypedDict):
    name: str
    year: int

def foo(**kwargs: Unpack[Movie]) -> None: ...

means that the **kwargs comprise two keyword arguments specified by Movie (i.e. a name keyword of type str and a year keyword of type int). This indicates that the function should be called as follows:

kwargs: Movie = {"name": "Life of Brian", "year": 1979}

foo(**kwargs)                               # OK!
foo(name="The Meaning of Life", year=1983)  # OK!

When Unpack is used, type checkers treat kwargs inside the function body as a TypedDict:

def foo(**kwargs: Unpack[Movie]) -> None:
    assert_type(kwargs, Movie)  # OK!

Using the new annotation will not have any runtime effect - it should only be taken into account by type checkers. Any mention of errors in the following sections relates to type checker errors.

Function calls with standard dictionaries

Passing a dictionary of type dict[str, object] as a **kwargs argument to a function that has **kwargs annotated with Unpack must generate a type checker error. On the other hand, the behaviour for functions using standard, untyped dictionaries can depend on the type checker. For example:

def foo(**kwargs: Unpack[Movie]) -> None: ...

movie: dict[str, object] = {"name": "Life of Brian", "year": 1979}
foo(**movie)  # WRONG! Movie is of type dict[str, object]

typed_movie: Movie = {"name": "The Meaning of Life", "year": 1983}
foo(**typed_movie)  # OK!

another_movie = {"name": "Life of Brian", "year": 1979}
foo(**another_movie)  # Depends on the type checker.

Keyword collisions

A TypedDict that is used to type **kwargs could potentially contain keys that are already defined in the function’s signature. If the duplicate name is a standard parameter, an error should be reported by type checkers. If the duplicate name is a positional-only parameter, no errors should be generated. For example:

def foo(name, **kwargs: Unpack[Movie]) -> None: ...     # WRONG! "name" will
                                                        # always bind to the
                                                        # first parameter.

def foo(name, /, **kwargs: Unpack[Movie]) -> None: ...  # OK! "name" is a
                                                        # positional-only parameter,
                                                        # so **kwargs can contain
                                                        # a "name" keyword.

Required and non-required keys

By default all keys in a TypedDict are required. This behaviour can be overridden by setting the dictionary’s total parameter as False. Moreover, PEP 655 introduced new type qualifiers - typing.Required and typing.NotRequired - that enable specifying whether a particular key is required or not:

class Movie(TypedDict):
    title: str
    year: NotRequired[int]

When using a TypedDict to type **kwargs all of the required and non-required keys should correspond to required and non-required function keyword parameters. Therefore, if a required key is not supported by the caller, then an error must be reported by type checkers.

Assignment

Assignments of a function typed with **kwargs: Unpack[Movie] and another callable type should pass type checking only if they are compatible. This can happen for the scenarios described below.

Source and destination contain **kwargs

Both destination and source functions have a **kwargs: Unpack[TypedDict] parameter and the destination function’s TypedDict is assignable to the source function’s TypedDict and the rest of the parameters are compatible:

class Animal(TypedDict):
    name: str

class Dog(Animal):
    breed: str

def accept_animal(**kwargs: Unpack[Animal]): ...
def accept_dog(**kwargs: Unpack[Dog]): ...

accept_dog = accept_animal  # OK! Expression of type Dog can be
                            # assigned to a variable of type Animal.

accept_animal = accept_dog  # WRONG! Expression of type Animal
                            # cannot be assigned to a variable of type Dog.

Source contains **kwargs and destination doesn’t

The destination callable doesn’t contain **kwargs, the source callable contains **kwargs: Unpack[TypedDict] and the destination function’s keyword arguments are assignable to the corresponding keys in source function’s TypedDict. Moreover, not required keys should correspond to optional function arguments, whereas required keys should correspond to required function arguments. Again, the rest of the parameters have to be compatible. Continuing the previous example:

class Example(TypedDict):
    animal: Animal
    string: str
    number: NotRequired[int]

def src(**kwargs: Unpack[Example]): ...
def dest(*, animal: Dog, string: str, number: int = ...): ...

dest = src  # OK!

It is worth pointing out that the destination function’s parameters that are to be compatible with the keys and values from the TypedDict must be keyword only:

def dest(dog: Dog, string: str, number: int = ...): ...

dog: Dog = {"name": "Daisy", "breed": "labrador"}

dest(dog, "some string")  # OK!

dest = src                # Type checker error!
dest(dog, "some string")  # The same call fails at
                          # runtime now because 'src' expects
                          # keyword arguments.

The reverse situation where the destination callable contains **kwargs: Unpack[TypedDict] and the source callable doesn’t contain **kwargs should be disallowed. This is because, we cannot be sure that additional keyword arguments are not being passed in when an instance of a subclass had been assigned to a variable with a base class type and then unpacked in the destination callable invocation:

def dest(**kwargs: Unpack[Animal]): ...
def src(name: str): ...

dog: Dog = {"name": "Daisy", "breed": "Labrador"}
animal: Animal = dog

dest = src      # WRONG!
dest(**animal)  # Fails at runtime.

Similar situation can happen even without inheritance as compatibility between TypedDicts is based on structural subtyping.

Source contains untyped **kwargs

The destination callable contains **kwargs: Unpack[TypedDict] and the source callable contains untyped **kwargs:

def src(**kwargs): ...
def dest(**kwargs: Unpack[Movie]): ...

dest = src  # OK!

Source contains traditionally typed **kwargs: T

The destination callable contains **kwargs: Unpack[TypedDict], the source callable contains traditionally typed **kwargs: T and each of the destination function TypedDict’s fields is assignable to a variable of type T:

class Vehicle:
    ...

class Car(Vehicle):
    ...

class Motorcycle(Vehicle):
    ...

class Vehicles(TypedDict):
    car: Car
    moto: Motorcycle

def dest(**kwargs: Unpack[Vehicles]): ...
def src(**kwargs: Vehicle): ...

dest = src  # OK!

On the other hand, if the destination callable contains either untyped or traditionally typed **kwargs: T and the source callable is typed using **kwargs: Unpack[TypedDict] then an error should be generated, because traditionally typed **kwargs aren’t checked for keyword names.

To summarize, function parameters should behave contravariantly and function return types should behave covariantly.

Passing kwargs inside a function to another function

A previous point mentions the problem of possibly passing additional keyword arguments by assigning a subclass instance to a variable that has a base class type. Let’s consider the following example:

class Animal(TypedDict):
    name: str

class Dog(Animal):
    breed: str

def takes_name(name: str): ...

dog: Dog = {"name": "Daisy", "breed": "Labrador"}
animal: Animal = dog

def foo(**kwargs: Unpack[Animal]):
    print(kwargs["name"].capitalize())

def bar(**kwargs: Unpack[Animal]):
    takes_name(**kwargs)

def baz(animal: Animal):
    takes_name(**animal)

def spam(**kwargs: Unpack[Animal]):
    baz(kwargs)

foo(**animal)   # OK! foo only expects and uses keywords of 'Animal'.

bar(**animal)   # WRONG! This will fail at runtime because 'breed' keyword
                # will be passed to 'takes_name' as well.

spam(**animal)  # WRONG! Again, 'breed' keyword will be eventually passed
                # to 'takes_name'.

In the example above, the call to foo will not cause any issues at runtime. Even though foo expects kwargs of type Animal it doesn’t matter if it receives additional arguments because it only reads and uses what it needs completely ignoring any additional values.

The calls to bar and spam will fail because an unexpected keyword argument will be passed to the takes_name function.

Therefore, kwargs hinted with an unpacked TypedDict can only be passed to another function if the function to which unpacked kwargs are being passed to has **kwargs in its signature as well, because then additional keywords would not cause errors at runtime during function invocation. Otherwise, the type checker should generate an error.

In cases similar to the bar function above the problem could be worked around by explicitly dereferencing desired fields and using them as arguments to perform the function call:

def bar(**kwargs: Unpack[Animal]):
    name = kwargs["name"]
    takes_name(name)

Using Unpack with types other than TypedDict

TypedDict is the only permitted heterogeneous type for typing **kwargs. Therefore, in the context of typing **kwargs, using Unpack with types other than TypedDict should not be allowed and type checkers should generate errors in such cases.

Callable

Frameworks expecting callback functions of specific signatures might be type hinted using Callable[[Arg1Type, Arg2Type], ReturnType]. Examples:

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

It is possible to declare the return type of a callable without specifying the call signature by substituting a literal ellipsis (three dots) for the list of arguments:

def partial(func: Callable[..., str], *args) -> Callable[..., str]:
    # Body

Note that there are no square brackets around the ellipsis. The arguments of the callback are completely unconstrained in this case (and keyword arguments are acceptable).

Since using callbacks with keyword arguments is not perceived as a common use case, there is currently no support for specifying keyword arguments with Callable. Similarly, Callable does not support specifying callback signatures with a variable number of arguments of a specific type. For these use cases, see the section on Callback protocols.

Callback protocols

Protocols can be used to define flexible callback types that are hard (or even impossible) to express using the Callable[...] syntax as specified above, such as variadic, overloaded, and complex generic callbacks. They can be defined as protocols with a __call__ member:

from typing import Protocol

class Combiner(Protocol):
    def __call__(self, *vals: bytes,
                 maxlen: int | None = None) -> list[bytes]: ...

def good_cb(*vals: bytes, maxlen: int | None = None) -> list[bytes]:
    ...
def bad_cb(*vals: bytes, maxitems: int | None) -> list[bytes]:
    ...

comb: Combiner = good_cb  # OK
comb = bad_cb  # Error! Argument 2 has incompatible type because of
               # different name and kind in the callback

Callback protocols and Callable[...] types can be used interchangeably.