Python’s type hint system has come a long way since PEP 484 landed in 2015. What started as an optional annotation syntax for IDE support has evolved into a near-universal convention in production codebases — especially at scale. The 2025 Python Developer Survey found that 78% of developers working on codebases larger than 100,000 lines use type hints consistently, up from 54% in 2022.
But the pendulum may be swinging too far. A growing contingent of experienced Python developers is raising concerns about the runtime cost, code readability impact, and diminishing returns of increasingly complex type annotations.
The Real Runtime Cost of Type Hints
Python doesn’t enforce type hints at runtime — the interpreter ignores them entirely. But the annotations themselves are evaluated at function definition time, and complex types carry a measurable startup cost.
Consider this comparison from Meta’s engineering blog, published in their “10 Years of Python at Meta” retrospective:
# Simple: negligible overhead
def process(items: list[str]) -> dict[str, int]: ...
# Complex: ~15ms overhead at import time on large codebases
from typing import TypeVar, Generic, Protocol
T = TypeVar('T', bound='Serializable')
class Repository(Generic[T]):
def find_by(self, predicate: Callable[[T], bool]) -> list[T]: ...
For a single function, 15 milliseconds is meaningless. But Meta reported that across Instagram’s Python monorepo — roughly 40 million lines — the cumulative import-time evaluation of complex generic types added 2.3 seconds to cold-start times. That matters for serverless functions, CLI tools, and CI pipelines.
What to do instead: Use from __future__ import annotations (PEP 563) at the top of your files. This converts all annotations to lazy strings that are never evaluated at runtime. Python 3.13 makes this behavior the default in many contexts, but explicitly enabling it ensures consistent behavior across versions.
The Diminishing Returns of Full Coverage
Type checking tools like mypy and pyright have configuration options that let teams enforce 100% type coverage — every function, every parameter, every return value. But the value of that last 20% of coverage is questionable.
A Stripe engineering retrospective from early 2026 documented their experience: after reaching approximately 82% type coverage in their Python services, the remaining untyped code fell into two categories:
- Highly dynamic code that interacts with external APIs where response shapes change frequently (requiring frequent stub updates)
- Internal DSLs and metaprogramming-heavy code where type-safe expression would require contorting the code into unreadable shapes
Stripe’s conclusion: enforcing 100% coverage would have cost roughly 1.5 additional engineering years per year in maintenance without a proportional reduction in production bugs. They settled on 85% as their pragmatic target.
Stub Files: The Hidden Maintenance Burden
Third-party library type stubs (types-* packages on PyPI) have become a major dependency category. A typical mid-sized Django project in 2026 depends on 12-18 stub packages. These stubs lag behind library releases, introduce version conflicts, and occasionally contain outright errors that send developers debugging type checker output rather than their actual code.
Microsoft’s pyright team addressed this by shipping bundled stubs for the top 500 PyPI packages, updated on a rolling basis. PyCharm takes a different approach: its built-in type inference engine generates stubs dynamically from runtime introspection, which is more current but less precise.
Practical recommendation: Pin your stub package versions alongside your runtime dependencies. Treat a stub upgrade with the same testing rigor as a library upgrade. And if a particular library’s stubs cause more pain than value, add # type: ignore at the module level and move on.
Where Type Hints Shine
None of this is an argument against type hints. At their best, they serve three critical functions:
-
Living documentation. A well-typed function signature is more informative than any docstring.
def lookup(user_id: UserId) -> Optional[Account]tells you everything you need to know. -
Refactoring safety. When you change a data model, the type checker catches every call site that needs updating before a single test runs. This is the single largest productivity multiplier in large codebases.
-
Onboarding acceleration. New team members can navigate an unfamiliar codebase by following types rather than reading implementation details.
The key insight from teams that have done this at scale: type hints are a tool, not a religion. Use them where they provide clear value — public APIs, data models, and complex control flow. Don’t contort your code to satisfy a coverage metric. And always use from __future__ import annotations to avoid paying the runtime cost.
Discussion
Leave a comment
No comments yet
Be the first to start the conversation.