a-walk-in-python

A walk in Python

View on GitHub

Chapter: Type hinting in Python

1. Introduction to Type hinting in Python

Python is a dynamically typed language, meaning variables do not require explicit type definitions. However, Python introduced type hints (also called static typing) in PEP 484 to improve readability, maintainability, and error detection. Then some improvements were introduced in 3.9 with PEP 585

Example: Dynamic Typing in Python

def add(a, b):
    return a + b  # No type specified

The function add() works for integers, floats, and even strings, but this flexibility can lead to unexpected errors.

Example: Using Type Hints

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

Type hints do not enforce types at runtime, but tools like mypy can check for type errors statically.


2. Basic Type Hints

Python provides basic type hints for primitive types:

Type Example
int x: int = 10
float y: float = 3.14
str name: str = "Alice"
bool is_active: bool = True
list numbers: list[int] = [1, 2, 3]
tuple coords: tuple[int, int] = (10, 20)
dict user: dict[str, int] = {"age": 25}

numbers: list[int] = [1, 2, 3]
coordinates: tuple[int, float] = (10, 3.5)
user_data: dict[str, int] = {"age": 30, "score": 100}

3. Function Type Annotations

Example: Function with Type Hints


def multiply(numbers: list[int], factor: int) -> list[int]:
    return [n * factor for n in numbers]

4. Type Hints for Classes

Annotating Class Attributes

class Person:
    name: str
    age: int

    def __init__(self, name: str, age: int) -> None:
        self.name = name
        self.age = age

5. Type Checking at Runtime (isinstance)

While type hints do not enforce types at runtime, you can check types using isinstance():

def process(value: int | str) -> None:
    if isinstance(value, int):
        print(f"Processing an integer: {value}")
    elif isinstance(value, str):
        print(f"Processing a string: {value}")

process(42)
process("hello")

6. Advanced Typing Concepts

Multiple Allowed Types

If two or more types are allowed, you can separate them with |, as in the following example:

def process(value: int | str) -> None:
    print(value)

# so these would be valid:
process(1)
process("2")

# and these would not:
process(None)
process(True)
process([])
process((,))

This also applies if one of the valid types is None:

def accept_int_or_None(val: int | None) -> None:
    print(val)

Using Callable (Type Hint for Functions)

from typing import Callable

def apply(func: Callable[[int, int], int], x: int, y: int) -> int:
    return func(x, y)

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

print(apply(add, 2, 3))  # Output: 5

Using Any (Accepts Any Type)

If a function can accept any type, use Any:

from typing import Any

def echo(value: Any) -> Any:
    return value  # Can accept and return any type

Using aliases:

You can use aliases to avoid code duplication like this:

optional_number: int|float|complex|None

def foo(value: optional_number) -> optional_number:
    return value


7. Enforcing Type Checking with mypy

Even though Python does not enforce types at runtime, you can check them using mypy:

Install mypy:

pip install mypy

Run Type Checking:

mypy script.py

If there are mismatched types, mypy will report them.


8. Legacy typing uses

Before PIP 585 you had to import special type aliases and utilities from typing:

For more complex data structures, use typing module annotations:

from typing import List, Tuple, Dict
from typing import Union, Optional

numbers: List[int] = [1, 2, 3]
coordinates: Tuple[int, float] = (10, 3.5)
user_data: Dict[str, int] = {"age": 30, "score": 100}
a_number: Union[int, float] = 1
a_number = 1.1  # this would be allowed
discount: Optional[float] = 0.2

This is deprecated and you should use the newer style.


9. Summary