Introduction to Type Hints
Add annotations that document real contracts, run a static checker, and model records with dataclasses.
By the end, you’ll be able to…
- Explain what annotations do and do not enforce at runtime.
- Annotate parameters, return values, and optional data precisely.
- Run mypy and interpret its most common errors.
- Use dataclasses for typed records with safe defaults.
01Annotations describe a contract
Python remains dynamically typed: an annotation is metadata, not a runtime guard. A function declared to return float can still return a string unless your code prevents it. The value of the annotation is that readers, editors, and static checkers can reason about the intended contract before the program runs.
Annotate the boundary callers depend on: parameters, return values, and public attributes. Internal local variables rarely need annotations because the checker can infer them. Start with the functions that exchange data between modules; those are the contracts most likely to be misunderstood.
02Choose precise types
Use int, float, str, bool, and None for ordinary values. A value that may be absent is written X | None in modern Python, or Optional[X] in older code. Collections are described by their contents: list[str], dict[str, int], and tuple[str, int] say much more than a bare list or dict.
Prefer the abstract input type when the function only reads. A function that iterates over a sequence can accept Sequence[str] instead of list[str], which lets callers pass a tuple or another sequence. The return type should be concrete enough for callers to use without guessing.
03Run a static checker
mypy reads annotations without executing the program. Install it into the project environment and run it against a file or package. A typical error names the expected type, the received type, and the line. Fix the implementation or the annotation, whichever is actually wrong; do not silence the error reflexively.
Adopt checking gradually if a codebase has no annotations. Start with the modules that change most often, add return types first, and keep the checker green. A strict mode that reports hundreds of pre-existing errors is less useful than a smaller, enforced contract.
py -m pip install mypypy -m mypy average.pypy -m mypy --strict package/04Dataclasses make typed records
A dataclass generates initialization, representation, and equality methods from field declarations. It pairs naturally with annotations: the same declaration documents the record and gives the checker something to verify. Use field(default_factory=list) for a mutable default, because a plain [] would be shared by every instance.
Use frozen=True when the record should not change after construction, and slots=True when you want a compact object with a fixed set of attributes. A dataclass does not validate field values by default; add __post_init__ checks or use a validation library at the boundary when the rules matter.
The ideas worth keeping.
- Annotations document contracts; they do not enforce runtime behavior.
- Precise collection and optional types make interfaces easier to use.
- Dataclasses pair typed fields with generated initialization and representation.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Annotate a price parser
Write parse_price(text: str) -> float that strips whitespace, rejects an empty string, converts to float, and rejects a negative value. Include annotations and demonstrate a successful call.
Explore a worked solution
def parse_price(text: str) -> float: cleaned = text.strip() if not cleaned: raise ValueError("price cannot be empty") value = float(cleaned) if value < 0: raise ValueError("price cannot be negative") return valueprint(parse_price(" 19.95 "))The annotation states the accepted input and the promised result. The checks enforce the parts a type checker cannot: non-empty text and a nonnegative amount.
Model a task with safe defaults
Define a Task dataclass with title: str, done: bool = False, and tags: list[str] using field(default_factory=list). Create two tasks and prove they do not share the same tag list.
Explore a worked solution
from dataclasses import dataclass, field@dataclassclass Task: title: str done: bool = False tags: list[str] = field(default_factory=list)first = Task("Read")second = Task("Write")first.tags.append("docs")print(first.tags, second.tags)The output is ['docs'] [] because default_factory creates a new list for each instance. A shared default would make the second task appear to have the first task's tags.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Annotate a price parser
Write parse_price(text: str) -> float that strips whitespace, rejects an empty string, converts to float, and rejects a negative value. Include annotations and demonstrate a successful call.
parse_price(" 19.95 ")Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
One lesson closer.
Reading is a start. Trying it yourself makes the connection.