Type hints do nothing on their own โ Python ignores them at runtime. Their value comes from a checker like mypy reading them. Here are six habits that make that check actually catch bugs.
1. Annotate Function Boundaries First
You do not need to type every local variable. You do need to type every function signature. Boundaries are where wrong assumptions cross.
def total(prices: list[float], tax: float) -> float: return sum(prices) * (1 + tax)
2. Use Optional Honestly
If a value can be None, say so. Then the checker forces you to handle it.
def find_user(uid: int) -> User | None: ... user = find_user(1) print(user.name) # mypy error: user may be None
That error is a real crash you avoided.
3. Prefer Precise Types Over Any
Any disables checking. Reach for Sequence, Mapping, or a TypedDict instead.
from typing import TypedDict class Config(TypedDict): host: str port: int
4. Type Your Collections' Contents
list alone tells the checker nothing about elements. list[str] catches a stray int.
5. Use Literal for Fixed Choices
from typing import Literal def set_mode(mode: Literal["r", "w", "a"]) -> None: ... set_mode("x") # mypy error before you ever run it
6. Run mypy in CI, Not Just Locally
Hints you never check will rot. A CI step (mypy --strict src/) keeps them honest and blocks regressions in review.
Summary
Type hints are a contract only if something enforces the contract. Annotate boundaries, be honest about None, avoid Any, and let mypy run automatically. Do that and a whole class of AttributeError and TypeError bugs disappears before the code ever runs.