Skip to content

Practical Type Hint Patterns

Common patterns and best practices for applying type hints effectively in real-world Python code.

Mental Model

Practical type hinting follows a cost-benefit curve: annotate public API boundaries first (function signatures, class attributes), then work inward. Use TypedDict for JSON-shaped data, Protocol for duck-typed interfaces, and overload for functions with multiple signatures. The goal is not 100% coverage but maximum clarity at the points where bugs are most likely.

API Response Typing

Type hint API responses and data structures clearly.

```python from typing import TypeAlias from dataclasses import dataclass

@dataclass class User: id: int name: str email: str

ApiResponse: TypeAlias = dict[str, User]

def get_users() -> ApiResponse: return { "1": User(1, "Alice", "alice@example.com"), "2": User(2, "Bob", "bob@example.com") }

users = get_users() print(users["1"]) ```

User(id=1, name='Alice', email='alice@example.com')

Variadic Arguments

Type hint functions with variable arguments.

```python from typing import overload

@overload def concat(sep: str) -> Callable[[str, ...], str]: ...

def concat(*items: str) -> str: return ",".join(items)

result = concat("apple", "banana", "cherry") print(result) ```

apple,banana,cherry

Decorators with Generics

Type hint decorators using generics.

```python from typing import Callable, TypeVar, cast

F = TypeVar('F', bound=Callable)

def log_calls(func: F) -> F: def wrapper(args, kwargs): print(f"Calling {func.name}") return func(args, **kwargs) return cast(F, wrapper)

@log_calls def greet(name: str) -> str: return f"Hello {name}"

print(greet("Alice")) ```

Calling greet Hello Alice


Exercises

Exercise 1. Annotate a decorator retry(func) that takes a function with any signature and returns a function with the same signature. Use Callable and TypeVar (or ParamSpec if targeting Python 3.10+).

Solution to Exercise 1

```python from typing import Callable, TypeVar from functools import wraps

F = TypeVar("F", bound=Callable)

def retry(func: F) -> F: @wraps(func) def wrapper(args, kwargs): for attempt in range(3): try: return func(args, **kwargs) except Exception: if attempt == 2: raise return wrapper # type: ignore

@retry def fetch(url: str) -> str: return f"data from {url}" ```


Exercise 2. Write a @dataclass with type annotations for a Product with name: str, price: float, and quantity: int. Add a property total with a return type annotation.

Solution to Exercise 2

```python from dataclasses import dataclass

@dataclass class Product: name: str price: float quantity: int

@property
def total(self) -> float:
    return self.price * self.quantity

p = Product("Widget", 9.99, 3) print(p.total) # 29.97 ```


Exercise 3. Create a TypedDict called UserProfile with keys name (str), age (int), and email (str). Write a function that accepts this TypedDict and returns a formatted string.

Solution to Exercise 3

```python from typing import TypedDict

class UserProfile(TypedDict): name: str age: int email: str

def format_profile(user: UserProfile) -> str: return f"{user['name']} (age {user['age']}) - {user['email']}"

profile: UserProfile = {"name": "Alice", "age": 30, "email": "alice@test.com"} print(format_profile(profile)) ```


Exercise 4. Write a function process_items(items: Iterable[T]) -> list[T] using a TypeVar so that it preserves the element type. Demonstrate with both int and str inputs.

Solution to Exercise 4

```python from typing import TypeVar, Iterable

T = TypeVar("T")

def process_items(items: Iterable[T]) -> list[T]: return sorted(items)

ints: list[int] = process_items([3, 1, 2]) strs: list[str] = process_items(["c", "a", "b"]) print(ints) # [1, 2, 3] print(strs) # ['a', 'b', 'c'] ```