0% complete

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

  1. Step 01 Receive

    Read untrusted external data.

  2. Step 02 Validate

    Check the shape and allowed values.

  3. Step 03 Annotate

    Use clear types inside application logic.

Code examples

Worked example
PYTHON
def average(values: list[float]) -> float | None:
    if not values:
        return None
    return sum(values) / len(values)

print(average([2.0, 4.0]))
Expected output
3.0
Describe a structured return value
PYTHON
def user_label(user: dict[str, str]) -> str:
    return f"{user['name']} ({user['role']})"

print(user_label({'name': 'Lee', 'role': 'editor'}))
Expected output
Lee (editor)
Use a union for optional data
PYTHON
def display_name(name: str | None) -> str:
    return name if name is not None else 'Anonymous'

print(display_name(None))
Expected output
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

Do Python type hints automatically validate external JSON at runtime?

Your learning progress

Finish this lesson