Definition
You come across this call in a file you did not write: gross_price(amount, 0.2). Is amount a number, a string pulled from a form, an order object? The name does not say: to find out, you have to open the function and read its body. Type hints exist to cut that investigation short.
A hint states what kind of value a name is meant to hold. It is written after a colon for a variable or a parameter, and after an arrow for whatever a function hands back. Here is the function called above, this time annotated.
def gross_price(net_price: float, rate: float = 0.2) -> float:
return round(net_price * (1 + rate), 2)The signature now answers on its own: two decimal numbers go in, one decimal number comes out.
One point surprises everyone: Python reads that line, files it away, and stops there. Calling the function with a string raises no warning, until the multiplication fails much further along with a TypeError. A hint documents an intention, it never enforces one.
What Python does with them, and what it does not
If the interpreter checks nothing, what are they for? Python files the hints in a dictionary attached to the object, __annotations__, and leaves them available. The whole benefit comes from outside programs that read it.
The first is the type checker. mypy reads the entire project without running it and reports the types that do not line up: a sometimes empty return treated as if it never were, two arguments in the wrong order. The error shows up in your terminal, before the first run, rather than on a user's screen.
The second is your editor: annotate the parameters, and completion offers the right methods while you type.
A few libraries, finally, genuinely read them at run time. A dataclass builds its fields from the hints on the class, and Pydantic uses them to reject incoming data that does not fit. The hint becomes code that acts: getting it wrong no longer costs a confusing read, but a bug.
The shapes you will meet
Six forms keep coming back, and a first week is enough to meet them all.
| What you annotate | Writing |
|---|---|
| A variable | counter: int = 0 |
| A parameter | def send(recipient: str) |
| A return | -> bool |
| A function returning nothing | -> None |
| A collection | list[str] or dict[str, int] |
| A value sometimes missing | str | None |
What a collection holds deserves to be spelled out: that is where a hint stops being decorative. A list on its own says there is a list; list[str] says what will be found inside, and therefore what may be done with it. Same logic for a dict, keys and values separately.
These forms depend on the version: list[str] with no import requires Python 3.9, str | None requires 3.10. On a server still running 3.8, the file simply refuses to import, and the List[str] and Optional[str] equivalents from the typing module exist for those cases.
The trap: the hint that lies
Since nothing checks hints, nothing stops them from ageing. The story is always the same: the function is annotated correctly, then one more return case is added six months later without anyone fixing the signature.
def find_client(identifier: int) -> dict:
row = database.read(identifier)
if row is None:
return None # the signature promised a dictionary
return rowThe code runs and nobody complains. The breakage happens elsewhere: the caller read the signature, concluded a dictionary was coming, and reads a key from the result. The day the client does not exist, the error surfaces in the caller, far from the guilty function.
The accurate hint would be dict | None: it makes the None visible in the signature and forces the caller to plan for the empty case. When wrong, hints are worse than absent: they are read as a guarantee.
Should they be everywhere?
No, and common practice has settled on a simple split: boundaries benefit, the inside of a function does not. A boundary is anywhere called without being read, starting with what a module exposes to the rest of the project. A loop variable that lives three lines teaches nobody anything.
One last misunderstanding: hints do not cancel the duck typing that makes Python flexible, they describe it. Annotating a parameter with a protocol rather than a precise class amounts to writing "anything that knows how to do this".
Frequently asked questions
Do type hints slow the program down?
Negligibly: they are evaluated once, when the module loads, then filed in a dictionary that nothing reads again. If that cost matters, PEP 563 allows them to be treated as plain text and their evaluation postponed.
What can be done when the type is too complicated to write?
Start broad and tighten later: list beats nothing, and list[dict] beats list. When the writing turns unreadable, it is the data structure that deserves a class of its own, not the hint that deserves a cleverer piece of writing.
Does a hint replace the docstring?
No, they do not say the same thing: the hint gives the shape of the value, the docstring its meaning, its unit and the conditions for calling it. A return announced as a decimal number will never say whether it holds euros or seconds. The Python course shows both at work on the same code.