Skip to content
  • Type Hints & mypy · Python

Type Hints & mypy

LearnViaNotes @learnvianotes· 5 min read

Type hints describe what a function expects and returns. Python ignores them at runtime; static checkers (mypy, pyright) and IDEs use them to catch bugs before the code runs.

Annotation basics

Python
def greet(name: str, times: int = 1) -> str:
    return ", ".join([f"hi {name}"] * times)

count: int = 0
scores: dict[str, list[float]] = {"asha": [9.5, 8.0]}
point: tuple[int, int] = (3, 4)
ids: tuple[int, ...] = (1, 2, 3)      # variable-length tuple

print(greet("ravi", 2))               # hi ravi, hi ravi
print(greet.__annotations__)          # {'name': <class 'str'>, 'times': <class 'int'>, 'return': <class 'str'>}
print(greet(42))                      # hi 42   ← runs fine: hints are NOT enforced at runtime

The vocabulary

HintMeaning
list[int], dict[str, int], set[str]Built-in generics (3.9+) — no typing.List needed
int | None / Optional[int]May be None (the | form is 3.10+)
int | str / Union[int, str]One of several types
AnyOpt out of checking for this value
objectAny value, but you may only use what every object supports — safer than Any
Callable[[int, str], bool]A function taking (int, str) and returning bool
Iterable[T], Sequence[T], Mapping[K, V]Accept the abstract type (from collections.abc) in parameters
Literal["r", "w"]Only these exact values
FinalMust not be reassigned
ClassVar[int]A class-level attribute, not an instance field
SelfThe type of the current class — for fluent methods (3.11+)
Never / NoReturnFunction never returns normally (always raises)
Python
from collections.abc import Iterable, Callable
from typing import Literal

def total(xs: Iterable[float]) -> float:              # accepts list, tuple, generator…
    return sum(xs)

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

def open_mode(mode: Literal["r", "w"]) -> str:
    return f"mode={mode}"

print(total([1.5, 2.5]), total(x / 2 for x in range(3)))   # 4.0 1.5
print(apply(lambda n: n * n, 7), open_mode("w"))           # 49 mode=w

Structured data: TypedDict, NamedTuple, Protocol

Python
from typing import TypedDict, NotRequired, NamedTuple, Protocol

class Movie(TypedDict):
    title: str
    year: int
    rating: NotRequired[float]      # key may be absent

class Point(NamedTuple):
    x: float
    y: float

class SupportsClose(Protocol):      # structural: anything with close() matches
    def close(self) -> None: ...

def shutdown(things: list[SupportsClose]) -> int:
    for t in things:
        t.close()
    return len(things)

class Socket:
    def close(self) -> None:
        pass

m: Movie = {"title": "Dune", "year": 2021}
print(m["title"], Point(1, 2).x, shutdown([Socket(), Socket()]))   # Dune 1 2

Generics: TypeVar and the 3.12 syntax

Python
from typing import TypeVar

T = TypeVar("T")                          # pre-3.12 style
def first_old(xs: list[T]) -> T:
    return xs[0]

def first[T](xs: list[T]) -> T:           # 3.12+ style: same meaning
    return xs[0]

def largest[N: (int, float)](a: N, b: N) -> N:   # constrained type parameter
    return a if a > b else b

class Box[T]:
    def __init__(self, item: T) -> None:
        self.item = item
    def get(self) -> T:
        return self.item

type Matrix = list[list[float]]           # type alias statement (3.12+)

print(first_old(["a"]), first([1, 2]), largest(2.5, 1.0), Box("x").get())   # a 1 2.5 x
print(Matrix.__value__)                    # list[list[float]]

Overloads, Self, cast and TYPE_CHECKING

Python
from typing import overload, Self, cast, TYPE_CHECKING

if TYPE_CHECKING:                   # only True for the type checker — avoids import cycles
    from decimal import Decimal

@overload
def parse(x: str) -> int: ...
@overload
def parse(x: bytes) -> str: ...
def parse(x):                       # the one real implementation
    return int(x) if isinstance(x, str) else x.decode()

class Query:
    def __init__(self) -> None:
        self.parts: list[str] = []
    def where(self, cond: str) -> Self:   # subclasses get their own type back
        self.parts.append(cond)
        return self

def to_money(x: object) -> "Decimal":
    return cast("Decimal", x)       # tells the checker "trust me" — no runtime effect

print(parse("42"), parse(b"hi"), Query().where("a").where("b").parts)   # 42 hi ['a', 'b']

Running a type checker

Terminal
$ pip install mypy
$ mypy app.py                     # or: pyright app.py
$ mypy --strict src/              # require annotations everywhere
Python
# app.py
def average(xs: list[float]) -> float:
    return sum(xs) / len(xs)

average(["1", "2"])            # a bug mypy finds without running anything
mypy output
app.py:5: error: List item 0 has incompatible type "str"; expected "float"  [list-item]
app.py:5: error: List item 1 has incompatible type "str"; expected "float"  [list-item]
Found 2 errors in 1 file (checked 1 source file)

Annotations at runtime

Python
import typing

def make() -> Widget:          # Widget doesn't exist yet
    return Widget()

class Widget:
    size: int = 3

print(typing.get_type_hints(make))            # {'return': <class '__main__.Widget'>}
print(typing.get_type_hints(Widget))          # {'size': <class 'int'>}

Does Python enforce type hints?

Optional[int] vs a default of None?

What is a Protocol and how does it differ from an ABC?

Free for students and teachers

Write notes like this one

Cards for every idea, quizzes and flashcards, read aloud — on the web and your phone.

Something wrong with this note? Open it to report