Python Type Hinting: Best Practices for Large Codebases
Python's dynamic typing is fantastic for prototyping, but in a 100,000-line enterprise monorepo,
AttributeError: 'NoneType' object has no attribute 'get' is a daily nightmare. By enforcing
strict type hinting with tools like MyPy, you shift these runtime errors to compile-time (or CI-time) errors.
1. Embrace Optional and Union Types
One of the most common bugs in Python is assuming a function returns an object when it can actually return
None. In modern Python (3.10+), use the | operator to explicitly define this.
# BAD: MyPy cannot warn you if you don't check for None
def get_user(user_id: int):
if user_id < 0: return None
return User(id=user_id)
# GOOD: MyPy forces the caller to check `if user is not None:`
def get_user(user_id: int) -> User | None:
if user_id < 0: return None
return User(id=user_id)
2. Use Protocols for Structural Subtyping (Duck Typing)
Python developers love "duck typing" (if it walks like a duck, it is a duck). But how do you type-hint a duck
without forcing inheritance? Use typing.Protocol.
from typing import Protocol
class EmailSender(Protocol):
def send(self, to: str, body: str) -> bool:
...
# This class doesn't inherit from EmailSender, but it matches the structure!
class SendGridClient:
def send(self, to: str, body: str) -> bool:
# Implementation...
return True
# The type checker is happy!
def notify_user(email: str, sender: EmailSender):
sender.send(email, "Welcome!")
client = SendGridClient()
notify_user("test@example.com", client)
3. TypedDict for Legacy JSON/Dict Passing
In older codebases, functions often pass around generic dictionaries. Refactoring them entirely to Pydantic
models or dataclasses might be too risky. You can use TypedDict to enforce the structure of
dictionaries without changing runtime behavior.
from typing import TypedDict
class MoviePayload(TypedDict):
title: str
year: int
director: str | None
# MyPy will error if you try to pass {"title": "Inception"} because year is missing.
def process_movie(payload: MoviePayload):
print(f"Processing {payload['title']} ({payload['year']})")
4. Strict MyPy Configuration
Type hints are useless if your CI pipeline doesn't enforce them. In your pyproject.toml or
mypy.ini, enable strict mode to prevent developers from sneaking untyped code into the
repository.
[tool.mypy]
strict = true
disallow_untyped_defs = true
warn_return_any = true
no_implicit_optional = true
Conclusion
Adding type hints to Python requires a slight paradigm shift. However, the benefits are undeniable: incredible IDE autocompletion, vastly reduced unit test requirements (the type checker proves the types), and fearless refactoring. Type hinting is no longer optional for professional Python development.