Skip to content

Python Typing

Type hints make Python code self-documenting and catch whole classes of bugs before they run. This sheet covers annotations, generics, static checking with mypy, and typing patterns that work well in data science code.

Table of Contents

Installation / Setup

Type hints work in any Python 3.x with zero setup. Add a checker for real value:

pip install mypy          # or: uv add --dev mypy
mypy src/                 # check a directory
mypy model.py --strict    # strict mode: no untyped defs

Alternatives: pyright (fast, VS Code/Pylance use it), pytype (Google).

pip install pyright && pyright .

Core Syntax

Basic annotations

def accuracy(y_true: list[int], y_pred: list[int]) -> float:
    return sum(a == b for a, b in zip(y_true, y_pred)) / len(y_true)

def load(path: str, limit: int | None = None) -> list[dict[str, float]]:
    ...
Annotation Meaning
int, str, float, bool, bytes Built-ins (lowercase!)
list[int], dict[str, int], set[str], tuple[int, ...] Parameterized built-ins (3.9+)
int \| None Union shorthand (3.10+); pre-3.10: Optional[int]
Any Anything goes — use sparingly
Literal["train", "eval"] Exact allowed values
Callable[[int, int], int] Function signature
Iterable[int] / Sequence[int] / Mapping[str, int] Abstract containers
TypeVar("T") Generic placeholder
TypedDict Dict with fixed keys and types
Protocol Structural interface (duck typing)
NewType("UserId", int) Distinct type at check time
Final, ClassVar Constants, class-level vars
Annotated[int, ...] Extra metadata (e.g., pydantic constraints)
-> None Function returns nothing

Variables and classes

from dataclasses import dataclass

@dataclass
class Config:
    lr: float = 0.01
    epochs: int = 10
    tags: list[str] = field(default_factory=list)

x: int = 5
y: list[float] = []

Generics

from typing import TypeVar

T = TypeVar("T")

def first(xs: list[T]) -> T:      # works for list[int], list[str], ...
    return xs[0]

from typing import Generic

class Stack(Generic[T]):
    def __init__(self) -> None:
        self._items: list[T] = []
    def push(self, item: T) -> None: ...
    def pop(self) -> T: ...

Protocols (structural typing)

from typing import Protocol

class HasMean(Protocol):
    def mean(self) -> float: ...

def summarize(x: HasMean) -> float:   # any object with .mean() passes
    return x.mean()

Common Operations

Check types

mypy src/ --strict                # full strictness
mypy src/ --ignore-missing-imports  # untyped third-party libs
# Runtime introspection
from typing import get_type_hints
get_type_hints(accuracy)          # {'y_true': list[int], ...}

TYPE_CHECKING for circular imports

from typing import TYPE_CHECKING
if TYPE_CHECKING:
    from data_loader import Loader   # never imported at runtime

Practical Examples

1. Typed data pipeline

from typing import TypedDict

class Row(TypedDict):
    user_id: int
    score: float
    tags: list[str]

def load_rows(path: str) -> list[Row]:
    import json
    return json.loads(Path(path).read_text(encoding="utf-8"))

rows: list[Row] = load_rows("data.json")
rows[0]["score"]          # float — checker knows
rows[0]["missing"]        # mypy error: key not in Row

2. Typed NumPy/pandas interop

import numpy as np
import pandas as pd

def normalize(x: np.ndarray) -> np.ndarray:
    return (x - x.mean()) / x.std()

def top_features(df: pd.DataFrame, k: int) -> pd.DataFrame:
    return df.abs().sum().sort_values(ascending=False).head(k).to_frame()

Add stubs to make the checker happy: pip install pandas-stubs (NumPy ships its own types).

3. Generic training function

from typing import TypeVar, Protocol

T = TypeVar("T")

class Trainer(Protocol):
    def fit(self, X: np.ndarray, y: np.ndarray) -> None: ...
    def predict(self, X: np.ndarray) -> np.ndarray: ...

def run(model: Trainer, X: np.ndarray, y: np.ndarray) -> np.ndarray:
    model.fit(X, y)
    return model.predict(X)

Common Patterns

# Optional input + explicit None check
def load(path: str | None = None) -> str:
    path = path or "default.json"
    ...

# NewType for unit-like values
UserId = NewType("UserId", int)
def fetch(uid: UserId) -> ...     # UserId(5), not plain 5

# Annotated metadata for pydantic
from typing import Annotated
from pydantic import Field
Age = Annotated[int, Field(ge=0, le=130)]

# Literal for enums-like choices
Split = Literal["train", "val", "test"]
def get_split(name: Split) -> pd.DataFrame: ...

Best Practices

  • Annotate public function signatures — parameters and return types.
  • Prefer list[int] / dict[str, int] over List[int] / Dict (modern style).
  • Use Any deliberately, never as a default habit — it disables checking.
  • Use Sequence/Iterable/Mapping for parameters, concrete types for returns.
  • Add pandas-stubs and prefer np.ndarray annotations over plain object.
  • Run mypy in CI — a mypy.ini/pyproject.toml config makes checks consistent.
  • Type dataclasses fully — they're the backbone of typed configs.
  • Gradually annotate existing code; --strict on new modules is fine.

Common Mistakes

Mistake Consequence Correct approach
List[int]/Dict from typing Deprecated-style noise (works, but legacy) Built-ins: list[int], dict[str, int]
Capitalized Int, Str Runtime errors (Int is a class, not a type) Lowercase built-ins
Optional[int] where int \| None fits Old syntax (fine pre-3.10) Use \| on 3.10+
No stubs for pandas mypy errors on every DataFrame pip install pandas-stubs
Over-annotating internals Noisy, hard to maintain Annotate APIs, not every local
NewType at runtime only No runtime validation Pair with pydantic for real validation

Troubleshooting

Symptom Cause Fix
Missing type parameters for generic type "list" Used list without [...] list[int]
Name "list" is not defined Python < 3.9 Use typing.List or upgrade
Cannot find implementation or library stub Third-party package untyped --ignore-missing-imports or install stubs
error: Returning Any from function declared to return "int" Value from untyped lib Annotate the library boundary explicitly
Attribute "mean" is unknown for "object" Annotated as object Annotate as np.ndarray or define a Protocol

Official Documentation

Last Updated

2026-09-06 — Initial version.