Read the body first

Development

Writing Python Code Like a Pro: A Deep Dive into Type Checking #3

In Part 2, we introduced several basic types and showed how to use the typing module to specify function arguments and return types. In this part, we'll explore some more advanced types and how to use them in your code.

5. Using typing.Generator, typing.Iterable, typing.Iterator for generators and iterators

When defining a function that acts as a generator, specify the return type as Generator[YieldType, SendType, ReturnType]. If you're unfamiliar with generators, check out this article for an introduction.

def echo_round() -> Generator[int, float, str]:
    sent = yield 0
    while sent >= 0:
        sent = yield round(sent)
    return 'Done'

The echo_round() function above is a generator that takes float as input and yields int, returning a str when it receives a negative value.

>>> a = echo_round()
>>> a.send(None)  # Start
0
>>> a.send(3.5)
4
>>> a.send(-3)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration: Done

Generators often only yield values instead of taking or returning them. In such cases, specify the return type as Generator[YieldType, None, None]. Alternatively, you can use typing.Iterable[YieldType] or typing.Iterator[YieldType], which we'll cover shortly.

import random
from typing import Iterator

def random_generator(val: int) -> Iterator[float]:
    for i in range(val):
        yield random.random()

Note

Starting from Python 3.9, you can use the collections.abc module's Generator, Iterator, and Iterable instead of the typing module.

There's no need to differentiate between Iterator and Iterable when using generators. However, if the object implements the __next__ magic method, it's an Iterator. Otherwise, it's an Iterable. Both types assume that the object implements the __iter__ magic method.

6. Using typing.Callable for functions as arguments

When passing functions as arguments, use the Callable[[Arg1Type, Arg2Type], ReturnType] type:

def on_some_event_happened(callback: Callable[[int, str, str], int]) -> None:
    ...

def do_this(a: int, b: str, c:str) -> int:
    ...

on_some_event_happened(do_this)

We'll cover more advanced topics related to Callable in Part 2.

Note

Callable can also be used for callable objects.

7. Using typing.Type for classes as arguments

When passing a class instance as an argument, simply specify the class name:

class Transaction:
    ...

def process_txn(txn: Transaction):
    ...

However, if you're passing the class itself as an argument, use typing.Type[ClassName]:

class Factory:
    ...

class AFactory(Factory):
    ...

class BFactory(Factory):
    ...

def initiate_factory(factory: Type[Factory]):
    ...

There are functions that receive exceptions as arguments. In such cases, use Type[ExceptionC] for the same reason:

def on_exception(exception_class: Type[Exception]):
    ...

8. Using typing.Any for arbitrary types

If the type doesn't matte, use Any (although it's better to avoid this if possible).

_Stay tuned for Part 4, where we'll explore more advanced typing concepts.