Type hints are everywhere in Python now. A 2025 survey of 1,241 developers found 86% use them "always" or "often"; a study of 1,000 popular libraries found 91% use them at least once, though in half of those only about 14% of the code actually carries a type.
I think adding them was one of the worst design decisions in Python. Here's what's wrong with them, and what I do instead.
The Zen of Python disagrees
Python's whole personality is in import this, the Zen of Python. Readability counts. Simple is better than complex. Sparse is better than dense. The language was always two things at once: easy to read and easy to write.
Type hints are neither. users: list[User] is more to type than users, and more to read.
Someone always quotes Explicit is better than implicit to defend them. Sure, but that's one line, and the very next one is Readability counts, with Sparse is better than dense just above it. Being explicit about a type you never enforce is just decoration.
If I wanted a type system, I'd reach for one of the many languages built around one: Rust, Go, whatever.
Python's niche was being the easy one: no clutter, readable, writable, great for beginners and advanced programmers alike.
Why people use type hints
Type hints don't make code prettier, but people use them to:
- Document the data types, especially in a team.
- Catch bugs from passing the wrong type.
- Get autocomplete in their editor.
The first two, type hints don't actually solve. The third is their best case, but smaller than it looks.
For this first problem, here's a real signature from my own code:
def get_or_compute(self, key: str,
compute_func: Callable[[], Any],
ttl_seconds: int = 120) -> Any:
...
Callable[[], Any] and -> Any tell you nothing. The plain name ttl_seconds carries more real information than the whole annotation set around it.
For the second issue, the hints aren't checked at runtime. def f(x: int) will happily take a string and never say a word.
The only thing enforcing them is an external linter like mypy: if you run it, if it's wired into CI, if nobody silenced the error with an Any. You get the look of a strict interface without the guarantee.
The third is the most important reason. Type hints help with editor autocomplete. But a modern editor (PyCharm, Pylance) already infers in many cases: write user = get_user(id) and it knows user is a User from the return value, no annotation needed.
The price for that is high in readability and writability. Autocomplete on a few parameters isn't worth reshaping the whole language for. There are lighter fixes: smarter editor inference, naming conventions, or a docstring.
Measuring the cost
There's a metric for this: Halstead volume. Count the operators and operands in a piece of code (the tokens), weight them by how many distinct ones there are, and you get a number for how dense the code is.
It's a proxy for one real thing: how much your eye has to take in to read it.
Take a plain function:
def total_spent(orders, since=None):
total = 0
for order in orders:
if since and order.date < since:
continue
total += order.amount
return total
Now add a normal set of hints:
def total_spent(orders: list[Order], since: date | None = None) -> float:
total = 0
for order in orders:
if since and order.date < since:
continue
total += order.amount
return total
Count the tokens in each (I used Python's tokenize):
- the signature alone: 10 to 21 tokens, doubled for the same parameters.
- the whole function: 35 to 46 tokens (+31%), Halstead volume 160 to 228.
Same logic, no new behavior. The signature doubled and the function grew by a third. Sparse is better than dense. Your eye pays it on every read.
What to do instead
Choose good variable names. The name carries the type to every place the variable is used, not just the declaration, and it costs no extra syntax.
Start with singular and plural: user is a User; users is a list[User] (or a set, or any iterable). The plural already tells you there are several, which is the part that matters; the exact container usually doesn't. That covers most cases, for free.
For the rest, a few conventions straight out of my own code:
dossier_id,user_id: an int, a key. Drop the suffix anddossieris the object.model_name,plugin_name,filename: strings.duration_seconds,ttl_seconds: the unit, whichintwill never give you.pdf_data: bytes.dossiers_by_name: a dict, keyed by name.user_metrics_list: a list. The_listis there becauseuser_metricsalready reads as plural, so a plain plural wouldn't disambiguate.has_session_id: a bool.
The important part is when you need it. Don't suffix everything with its type. get_dossier(dossier_id) earns the _id because there's also a dossier object to confuse it with. notify(user) needs nothing; user is already a User. You spend the extra characters only where there's a real ambiguity to kill: id versus object, a unit, a string that could be mistaken for the thing it points at.
The function name does the same for the return value:
get_userreturns aUser.get_user_countreturns an int.has_permissionsreturns a bool.
Conclusion
Type hints are a little useful for editor integration, but they cost a lot in readability and writability.
Decide it once, for the whole project. Leave it to each developer, and the day a fan joins the team, type hints creep in file by file until the codebase is half-typed.
Whatever you decide, don't let people call it the cleaner, better, or correct way. It's a choice, but I think Python's success comes from its simplicity and readability.
I choose the side Python was built for.
References
- PEP 484 - Type Hints. Introduced type hints in Python 3.5 (2015).
- Python Typing Survey 2025. JetBrains, Meta, and the Python typing community; 1,241 respondents, 86% use type hints "always" or "often".
- Where to Start: Studying Type Annotation Practices in Python. 2021; 91% of 1,000 popular libraries use type hints at least once, median coverage 13.6%.
- Why I stay away from Python type annotations and r/Python: why type hinting sucks. Related takes.