Type Hints Best Practices
Types should reduce incidents without drowning the team in ceremony. These rules focus effort where static analysis prevents real production bugs.
How to Use This List
- Apply to all new modules and touched code in review.
- Pair with mypy/pyright CI on typed packages.
- Revisit when
Anycount grows or strict overrides multiply. - Onboard juniors with Type Hints Basics then this checklist.
A - Where to Type
- Type every public function signature in application code. Private helpers when non-obvious.
- Type HTTP/CLI/config boundaries first. Pydantic 2 models for FastAPI/Django REST payloads.
- Use Protocols for injected dependencies (storage, clock, notifier). Test doubles without inheritance.
- Annotate container contents:
list[str], not barelist. Bare generics defeat checking. - Stop at hot inner loops unless bugs proved. Profile before typing numpy kernels exhaustively.
B - Syntax and Style
- Use
X | Yunion syntax on Python 3.10+. Consistent with 3.14 codebase. - Use
collections.abctypes in parameters (Sequence,Mapping). Accept wider read-only inputs. - Prefer
TypedDict/NamedTuplefor JSON rows; dataclass for domain objects. Match runtime shape. - Use
LiteralandStrEnumfor fixed string sets. Prevent typos at check time. - Avoid
Any- useobjectwhen truly unknown then narrow. Track Any with lint rule where possible.
C - Tooling and CI
- Run mypy or pyright in CI on typed packages. Block regressions on merged modules.
- Install
types-*stubs for untyped dependencies. requests, pyyaml, etc. - Enable
warn_unused_ignoresandwarn_return_any. Clean stale ignores promptly. - Use per-package mypy overrides, not global
ignore_errors. Quarantine with sunset dates. - Keep ruff 0.9+ separate from type checking. Formatter/linter does not replace mypy.
D - Runtime vs Static
- Remember hints are not runtime validation. Use Pydantic/isinstance at trust boundaries.
- Do not rely on
typing.castto fix logic. cast silences checker only. - Use
isinstance/matchnarrowing after optional checks.if x is not Nonepattern. - Validate external JSON before treating as TypedDict shape. Schema or Pydantic first.
- Document when
type: ignoreunavoidable with error code and ticket link.
E - Gradual Typing Hygiene
- Type new code 100%; legacy follows gradual strategy. See gradual typing article.
- Shrink quarantined legacy modules over time.
ignore_errorsmust have owner and date. - Prefer generics (
TypeVar) when utility preserves input types. NotAnyin/out. - Use
@overloadsparingly for real signature dependence. Not for cosmetic hints. - Review Protocol/ABC choice in design docs. Structural vs nominal consistency.
FAQs
type everything?
Public APIs yes; trivial locals optional unless mypy needs help inferring.
Pydantic replaces hints?
Pydantic adds runtime validation; keep hints on pure functions for mypy coverage.
tests typed?
Lower priority than src; type tests when they clarify fixtures or prevent copy-paste errors.
numpy/pandas typing?
Use library stubs and precise dtypes where ROI clear; do not block ship on perfect ndarray generics.
SQLAlchemy typing?
2.0 style mapped types improving - follow sqlalchemy stubs/plugins in mypy config.
too strict too fast?
Rollback override to previous level; fix forward in weekly slices not heroics.
document typing policy?
Short ADR: checker choice, strict timeline, Pydantic boundaries, Protocol for ports.
Any budget?
Some teams cap Any count via script in CI - optional advanced governance.
pyright in IDE only?
Fine for dev; still need CI gate so teammates without Pylance get same safety.
single biggest win?
Typed HTTP models + service function signatures - catches most production type bugs early.
Related
- Type Hints Basics - syntax tour
- Gradual Typing Strategy - brownfield rollout
- mypy & pyright Setup - configuration
- Python Fundamentals Best Practices - general habits
Stack versions: This page was written for Python 3.14.0 (stable 3.14, maintenance 3.13), FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.