Annotations That Describe, Don't Enforce
A type hint is a note you attach to a name — usually a function parameter — saying "this should be an int," "this returns a list of strings," and so on. Python doesn't check them at runtime. Passing a string where you annotated an int doesn't raise an error. Your editor and external tools (mypy, pyright, Pyright-via-VS-Code's Pylance) read the hints and warn you before the code runs.
The simplest possible case:
name: str annotates the parameter. -> str annotates the return value. Both calls run. The second one is wrong — a static type checker would flag it — but Python itself happily processes it because 42 happens to support f"{...}" interpolation.
That's the crucial mental model: hints are documentation a machine can read. They don't change the runtime.
Why Bother?
Three concrete wins, in order of how quickly they pay off:
- Your editor gets smarter. Autocomplete shows the right methods, renames propagate correctly, and hovering a variable tells you its type.
- Function signatures become self-describing.
def fetch(url: str, timeout: float = 5.0) -> dict:tells a reader exactly what to pass in and what they'll get back — no need to read the body. - Type checkers catch mistakes before you run the code. Running
mypy .on a project surfaces the kind of bugs unit tests often miss —Nonereturned where you expected a value, a dict used where a list belongs.
For a one-file script that's only you and only for today, skip the hints. For anything you'll come back to or share, the fifteen seconds they take to write pay off within the hour.
Basic Built-In Types
You don't need an import for any of these:
Variable annotations (name: str = "Rosa") are rarely necessary — Python infers the type from the right-hand side. Keep them for parameters, return types, and the occasional case where the inferred type is ambiguous.
Functions that return nothing use -> None:
Lists, Dicts, Tuples, and Sets
Containers need a second piece of information — what they contain. Modern Python lets you subscript the built-in types directly:
Reading them out loud:
list[float]— a list of floats.dict[str, int]— a dict with string keys and int values.tuple[float, float]— a tuple of exactly two floats.set[str]— a set of strings.
The list[...], dict[...] subscripting syntax works in Python 3.9 and later. In older code you'll see List, Dict, Tuple imported from typing — same meaning, older spelling.
Optional Values
"Might be None" is common. It has two equivalent spellings — both are fine, but the newer one reads better:
str | None means "a string, or None." The | syntax works in Python 3.10+. In older code you'll see Optional[str] from the typing module, which means the same thing.
A caller who sees -> str | None knows to check for None before using the result — that's the whole point of the annotation.
Union Types: This Or That
When a value could be one of several types, use |:
You can union more than two types. int | str | float means "any of these three."
Annotating Variables Inside Functions
Most of the time Python can figure out a local variable's type from its initializer. You only need an annotation when:
- The container starts empty and the type checker can't guess its contents.
- The value could be several types and you want to commit to one.
- You want to document the intent for a human reader.
typing.Any is the escape hatch — "I don't want to annotate this precisely." Use it sparingly. Abuse of Any makes the rest of your type hints worthless.
Annotating Classes
Class attributes and method signatures annotate the same way as any other function:
Dataclasses actually require type annotations — the @dataclass decorator reads them to generate __init__ and __repr__. That's the one place annotations do affect runtime behavior.
Tuples and the "Any Length" Case
tuple[...] has two shapes that confuse newcomers:
tuple[float, float]— exactly two floats.tuple[int, ...]— any number of ints. The...(a real syntax element in the type system) means "and so on."
Callables and Type Aliases
When a function takes or returns another function, use Callable:
Callable[[int], int] means "a function that takes one int and returns an int."
When an annotation gets repetitive, name it:
An alias is just a regular Python assignment. Anywhere you'd use the long form, the short name works.
Running a Type Checker
Python's own interpreter ignores type hints. To actually check them, install a type checker. mypy is the original; pyright (used by VS Code's Pylance) is faster.
pip install mypy
mypy your_project/
The first run will surface errors in places you hadn't realised. Work through them incrementally — # type: ignore silences a single line when you need to move on.
Modern IDEs run type checking continuously as you edit, so most feedback arrives before you save.
When Type Hints Don't Fit
- Quick exploratory scripts. Annotations add friction to code that lives for an hour.
- Heavily dynamic code. Metaprogramming, plugin systems, and similar patterns often outgrow what the type system can describe. Annotate the outer API and let the internals stay loose.
- Third-party libraries without types. If a library you import has no type info,
Anyleaks into your code. Fine — it's not your code to annotate.
For everything in between, type hints are a small habit with a big payoff. The cost is a few extra keystrokes per function signature. The return is fewer bugs, easier refactors, and code that documents itself.
Next: Modules and Imports
You've now got all the function-level tooling — arguments, decorators, type hints. Next we'll look at how Python organises code across files: modules, packages, and the import system.
Frequently Asked Questions
What are type hints in Python?
Type hints are annotations that describe the expected types of variables, function parameters, and return values. Python itself doesn't enforce them at runtime — they're for tools (IDEs, linters, type checkers like mypy or pyright) and for humans reading the code.
Do type hints make Python run faster?
No. The Python interpreter ignores type hints at runtime. The speedup is in your development loop — fewer typos caught by editors, clearer function signatures, safer refactors.
When should I add type hints?
Add them to public function signatures — parameters and return types. Add them sparingly inside function bodies, only where a variable's type isn't obvious. For one-off scripts, they're optional. For shared code and libraries, they pay for themselves quickly.