IntermediatePython · Lesson 7 of 9

Type Hints & mypy

Add types to functions and data, then let mypy find bugs before you run the code.

Type hints describe what a function accepts and returns. Python ignores them at runtime, but a type checker such as mypy (or your editor) reads them and reports mistakes like passing a str where an int is expected.

Common hints: list[int], dict[str, float], tuple[str, int], X | None for optional values, Literal["pending", "paid"] for fixed options, and TypedDict for dictionaries with known keys.

Add hints gradually — start with function signatures. Run mypy in your project (and in CI) the same way you run tests.

typed.pyPython
from typing import Literal, TypedDict

Status = Literal["pending", "paid", "failed"]


class Payment(TypedDict):
    id: int
    amount: int
    status: Status
    phone: str | None


def total_paid(payments: list[Payment]) -> int:
    return sum(p["amount"] for p in payments if p["status"] == "paid")


def find(payments: list[Payment], payment_id: int) -> Payment | None:
    for p in payments:
        if p["id"] == payment_id:
            return p
    return None


payments: list[Payment] = [
    {"id": 1, "amount": 15_000, "status": "paid", "phone": "0712345678"},
    {"id": 2, "amount": 8_000, "status": "pending", "phone": None},
]

print(total_paid(payments))
found = find(payments, 2)
if found is not None:                  # mypy forces this check
    print(found["status"])
Runs in your browser · Python
TerminalShell
pip install mypy
mypy --strict typed.py

Key points

  • Hints are checked by tools, not by Python at runtime.
  • X | None forces you to handle the missing case.
  • Run mypy regularly — it catches bugs tests may miss.

Exercise

Add full type hints to your Library exercise from the Classes lesson and run mypy --strict on it until it reports no errors.

Show solution

Try the exercise yourself first — then compare your approach with this one.

With every attribute, parameter and return value annotated, mypy --strict checks the whole module. Note Book | None for the lookup — mypy then forces the None case to be handled before using the book.

typed_library.pyPython
from dataclasses import dataclass


class BookAlreadyBorrowedError(Exception):
    pass


@dataclass
class Book:
    isbn: str
    title: str
    author: str
    available: bool = True


class Library:
    def __init__(self) -> None:
        self._books: dict[str, Book] = {}

    def add(self, book: Book) -> None:
        self._books[book.isbn] = book

    def find(self, isbn: str) -> Book | None:
        return self._books.get(isbn)

    def borrow(self, isbn: str) -> Book:
        book = self.find(isbn)
        if book is None:
            raise KeyError(f"no book with ISBN {isbn}")
        if not book.available:
            raise BookAlreadyBorrowedError(book.title)
        book.available = False
        return book

    def available_books(self) -> list[str]:
        return [b.title for b in self._books.values() if b.available]


library = Library()
library.add(Book("978-1", "Things Fall Apart", "Chinua Achebe"))
print(library.borrow("978-1").title, library.available_books())
Runs in your browser · Python
TerminalShell
mypy --strict typed_library.py
# Success: no issues found in 1 source file

Check your understanding

  1. What happens at runtime if you pass a str to a function hinted def f(x: int)?

  2. How do you write "a string or nothing" in a modern type hint?

  3. Which type describes a dictionary with fixed, known keys like id, amount and status?

  4. What does Literal["pending", "paid", "failed"] restrict a value to?

Ask AI