Nine lessons ago you learned that Python is dynamically typed: types live with values, and variables are just names. That freedom is why Python is easy to start, and it has a cost that grows with your programs: a function called process(data) tells you nothing about what data must be, and the wrong guess becomes a TypeError at runtime, usually at the worst time. Type hints are Python's answer, and they are one of the best deals in the language: write down what you already know about your functions, and in exchange your editor catches bugs as you type, autocomplete becomes precise, and your code documents itself forever.
Two facts frame everything in this lesson. First, hints are optional and gradual; you can annotate one function in a ten-thousand-line program and benefit immediately. Second, Python itself ignores them at runtime, no speed change, no enforcement; checking is done by your editor or a dedicated tool reading your code without running it. Hints are communication, not control, and modern professional Python, including every library you will meet from here to the end of this series, is written with them.
What you will learn in Part 10
- Annotating variables, parameters, and return values
- Hinting collections: list[int], dict[str, float], and friends
- Optional values, unions, and the meaning of None in signatures
- Type aliases and hinting functions passed as values
- Running a type checker, and what it catches before you run anything
- How hints power dataclasses, editors, and the wider ecosystem
Note
Before you start
You need functions from Part 4, collections from Part 5, and dataclasses from Part 6, where you already wrote your first hints without ceremony. This lesson makes that syntax precise.
1. The basic annotations
The syntax is a colon after a name and an arrow before a return. Parameters take name colon type; the return type follows the parentheses with ->; variables can be annotated at assignment when the type is not obvious. That is most of the grammar already. Read the example below and notice how much the signature now communicates: what goes in, what comes out, no docstring archaeology, no guessing.
def tax_due(income: float, rate: float = 0.12) -> float:
return income * rate
def grade_for(score: int) -> str:
return "A" if score >= 75 else "B" if score >= 65 else "C"
def log_event(message: str) -> None: # returns nothing, says so
print(f"[event] {message}")
threshold: float = 0.5 # variable annotation
Run that file and Python behaves exactly as if the hints were absent; annotate income as float and pass a string, and Python will not stop you, but your editor will, with a red underline at the call site, before you have run anything. That shift, from discovering mistakes at runtime to seeing them at write time, is the entire value proposition, and it compounds: every annotated function makes every caller easier to check.
2. Hinting collections
Collections take their content types in square brackets, and since Python 3.9 the built-in names themselves do the job: list[int] is a list of integers, dict[str, float] maps strings to floats, set[str] and tuple[str, int] follow the pattern. Tuples deserve one extra note: their hints describe each position, because tuples are fixed-shape records, so tuple[str, int] is a pair of a name and a score, the exact shape you returned from functions in Part 4. Older code spells these List and Dict imported from typing; read it as the same thing in period costume.
def average(scores: list[int]) -> float:
return sum(scores) / len(scores)
def count_words(text: str) -> dict[str, int]:
counts: dict[str, int] = {}
for word in text.split():
counts[word] = counts.get(word, 0) + 1
return counts
def best_student(records: list[tuple[str, int]]) -> tuple[str, int]:
return max(records, key=lambda r: r[1])
print(best_student([("Amina", 87), ("Zane", 91)])) # ('Zane', 91)
Checkpoint
What does the hint dict[str, list[int]] describe?
3. None, Optional, and unions
Real functions sometimes return a value or nothing: a search that may not find, a parse that may fail politely, the get method from Part 5. The modern spelling is a union with the pipe character: str | None reads "a string or None", and it is a contract with the caller that the None case exists and must be handled. Unions generalize beyond None: int | float accepts either number type. When you annotate honestly with | None, type checkers will actually force callers to handle the missing case before using the value, converting a whole genus of AttributeError crashes into squiggles at write time.
def find_student(records: dict[str, int], name: str) -> int | None:
return records.get(name) # int when present, None when not
records = {"Amina": 87, "Zane": 91}
score = find_student(records, "Ruwan")
if score is None: # the checker insists on this guard
print("not found")
else:
print(f"score is {score + 0}") # safe: here it is definitely int
Two more tools round out the everyday kit. A type alias gives a complicated hint a name: Records = dict[str, list[int]] turns long signatures legible. And because Part 4 taught you to pass functions as values, hints can describe those too, with Callable: a sort key that takes a record and returns something comparable is key: Callable[[tuple[str, int]], int]. You will write aliases regularly and Callable occasionally; recognize both on sight in library docs.
Adopt hints with a strategy rather than guilt. The highest-value targets are boundaries: the public functions other code calls, the parsing functions where outside data enters, and anything that returns a union. Annotate those first, let inference handle the locals in between, and resist the escape hatch called Any, the type that means "stop checking here"; every Any is a hole in the net, and holes spread, because whatever touches an Any becomes unchecked too. A partially hinted codebase with honest boundaries beats a fully hinted one stuffed with Any, and checkers have strictness flags that will tell you exactly where the holes are when you are ready to close them.
4. Running a type checker
Editors check as you type, and the same analysis runs as a command-line gate. The two main tools are mypy, the original, and pyright, the engine inside VS Code; for a learner they are interchangeable. Install one, point it at your file, and it reads every annotation and every usage, then reports the contradictions, all without executing a single line, which means it checks the branches your tests forgot and the error paths that only happen in production at 3 a.m. Here is the kind of session that converts people.
$ pip install mypy
$ mypy gradebook.py
gradebook.py:14: error: Argument 1 to "average" has incompatible type
"list[str]"; expected "list[int]"
gradebook.py:22: error: Item "None" of "int | None" has no attribute
"__add__" [union-attr]
Found 2 errors in 1 file (checked 1 source file)
Both reported lines are real bugs that would have crashed at runtime, one of them only on the day a student was missing from the records. The checker found them in milliseconds, from the hints alone. Professional teams run exactly this command in their automated checks so unhinted lies cannot merge, a practice you will see again in Part 13 when testing joins the pipeline.
Checkpoint
A function is hinted to return str | None. The checker flags result.upper() right after the call. Why?
Hints also change how refactoring feels, and this is the benefit nobody appreciates until they have it. Rename a field, change a return type, split a function, and the checker instantly lists every call site that no longer agrees, across the whole project, including files you forgot existed. Without hints, that list is assembled by running the program and collecting crashes one by one; with them, it is a compile-time todo list. Combined with the tests you will write in Part 13, hints are what make changing old code feel routine instead of frightening, and codebases that feel safe to change are the ones that stay alive.
5. Hints in the ecosystem: dataclasses and beyond
You have already benefited from hints without noticing: the dataclasses in Part 6 are driven by them, reading the annotations to generate __init__ and friends. The pattern scales up through the entire modern Python stack. FastAPI builds whole web APIs from function signatures; Pydantic validates real-world data against annotated models at runtime, hints with teeth; and editors use the same information for the autocomplete you have been enjoying all course. When you finish this series, the Pydantic v2 deep dive in our production track shows just how far this one idea carries.
from dataclasses import dataclass
@dataclass
class Student: # hints ARE the field definitions
name: str
score: int
tags: list[str] | None = None
def top_scorers(students: list[Student], cutoff: int = 85) -> list[str]:
return [s.name for s in students if s.score >= cutoff]
squad = [Student("Amina", 87), Student("Zane", 91), Student("Ruwan", 78)]
print(top_scorers(squad)) # ['Amina', 'Zane']
Two more names complete your reading vocabulary for real codebases. TypedDict describes dictionaries with a fixed set of string keys, each with its own value type, the natural hint for the JSON-shaped data of Part 11: class UserPayload(TypedDict): name: str; age: int. And NamedTuple is the typed sibling of Part 5's tuples, giving positions names and types at once. Both are recognition knowledge today and become writing knowledge the first time a function returns a dictionary whose shape matters; the moment you catch yourself documenting keys in a comment, the language has a better tool waiting.
6. Practice: annotate and catch the bugs
The playground below contains working but unannotated code with three latent bugs that hints would expose. Your exercises: annotate every function, find the contradictions by reading like a checker, then fix them. Python executes hints as no-ops in the playground, so everything runs; the exercise is in your head, which is exactly where type checking lives. The final exercise peeks at annotations at runtime, just to prove they are ordinary, inspectable data.
! Common mistakes to avoid
-
✕Believing hints make Python check types at runtime.
✓Plain Python ignores them; enforcement comes from your editor or mypy/pyright. If you need runtime validation of outside data, that is Pydantic's job, built on these same hints.
-
✕Hinting everything as Any, or skipping returns.
✓Any silences the checker and spreads through everything it touches. Annotate honestly, and prefer precise types; an unhinted function is better than a falsely hinted one.
-
✕Writing list instead of list[int] and losing the content checking.
✓Bare list accepts a list of anything. The bracket parameter is where most of the protection lives; include it.
-
✕Forgetting that returning nothing means -> None, not no annotation.
✓Annotate side-effect functions with -> None explicitly. It tells callers not to use the result, and checkers flag the x = log_event(...) mistake instantly.
? Frequently asked questions
Do type hints slow my program down? +
No. They are stored as metadata and skipped by the interpreter. The cost is keystrokes; the payback is editor intelligence and pre-run bug discovery.
Should beginners really bother with hints? +
Yes, and early. Hints force you to know what your functions accept and return, which is half of design. Annotating signatures only, not every variable, gives ninety percent of the value for minutes of work.
What is the typing module I see imported everywhere? +
The home of advanced hint tools and the old spellings (List, Dict, Optional) from before the builtins learned brackets. Modern code needs it less, mostly for Callable, TypeVar, and friends. Read Optional[str] as str | None.
mypy or pyright? +
Either. Pyright ships inside VS Code's Python experience, so you may be using it already; mypy is the longtime standard in CI. They disagree only at the margins, far beyond this course's needs.
7. Recap and what comes next
Your functions now declare their contracts: parameter and return annotations, content-typed collections, honest unions with None, aliases for readability, and a checker that audits the whole program without running it. You also saw the hints you wrote in Part 6 power dataclasses, the first of many ecosystem tools that run on this fuel. From here on, every signature in this series carries hints, because from here on you can read them fluently.
Next, the course turns outward to the data formats the world actually speaks: Part 11, regex, JSON, and CSV, where messy text becomes structured data and back. The Type Hints lesson in the Learn Python app below has signature-reading drills, and the whole syllabus is on the series hub.
Pro tip
Adopt one habit today: never write def without finishing the signature, parameters and return, hints included. Thirty extra seconds per function, and you will never again open six-month-old code and wonder what process(data) wanted from you.
Practice on the go
Learn Python, the free Android app
Every topic in this series lives in the app too: bite-size lessons, runnable examples, quizzes, mini projects, and an offline Python playground that runs on your phone.
Comments
0No comments yet. Be the first to share your thoughts.