Type Hints
Type hint patterns and mypy best practices for statically typed Python code
You are an expert in Python type hints and mypy for writing statically verifiable, self-documenting code.
## Key Points
- **Type annotations** on function signatures and variables declare expected types without enforcing them at runtime.
- **Generics** (`list[int]`, `dict[str, Any]`) parameterize container types.
- **`TypeVar`** and **`ParamSpec`** define generic functions and decorators that preserve type relationships.
- **`Protocol`** (structural subtyping) defines interfaces without inheritance.
- **`TypeAlias`**, **`TypeGuard`**, and **`TypedDict`** handle aliases, narrowing, and structured dicts.
- **`mypy --strict`** enables all optional checks for maximum safety.
- Use modern syntax (`X | Y` instead of `Union[X, Y]`, `list[int]` instead of `List[int]`) when targeting Python 3.10+.
- Use `from __future__ import annotations` on Python 3.9 to enable modern syntax via PEP 563.
- Run mypy in CI with `--strict` or at least `--warn-return-any --disallow-untyped-defs`.
- Use `Protocol` over ABC when you want structural ("duck") typing without forcing inheritance.
- Annotate public APIs fully; internal helper functions benefit too but are lower priority.
- Use `Final` for constants and `ClassVar` for class-level attributes to distinguish from instance attributes.
## Quick Example
```python
from typing import TypeAlias
JSON: TypeAlias = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
Callback: TypeAlias = Callable[[int, str], None]
Matrix: TypeAlias = list[list[float]]
```skilldb get python-patterns-skills/Type HintsFull skill: 202 linesType Hints — Python Patterns
You are an expert in Python type hints and mypy for writing statically verifiable, self-documenting code.
Overview
Python's type annotation system (PEP 484+) enables static type checking with tools like mypy, pyright, and pytype while preserving Python's dynamic runtime. Type hints improve code readability, catch bugs before runtime, and power IDE autocompletion. Modern Python (3.10+) has streamlined annotation syntax significantly.
Core Philosophy
Type hints in Python occupy a unique position: they are optional annotations that carry no runtime enforcement, yet they fundamentally change how you write, read, and maintain code. The value is not in catching every possible type error — Python's dynamism makes that impossible — but in making your code's contracts explicit. When a function declares def process(items: list[str]) -> dict[str, int], every reader (human and machine) immediately understands what goes in and what comes out.
Gradual typing is the key principle. You do not need to annotate everything at once, and you should not try. Start with your public APIs — the functions and classes that other modules import and call. These are the contracts that matter most and where type errors cause the most confusion. Internal helpers, closures, and throwaway scripts benefit from annotations too, but they are lower priority. The goal is to increase type coverage steadily over time, guided by the type checker's feedback.
Type hints should serve the developer, not the other way around. When you find yourself wrestling with complex generic types, nested Callable signatures, or circular import gymnastics just to satisfy the type checker, step back and ask whether the code design itself is too complex. Often, a difficult-to-type function is a function with too many responsibilities or an overly clever API. Let the type system guide you toward simpler, more explicit designs rather than treating it as an obstacle to work around.
Core Concepts
- Type annotations on function signatures and variables declare expected types without enforcing them at runtime.
- Generics (
list[int],dict[str, Any]) parameterize container types. TypeVarandParamSpecdefine generic functions and decorators that preserve type relationships.Protocol(structural subtyping) defines interfaces without inheritance.TypeAlias,TypeGuard, andTypedDicthandle aliases, narrowing, and structured dicts.mypy --strictenables all optional checks for maximum safety.
Implementation Patterns
Basic annotations
def greet(name: str, times: int = 1) -> str:
return (f"Hello, {name}! " * times).strip()
# Variable annotations
count: int = 0
names: list[str] = []
config: dict[str, int | str] = {}
Union and Optional (modern syntax)
# Python 3.10+
def find_user(user_id: int) -> User | None:
...
def process(value: int | str) -> str:
if isinstance(value, int):
return str(value)
return value
TypeVar for generic functions
from typing import TypeVar, Sequence
T = TypeVar("T")
def first(items: Sequence[T]) -> T:
return items[0]
# Bounded TypeVar
Numeric = TypeVar("Numeric", int, float)
def add(a: Numeric, b: Numeric) -> Numeric:
return a + b
Protocol for structural typing
from typing import Protocol, runtime_checkable
@runtime_checkable
class Renderable(Protocol):
def render(self) -> str: ...
class HtmlWidget:
def render(self) -> str:
return "<div>widget</div>"
def display(item: Renderable) -> None:
print(item.render())
# HtmlWidget satisfies Renderable without inheriting from it
display(HtmlWidget())
TypedDict for structured dicts
from typing import TypedDict, NotRequired
class UserDict(TypedDict):
name: str
email: str
age: NotRequired[int]
def create_user(data: UserDict) -> None:
print(data["name"]) # type-safe key access
create_user({"name": "Alice", "email": "alice@example.com"})
ParamSpec for decorator typing
from typing import TypeVar, Callable, ParamSpec
import functools
P = ParamSpec("P")
R = TypeVar("R")
def log_calls(func: Callable[P, R]) -> Callable[P, R]:
@functools.wraps(func)
def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
print(f"Calling {func.__name__}")
return func(*args, **kwargs)
return wrapper
@log_calls
def add(a: int, b: int) -> int:
return a + b
# Type checker knows add(1, 2) -> int
TypeGuard for type narrowing
from typing import TypeGuard
def is_string_list(val: list[object]) -> TypeGuard[list[str]]:
return all(isinstance(item, str) for item in val)
def process(items: list[object]) -> None:
if is_string_list(items):
# Type checker knows items is list[str] here
print(", ".join(items))
Overloaded function signatures
from typing import overload
@overload
def parse(raw: str) -> dict[str, str]: ...
@overload
def parse(raw: bytes) -> dict[str, bytes]: ...
def parse(raw: str | bytes) -> dict[str, str] | dict[str, bytes]:
if isinstance(raw, str):
return {"data": raw}
return {b"data": raw}
Type aliases
from typing import TypeAlias
JSON: TypeAlias = dict[str, "JSON"] | list["JSON"] | str | int | float | bool | None
Callback: TypeAlias = Callable[[int, str], None]
Matrix: TypeAlias = list[list[float]]
Best Practices
- Use modern syntax (
X | Yinstead ofUnion[X, Y],list[int]instead ofList[int]) when targeting Python 3.10+. - Use
from __future__ import annotationson Python 3.9 to enable modern syntax via PEP 563. - Run mypy in CI with
--strictor at least--warn-return-any --disallow-untyped-defs. - Use
Protocolover ABC when you want structural ("duck") typing without forcing inheritance. - Annotate public APIs fully; internal helper functions benefit too but are lower priority.
- Use
Finalfor constants andClassVarfor class-level attributes to distinguish from instance attributes.
Common Pitfalls
- Using
Anyas an escape hatch silences the type checker entirely for that value — useobjectwhen you mean "any type but still type-safe." - Mutable default in annotations —
def f(x: list[int] = [])is a runtime bug unrelated to types; type checkers do not catch it. - Confusing
type[X]withX—type[User]means the class itself,Usermeans an instance. - Forgetting variance — a
list[Dog]is not alist[Animal]because lists are mutable (invariant); useSequence[Animal]for covariant read-only access. - Circular imports for type hints — use
from __future__ import annotationsorTYPE_CHECKINGguard to break cycles.
Anti-Patterns
-
Any as the default — using
Anywhenever the type is not immediately obvious instead of taking the time to determine the correct type.Anysilences the type checker entirely for that value, defeating the purpose of type annotations. Useobjectwhen you mean "any type but still type-safe." -
Type hints without a type checker — adding annotations to your codebase but never running mypy, pyright, or an equivalent tool in CI. Unenforced annotations rot quickly as the code evolves, becoming misleading documentation that claims one type while the runtime uses another.
-
Overly complex generic signatures — writing function signatures like
Callable[[Mapping[str, Sequence[tuple[int, ...]]], ...], Awaitable[list[dict[str, Any]]]]that are harder to read than the code itself. If a type annotation needs a paragraph to understand, simplify the API or use a TypeAlias to break it into named components. -
Ignoring variance in container types — accepting
list[Animal]when you only read from the collection, forcing callers to widen their specificlist[Dog]. UseSequence[Animal](covariant, read-only) for parameters that only iterate or index, and reservelistfor when you genuinely need to append or modify. -
Lying annotations — annotating a function as
-> strwhen it can also returnNonein certain branches, or declaring a parameter asintwhen the function also accepts strings. Incorrect annotations are worse than no annotations because they build false confidence and cause type-checker-verified code to crash at runtime.
Install this skill directly: skilldb add python-patterns-skills
Related Skills
Async Patterns
Asyncio patterns for concurrent I/O-bound programming in Python
Context Managers
Context manager patterns using with statements for reliable resource management in Python
Dataclasses
Dataclass and Pydantic model patterns for structured data in Python
Decorators
Decorator patterns for wrapping, extending, and composing Python functions and classes
Dependency Injection
Dependency injection patterns for loosely coupled, testable Python applications
Generators
Generator and itertools patterns for memory-efficient data processing in Python