Docstrings, comments & types

Python Type Hints: From PEP 484 to Modern Syntax, with Style Rules

Docstrings, comments & types · · Published 2026-10-27

A single red jigsaw puzzle piece lying on top of a heap of green pieces on a wooden table
A red jigsaw piece on a pile of green ones. Photo: Profpcde, CC0.

Type hints let you write down what kind of values a function expects and returns. Python itself does not enforce them; separate tools read them to find mistakes before the code runs, and editors use them for completion. Since PEP 484 introduced them for Python 3.5, the syntax has become noticeably lighter.

What an annotation does, and does not do

An annotation is an expression attached to a parameter, a return value or a variable. Python evaluates and stores it, and otherwise ignores it:

def label(code: int) -> str:
    return f"#{code}"


print(label("A7"))
print(label.__annotations__)
#A7
{'code': <class 'int'>, 'return': <class 'str'>}

The call with a string runs without complaint. PEP 8 is explicit that this is by design: "Python interpreters by default should not issue any messages due to type checking and should not alter their behavior based on annotations." A type checker, run as a separate step, does report the mistake. mypy, given the same file, printed:

lab.py:5: error: Argument 1 to "label" has incompatible type "str"; expected "int"  [arg-type]

That is the whole bargain: you write a little more, and a tool catches a class of errors early, often ones that would otherwise surface far from their cause. Some libraries, such as data validation frameworks, do read annotations at runtime for their own purposes, but that is their choice, not the language's.

The syntax, step by step

The type hint PEPs added syntax in stages. Most code today can use the newest forms:

NeedModern formSince
Function signaturedef f(x: int) -> str:3.5 (PEP 484)
Variable annotationcount: int = 03.6 (PEP 526)
Built-in collectionslist[int], dict[str, float]3.9 (PEP 585)
Unions and optional valuesint | None3.10 (PEP 604)
Generic functions and classesdef first[T](items: list[T]) -> T:3.12 (PEP 695)

Older code spells the same ideas with imports from the typing module: List[int], Optional[int], Union[int, str] and TypeVar. They still work, but the built-in forms need no import.

A few other tools from typing come up constantly: TypedDict for dictionaries with a fixed set of keys (PEP 589), Protocol for structural "duck" typing, where any object with the right methods qualifies (PEP 544), Literal for specific values, and Final for names that should not be reassigned. The example below type-checks cleanly with mypy:

from typing import Protocol, TypedDict


def total(prices: list[float], discount: float | None = None) -> float:
    subtotal = sum(prices)
    if discount is not None:
        subtotal *= 1 - discount
    return round(subtotal, 2)


class Item(TypedDict):
    name: str
    price: float


class SupportsClose(Protocol):
    def close(self) -> None: ...


def first[T](items: list[T]) -> T:
    return items[0]


def shutdown(resource: SupportsClose) -> None:
    resource.close()

Style rules for annotations

PEP 8 has short rules for formatting annotations, which pycodestyle checks:

Beyond formatting, a few habits keep hints readable: give complex types a name with a type alias, prefer abstract types such as Iterable[str] or Mapping[str, int] for parameters so callers can pass any suitable object, and return concrete types.

When annotations are evaluated

Annotations used to be evaluated when the function was defined, which caused problems with forward references, names defined later in the module. Two mechanisms changed that. PEP 563 added from __future__ import annotations in Python 3.7, which stores annotations as strings. PEP 649, implemented in Python 3.14, makes deferred evaluation the default: annotations are computed only when something asks for them. In practice, on 3.14 you can refer to a class inside its own methods' annotations without quotes.

Type checkers

Several independent type checkers read the same annotations: mypy, the reference checker developed alongside PEP 484, as well as others including Microsoft's pyright. They mostly agree on the standard, but differ in defaults, speed and in how they treat unannotated code. mypy, for example, does not check the body of an unannotated function unless you pass --check-untyped-defs or enable strict mode.

For third-party libraries, checkers rely on annotations shipped with the package (marked by a py.typed file) or on separate stub files (.pyi) such as those collected in the typeshed repository, which PEP 8 mentions.

Adopting hints gradually

  1. Start with public interfaces: function signatures that other modules call. They give the most value to readers and tools.
  2. Run a checker in CI with permissive settings, then tighten module by module.
  3. Use escape hatches deliberately: Any where a type is truly dynamic, and # type: ignore[code] with the specific error code, never a bare ignore.
  4. Let types replace some documentation: once a signature says timeout: float, the docstring can explain the unit and edge cases instead of the type; see our guide to docstrings.

The checker on our home page reports E251 and E252 for spacing around defaults. Type errors themselves need a type checker; our comparison of flake8 and pylint explains where linting ends and type checking begins.

Sources