18.1. Typing About

  • Also known as: "type annotations", "type hints", "gradual typing"

  • Types are not required, and never will be

  • Good IDE will give you hints

  • Types are used extensively in system libraries

  • More and more books and documentations use types

  • Introduced in Python 3.5

  • To type check use: mypy, pyre-check, pytypes

  • https://typing.readthedocs.io/en/latest/

Types are not required, and never will be. -- Guido van Rossum, Python initiator, core developer, former BDFL

It should be emphasized that Python will remain a dynamically typed language, and the authors have no desire to ever make type hints mandatory, even by convention. -- Python Software Foundation

../../_images/typeannotation-timeline.png

Figure 18.4. Timeline of changes to type annotations from Python 3.0 to now [1]

18.1.1. Rationale

Imagine you have a function that takes an email address as an argument and checks if it ends with a specific domain:

>>> email = 'alice@example.com'

We want to check if the email ends with 'example.com' or 'example.edu':

>>> email.endswith('example.com') or email.endswith('example.edu')
True

Python provides a more concise way to check if a string ends with any of a tuple of suffixes:

>>> domains = ('example.com', 'example.edu')
>>> email.endswith(domains)
True

However, if we try to use a list instead of a tuple, we get an error:

>>> domains = ['example.com', 'example.edu']
>>> email.endswith(domains)
Traceback (most recent call last):
TypeError: endswith first arg must be str or a tuple of str, not list

If the codebase is large and the error is not caught by tests, it can lead to a runtime error in production. With type annotations and static type checkers, we can catch such errors before running the code.

18.1.2. Problem

>>> def add_numbers(a, b):
...     return a + b
>>>
>>>
>>> add_numbers(1, 2)
3
>>> add_numbers(1.0, 2.0)
3.0
>>> add_numbers('a', 'b')
'ab'
>>> add_numbers(['a'], ['b'])
['a', 'b']

18.1.3. Runtime Validation

  • if + isinstance() checks the type at runtime

  • if + type() checks the type at runtime

  • assert type() checks the type at runtime

  • raises TypeError if the type is wrong

if + isinstance():

>>> def add_numbers(a, b):
...     if not isinstance(a, int|float):
...         raise TypeError('a must be int or float')
...     if not isinstance(b, int|float):
...         raise TypeError('b must be int or float')
...     return a + b
>>>
>>>
>>> add_numbers(1, 2)
3
>>> add_numbers(1.0, 2.0)
3.0
>>> add_numbers('a', 'b')
Traceback (most recent call last):
TypeError: a must be int or float
>>>
>>> add_numbers(['a'], ['b'])
Traceback (most recent call last):
TypeError: a must be int or float

if + type():

>>> def add_numbers(a, b):
...     if type(a) not in (int, float):
...         raise TypeError('a must be int or float')
...     if type(b) not in (int, float):
...         raise TypeError('b must be int or float')
...     return a + b
>>>
>>>
>>> add_numbers(1, 2)
3
>>> add_numbers(1.0, 2.0)
3.0
>>> add_numbers('a', 'b')
Traceback (most recent call last):
TypeError: a must be int or float
>>>
>>> add_numbers(['a'], ['b'])
Traceback (most recent call last):
TypeError: a must be int or float

assert + type():

>>> def add_numbers(a, b):
...     assert type(a) in (int, float), 'a must be int or float'
...     assert type(b) in (int, float), 'b must be int or float'
...     return a + b
>>>
>>>
>>> add_numbers(1, 2)
3
>>> add_numbers(1.0, 2.0)
3.0
>>> add_numbers('a', 'b')
Traceback (most recent call last):
AssertionError: a must be int or float
>>>
>>> add_numbers(['a'], ['b'])
Traceback (most recent call last):
AssertionError: a must be int or float

18.1.4. Static Validation

>>> def add_numbers(a: int|float, b: int|float) -> int|float:
...     return a + b
>>>
>>>
>>> add_numbers(1, 2)
3
>>> add_numbers(1.0, 2.0)
3.0
>>> add_numbers('a', 'b')
'ab'
>>> add_numbers(['a'], ['b'])
['a', 'b']

18.1.5. Python 2.X

>>> def add(a, b):
...     """
...     :param a: int
...     :param b: int
...     :return: int
...     """
...     return a + b

18.1.6. Python 2.7

>>> def add(a, b):
...     # (int, int) -> int
...     return a + b

18.1.7. Python 3.0

>>> def add(a: 'int', b: 'int') -> 'int':
...     return a + b

18.1.8. Python 3.5

>>> def add(a: int, b: int) -> int:
...     return a + b

18.1.9. Typing PEPs

  • https://peps.python.org/topic/typing

  • Since Python 3.5: PEP 484 -- Type Hints

  • Since Python 3.6: PEP 526 -- Syntax for Variable Annotations

  • Since Python 3.8: PEP 544 -- Protocols: Structural subtyping (static duck typing)

  • Since Python 3.9: PEP 585 -- Type Hinting Generics In Standard Collections

  • Since Python 3.10: PEP 604 -- Allow writing union types as X | Y

  • PEP 482 - literature overview on type hints

  • PEP 483 - background on type hints

  • PEP 484 - type hints

  • PEP 526 - variable annotations and ClassVar

  • PEP 544 - Protocol

  • PEP 561 - distributing typed packages

  • PEP 563 - from __future__ import annotations

  • PEP 585 - subscriptable generics in the standard library

  • PEP 586 - Literal

  • PEP 589 - TypedDict

  • PEP 591 - Final

  • PEP 593 - Annotated

  • PEP 604 - union syntax with |

  • PEP 612 - ParamSpec

  • PEP 613 - TypeAlias

  • PEP 646 - variadic generics and TypeVarTuple

  • PEP 647 - TypeGuard

  • PEP 649 - (draft), from __future__ import co_annotations

  • PEP 655 - Required and NotRequired

  • PEP 673 - Self

  • PEP 675 - LiteralString

  • PEP 677 - (rejected), (int, str) -> bool callable type syntax

  • PEP 681 - @dataclass_transform()

  • PEP 688 - Buffer

  • PEP 692 - Unpack[TypedDict] for **kwargs

  • PEP 695 - class Class[T]: type parameter syntax

  • PEP 696 - (draft), defaults for type variables

  • PEP 698 - @override

  • PEP 702 - (draft), @deprecated()

  • PEP 705 - (draft), TypedMapping

18.1.10. Errors

  • Types are not Enforced

  • This code will run without any problems

  • Types are not required, and never will be

  • Although mypy, pyre-check or pytypes will throw error

>>> def add(a: int, b: int) -> int:
...     return a + b
>>>
>>>
>>> add(1, 2)
3
>>>
>>> add(1.0, 2.0)
3.0
>>>
>>> add('a', 'b')
'ab'

18.1.11. Annotation

  • Annotation and definition can be separated

Two step process:

>>> x: int
>>> x = 1

One liner:

>>> x: int = 1

18.1.12. Annotation is not Definition

Two step process:

>>> print(a)
Traceback (most recent call last):
NameError: name 'a' is not defined
>>>
>>> a: int
>>>
>>> print(a)
Traceback (most recent call last):
NameError: name 'a' is not defined
>>>
>>> a = 1
>>> print(a)
1

Oneliner:

>>> print(b)
Traceback (most recent call last):
NameError: name 'b' is not defined
>>>
>>> b: int = 1
>>>
>>> print(b)
1

Even if the type is wrong, the code will run and the value will be assigned:

>>> print(c)
Traceback (most recent call last):
NameError: name 'c' is not defined
>>>
>>> c: int = 'one'
>>>
>>> print(c)
one

18.1.13. Dynamic Typing

  • good: fast development

  • bad: runtime errors (in projects with many developers)

>>> def add(a, b):
...     return a + b

18.1.14. Static Typing

  • good: no runtime errors

  • bad: slow development

int add(int a, int b) {
    return a + b;
}

float add(float a, float b) {
    return a + b;
}

float add(int a, float b) {
    return (float)a + b;
}

float add(float a, int b) {
    return a + (float)b;
}

18.1.15. Gradual Typing

  • good: fast development

  • good: no runtime errors

Iteration 1:

>>> def add(a, b):
...     return a + b

Iteration 2:

>>> def add(a: int, b: int):
...     return a + b

Iteration 3:

>>> def add(a: int, b: int) -> int:
...     return a + b

Iteration 4:

>>> def add(a: int|float, b: int|float) -> int|float:
...     return a + b

18.1.16. Checkers

  • mypy - written in Python, reference implementation for type checkers (developed by Python Software Foundation)

  • pyre-check - written in OCaml, optimized for performance (developed by Facebook/Meta)

  • pyrefly - written in Rust, type checker and language server, lightning-fast (developed by Facebook/Meta)

  • pyright - written in TypeScript, full-featured, standards-based, designed for high performance (developed by Microsoft)

  • pytype - written in Python, uses inference instead of gradual typing - works even without annotations (developed by Google)

  • ty - written in Rust, part of the ruff, uv family (developed by Astral/OpenAI)

  • pyrefly - written in Rust, fast type checker and language server for Python with powerful IDE features (developed by Astral/OpenAI)

18.1.17. Further Reading

18.1.18. References