
Python Typing in Production: Beyond mypy Hello-World
TypedDict, Protocol, Literal, generics, and how to actually enforce types on a large Python codebase without slowing your team down.
Type hints in Python are worth the effort — but only with a strict configuration you actually enforce in CI. TypedDict, Protocol, and Literal cover 90% of real cases; generics cover the rest. Pyright is the checker to use in 2026.
- Untyped Python at scale becomes a codebase you can't refactor. Types are the escape hatch.
- Pyright is faster and stricter than mypy; run it in CI, not just in editors.
- TypedDict maps JSON-shaped data to types without dataclass overhead.
- Protocol is duck typing you can typecheck — the right tool for injected dependencies.
- Literal + Union gives you exhaustive checks for enum-like values without an Enum class.
Why bother
Type hints do three things that matter more the bigger your codebase gets:
- They document the intended shape of every function without a doc block.
- They let a checker prove that a refactor didn't break anything, in milliseconds.
- They give editors real autocomplete instead of guessing.
On a 50k-line codebase I inherited untyped, retrofitting types took two engineers two months and paid back within the same quarter in caught refactor bugs. If you have >10k lines of Python and no plan for types, you have a growing tax you don't yet see.
Pyright, not mypy, in CI
mypy is the historical default and it's fine. Pyright (from Microsoft, also powers Pylance) is faster, stricter by default, and evolves faster with new PEPs. In 2026 pyright is the pragmatic pick.
# pyproject.toml
[tool.pyright]
typeCheckingMode = "strict"
reportMissingTypeStubs = false # third-party packages
reportUnknownMemberType = false # too noisy on untyped libs
pythonVersion = "3.12"
venvPath = "."
venv = ".venv"Run pyright in CI. Fail the build on error. Do not accept warnings-only mode after the first month — it decays.
TypedDict for JSON-shaped data
You receive JSON from an API or database. Don't parse it into a dataclass just for typing — TypedDict gives you type safety with zero runtime cost:
from typing import TypedDict, NotRequired
class OrderRow(TypedDict):
id: str
customer_id: str
total_cents: int
currency: Literal['USD', 'EUR', 'GBP']
metadata: NotRequired[dict[str, str]]
def get_order(row: OrderRow) -> int:
return row['total_cents'] # checker knows this is intProtocol for structural typing
Duck typing is Python's soul, but classic duck typing can't be typechecked. Protocol brings it back with static verification:
from typing import Protocol
class UserRepo(Protocol):
def get(self, user_id: str) -> User | None: ...
def save(self, user: User) -> None: ...
def register(repo: UserRepo, email: str) -> User:
...Any class with matching methods satisfies UserRepo — no inheritance needed. This is the right way to type injected dependencies and makes tests trivial to fake.
Literal + Union for enum-like fields
Instead of an Enum class for a simple discriminator, use Literal:
Status = Literal['pending', 'active', 'closed']
def transition(current: Status, event: str) -> Status:
match (current, event):
case ('pending', 'activate'): return 'active'
case ('active', 'close'): return 'closed'
case _: raise ValueErrorPyright will error if you pass a string that isn't one of those three, and will exhaustiveness-check match if you annotate the return with the same Literal union. Enum works too, but Literal is lighter and serializes to JSON as-is.
Generics that pay off
Most day-to-day generics come from list[T] and dict[K, V]. Write your own generic only when you have a container or wrapper that operates over many types:
from typing import TypeVar, Generic
T = TypeVar('T')
class Paginated(Generic[T]):
def __init__(self, items: list[T], next_cursor: str | None):
self.items = items
self.next_cursor = next_cursor
def list_orders() -> Paginated[Order]: ...Rolling it out on an existing codebase
- Turn on pyright in warn-only mode. Capture the current error count.
- Fix the lowest-hanging fruit (missing return types on new code, obvious
Anyescapes). - Enable strict mode module-by-module using
[[tool.pyright.strict]]patterns. - Add pyright to CI as a warning, then a failure, on a fixed date.
- Add a lint rule that forbids new
Anyoutside a small allowlist.
This roadmap has moved multiple large codebases from 'no types' to 'strict everywhere' inside a quarter without a rewrite.
Frequently asked
Types slow me down. Are they worth it?
In prototypes, skip them. In anything that outlives a quarter and has more than one contributor, they save more time than they cost within weeks.
Do I need Pydantic if I have types?
Different jobs. Types describe intent to the checker. Pydantic validates runtime data. Use both: types everywhere, Pydantic at trust boundaries (API inputs, config loading).
What about runtime type checking?
Overkill for most code. Use Pydantic at the edges and trust types internally. beartype is useful in libraries.
Building something similar?
I help teams ship production-grade AI agents, n8n workflows, and data platforms. Let's talk about what you're building.
Work with me
Islam Gamal
AI, Data & Automation Engineer. I design and ship production AI agents, n8n workflows, and cloud data platforms — with a focus on reliability, cost, and measurable business impact. Founder of Tashghil and Tek bil Arabi.
More from Islam Gamal
Async Python for AI Pipelines: Concurrency, Backpressure, and Not Melting Your API Bill
A pragmatic 30-minute deep-dive into asyncio for AI workloads — semaphores, timeouts, retries with jitter, streaming responses, connection pooling, and the patterns that let one Python process fan out to thousands of concurrent LLM calls without exploding.
Postgres for Product Engineers: Indexing, JSONB, RLS, and the 20% That Ships Millions of Users
The Postgres playbook I've used across product teams — index type per workload, JSONB without regret, RLS that doesn't leak or crawl, connection pooling that survives spikes, and the observability that turns guessing into ground truth.
Designing REST APIs LLMs Can Actually Use: A Field Guide for Tool-Using Agents
The API design choices that make or break tool-using LLM agents — naming, error shapes, idempotency, discoverability, JSON schema hygiene, pagination, and the eight anti-patterns that confuse every model.
Browse every article by Islam Gamal on the author page.