Type hints & static typing
Annotations, unions and Optional, collections.abc types, generics, Protocols and checking code with mypy.
Python is dynamically typed, and that won't change. But since Python 3.5 you can annotate variables, parameters and return values with their expected types. The interpreter ignores these hints at runtime, yet they're enormously valuable: editors autocomplete better, type checkers like mypy and pyright catch bugs before you run anything, and readers understand your code faster. Most professional codebases use them, and frameworks like FastAPI and Pydantic even use them at runtime.
The basics#
- Parameters:
name: type, with defaults after:times: int = 1. - Return type:
-> typebefore the colon. Use-> Nonefor functions that don't return anything. - Since Python 3.9 you use the built-in collection types directly with square brackets:
list[str],dict[str, int]. Older code importsList,Dictfromtyping— still valid, but no longer needed.
Hints are not enforced at runtime#
That's where a type checker comes in.
Checking types with mypy#
Install mypy into your virtual environment and run it on your code:
Both bugs were found without running the program. VS Code's Pylance extension (based on pyright) shows the same errors as red squiggles while you type. Many teams run mypy or pyright in CI on every pull request.
Optional values and unions#
str | None (Python 3.10+) means "a string or None" — the older spelling is Optional[str]. The is not None check narrows the type: type checkers understand isinstance and None checks, so they'll warn you if you call .upper() without handling None first. This alone prevents a huge class of AttributeError: 'NoneType' object has no attribute ... bugs.
Useful types from typing and collections.abc#
Tip: accept abstract types in parameters (Iterable, Sequence, Mapping) so callers can pass any suitable container, and return concrete types (list, dict) so callers know exactly what they get.
Type aliases and generics#
Python 3.12 added a clean syntax for type aliases and generic functions/classes:
T is a type variable: first_or_default([3, 4], 0) is known to return an int, while a list of strings gives a str. In code that must support Python 3.11 or older, you'll see the equivalent T = TypeVar("T") and class Stack(Generic[T]).
Protocols: static duck typing#
Python favours duck typing — "anything with a .read() method". A Protocol expresses that to the type checker without forcing inheritance:
Any class with a matching draw() method is accepted. Pass an object without one, and mypy reports an error.
Annotating classes#
Self (3.11+) is the right return type for methods that return their own instance, so chaining works correctly in subclasses too.
Gradual typing: how much to annotate#
You don't have to annotate everything at once. Sensible guidelines:
- Annotate function signatures (parameters and returns), especially public ones. That's where most value comes from.
- Let the checker infer local variables:
count = 0is obviously anint. - Annotate empty containers, which can't be inferred:
items: list[str] = []. - Use
Anysparingly as an escape hatch. - Turn on stricter mypy settings over time (
mypy --strict), typically configured inpyproject.toml.
Runtime uses of type hints#
Although Python ignores hints when running ordinary code, libraries can read them. dataclasses uses them to find fields; Pydantic and FastAPI use them to validate and convert incoming data:
Common mistakes#
- Expecting runtime enforcement — use a type checker (or Pydantic for untrusted input).
- Annotating with values instead of types:
def f(x: 5)oritems: []. - Forgetting
| Nonewhen a function can returnNone. - Overusing
Any, which silently turns checking off. - Mutable default with a hint:
items: list[str] = []in a function signature is still the shared-default bug — useitems: list[str] | None = None.
What's next#
You've finished the intermediate section! The advanced section starts with a tool you'll use for searching, validating and cleaning text: regular expressions.
Check your understanding
Quick quiz
1.What does Python do at runtime if you call
def double(n: int) -> intwith a string?2.How do you annotate a parameter that can be a string or
Nonein modern Python?3.What is a
Protocolused for?
Finished reading?
Mark this lesson complete to track your progress.