Warming up the neural circuits...
By the end of this chapter you will:
Type hints are design documentation that your editor can execute continuously. They catch mismatches before runtime, reduce refactor fear, and make large codebases easier to navigate.
You are not trying to make Python "Java-like." You are trying to make contracts explicit.
def parse_amount(text: str) -> float:
value = float(text)
if value <= 0:
raise ValueError("amount must be positive")
return value
def maybe_name(raw: str | None) -> str:
return raw or "anonymous"Prefer X | Y unions and explicit None handling.
from typing import Protocol, TypedDict
class UserPayload(TypedDict):
id: int
email: str
class Sender(Protocol):
def send(self, msg: str) -> None:
TypedDict describes dict structure. Protocol describes required behavior.
from typing import TypeVar
T = TypeVar("T")
def first_or_default(items: list[T], default: T) -> T:
return items[0] if items else defaultGenerics preserve type information end-to-end.
# pyproject.toml
[tool.mypy]
python_version = "3.12"
strict = false
warn_unused_ignores = true
no_implicit_optional = trueStart pragmatic, then increase strictness by module.
Frequent patterns:
Item "None" has no attribute ... -> add guard or assertIncompatible return value type -> align function contract and implementationArgument has incompatible type -> convert or narrow before callAny leakage from third-party code -> add stubs or local ProtocolTreat typing like test coverage: start where bugs hurt most, then widen gradually. Do not block delivery by trying to type the entire repo in one sprint.
| Need | Use |
|---|---|
| Function contract clarity | Basic annotations |
| Dict payload schema | TypedDict |
| Behavior-oriented interface | Protocol |
| Reusable typed utility | TypeVar generics |
| Runtime | Pydantic or explicit checks (separate from static typing) |
| Mistake | Symptom | Fix |
|---|---|---|
Using Any everywhere | Type checker gives little value | Type boundaries strictly, especially I/O adapters |
| Mixing runtime validation with static typing expectations | False sense of safety | Keep static and runtime guarantees explicit |
Blanket # type: ignore | Real issues hidden | Scope ignores narrowly with reason |
| Enabling strict mode globally on day one | Team friction and blocked merges | Adopt per-module strictness ramp |
Forgetting None handling | Frequent optional-related errors | Guard, default, or assert before access |
parse_amount accepts text inputTyped function returning floatDictionary-based API payloadTypedDict schema definitionsend_welcome(sender, email)Works with any object implementing send(msg)list[T], default TReturn type preserved as TModules: api, services, legacyStrictness increases where safety matters most firstBeginner:
"What is the difference between TypedDict and dataclass?"
TypedDicttypes dictionary shapes for static checking, whiledataclasscreates runtime classes with methods and structured instances.
"Why use Protocol?"
Protocol supports structural typing so any object with required methods is accepted, even without explicit inheritance.
Senior:
"How would you introduce mypy into a legacy codebase?"
Start at boundaries and critical modules, avoid all-or-nothing strict mode, and enforce incremental CI checks.
"When is Any acceptable?"
At unavoidable dynamic boundaries (untyped libraries, plugin interfaces) with containment and explicit adapters.
TypedDict, Protocol, and generics cover most real-world typing needs.Need shape typing? -> TypedDict
Need behavior typing? -> Protocol
Need reusable typed helper? -> TypeVar
Need enforcement? -> mypy in CIWhat is TypedDict primarily used for?