Python Type Hints: Writing Python Like You Mean It
Optional typing that pays for itself in the codebases you actually have to maintain

Python’s great selling point is that you can write a function in ten seconds without telling the interpreter anything about what goes in or comes out. Its great curse is that six months later you are staring at that same function trying to remember whether data is a list, a dict, or a generator that has already been consumed. Type hints are Python’s answer to its own success: a way of writing down what you meant, so that your tools, your colleagues, and your future self can hold you to it.
1 What hints actually do (and don’t)
The first thing to understand is that Python does not enforce type hints at runtime. The interpreter cheerfully ignores them. Annotate a parameter as int, pass it a string, and Python will shrug and run anyway until something downstream explodes. Hints are documentation that happens to be machine-checkable, and the machine doing the checking is a separate tool, usually mypy or your editor’s language server, not the interpreter.
That sounds like a weakness and it is the single most common reason people dismiss them. But it is also the design that let Python adopt typing gradually. You can annotate one function in a million-line codebase and nothing else has to change. The hints are opt-in, file by file, and they compose: a typed function calling an untyped one simply loses visibility at the boundary rather than erroring.
Here is the shape of it:
def average(values: list[float]) -> float:
if not values:
raise ValueError("average() of empty sequence")
return sum(values) / len(values)The : list[float] and -> float are the whole feature. Run mypy over that file and it will now flag callers that pass a dict, or that try to treat the result as a string.
2 The constructs worth knowing
Most real code needs more than int and str. The ones I reach for constantly are Optional, which is shorthand for “this or None”, and Union for “one of these”. As of Python 3.10 you can write both with the | operator, which reads far better:
def find_user(uid: int) -> User | None:
return _users.get(uid)That return type is a promise and a warning at once: callers must handle the None, and mypy will nag them until they do. This single pattern catches a startling proportion of the AttributeError: 'NoneType' object has no attribute failures that plague untyped Python.
For anything structural, dataclasses and TypedDict earn their keep. A dataclass turns a bag of attributes into a named, typed record with a free __init__, while TypedDict lets you describe the shape of a dictionary you are stuck with — the JSON blob from some API you cannot change. And when you want to accept “anything that behaves like a file” rather than a specific class, Protocol gives you structural typing: define the methods you need, and any object with those methods qualifies, no inheritance required.
3 Living with mypy
Adding hints is easy; running the checker honestly is where the discipline lives. My advice is to start lax and tighten over time. Drop a mypy.ini in your project and begin permissive:
[mypy]
python_version = 3.10
warn_unused_ignores = True
warn_return_any = True
[mypy-thirdparty.*]
ignore_missing_imports = TrueThat last stanza is the pragmatic escape hatch for libraries that ship no type information. You will need it more than you would like, though the situation has improved enormously as popular packages bundle their own stubs.
The trap is Any. It is the type that means “stop checking”, and it spreads. One Any returned from a function quietly disables type checking for everything that touches the result. warn_return_any exists precisely to catch this. Treat Any as a confession, not a tool — sometimes necessary at the edges of your system, never something to scatter through the core.
4 Is it worth it?
For a thirty-line script you will run once and delete, no. The ceremony outweighs the payoff, and Python’s whole appeal there is the lack of ceremony. Don’t let anyone shame you into annotating a throwaway.
For anything that will outlive the afternoon — a library, a service, anything more than one person touches — type hints are among the highest-leverage habits you can pick up. They turn a category of runtime crashes into squiggly red underlines you see before you ever hit save. They make refactoring sane, because the checker tells you everywhere a signature change ripples to. And they document intent in a way comments never manage, because comments lie and types are verified.
The learning curve is gentle precisely because it is optional. Annotate the function that bit you last week, run mypy, and feel the small satisfaction of being told off by a robot before your users have the chance. That is Python written like you mean it.




