Object-oriented Python and maintainable design · Lesson 3 of 3
Type hints and API contracts
Use annotations and structural typing to explain how Python code should be used.
Lesson
1. Concept and mental model
Type hints document the expected inputs and outputs of functions, attributes, and classes. They help editors, static analyzers, and reviewers detect mismatches before a program runs. Python does not normally enforce annotations at runtime, so do not treat a type hint as input validation for an HTTP request, uploaded file, or database value. Validate untrusted data at the boundary, then pass well-defined values deeper into the application.
2. How it works
Use common built-in annotations such as list[str], dict[str, int], and str | None when they describe the real contract. A Protocol can express structural typing: a component needs particular methods, regardless of its concrete class. This is often useful for test doubles and interchangeable storage or notification adapters. Avoid making signatures overly generic when a simpler type would be clearer.
3. Use it safely and effectively
Keep annotations accurate as behavior changes. A function that may fail should communicate failure through its documented exceptions, or return an explicit result type when callers need to branch on expected outcomes. Type hints improve communication but do not replace tests, code review, or runtime checks. Practice by annotating a function that consumes a sequence of prices and returns a numeric total, including how an empty sequence behaves.
4. Syntax reference
def average(values: list[float]) -> float | None:
if not values:
return None
return sum(values) / len(values)
print(average([2.0, 4.0]))5. Guided walkthrough
Annotate parameters and return values at the boundary where code is reused, then run a static type checker as part of development. A hint documents the expected shape and helps tools find mistakes, but runtime validation is still needed for untrusted input.
6. Common mistakes to avoid
- Assuming a type hint converts a string into a number or validates a request at runtime.
- Using Any everywhere, which removes many of the checks type annotations could provide.
Visual guide
Typed data boundary
-
Step 01
Receive
Read untrusted external data.
-
Step 02
Validate
Check the shape and allowed values.
-
Step 03
Annotate
Use clear types inside application logic.
Code examples
def average(values: list[float]) -> float | None:
if not values:
return None
return sum(values) / len(values)
print(average([2.0, 4.0]))
3.0
def user_label(user: dict[str, str]) -> str:
return f"{user['name']} ({user['role']})"
print(user_label({'name': 'Lee', 'role': 'editor'}))
Lee (editor)
def display_name(name: str | None) -> str:
return name if name is not None else 'Anonymous'
print(display_name(None))
Anonymous
Key points
- Use annotations and structural typing to explain how Python code should be used.
- Prefer explicit, testable behavior over hidden side effects.
- Validate assumptions at system boundaries and handle failure deliberately.
Check your understanding
Practice
Your learning progress