Python is still dynamically typed — type hints annotate, not enforce
Type hints do not change how the interpreter runs your code. They are documentation that static analysers, linters, and editors can verify before runtime. This article explains what actually happens under the hood.
- How __annotations__ works at runtime
- mypy and pyright compared
- Generics and TypeVar
- Protocol vs ABC
- Type narrowing patterns
Abstract: Python in 2026 is still the same dynamically typed language it has always been. What changed is not the interpreter, but the ecosystem around it — type checkers, linters, and editors that reason about your code before it ever runs.
Programming becomes much more enjoyable once you stop memorizing syntax and start understanding the machinery beneath it.
There is a common misconception that Python has suddenly become a “statically typed language.” It hasn’t. Understanding why is one of those small conceptual shifts that suddenly makes the whole language feel more coherent.
1. Python Is Still Dynamically Typed
Let’s begin with a simple function.
def area(width, height):
return width * height
Nothing unusual here. Now suppose someone calls it like this:
area("10", 5)
Many beginners expect Python to complain. Instead it happily produces:
1010101010
because multiplying a string by an integer is perfectly legal Python. The interpreter did exactly what you asked. It did not understand what you meant.
The risk: Python delays many checks until runtime. This is one of its greatest strengths — and one of its greatest dangers.
2. Type Hints Change Nothing…
Now let’s add type hints.
def area(width: float, height: float) -> float:
return width * height
What changed? Almost nothing. Python executes this function exactly as before. You can still write
area("10", 5)
and Python will still produce 1010101010. The interpreter simply ignores the annotations. So why
do they exist?
3. …Because They Are Not For Python
This is the conceptual leap. Type hints are not primarily written for the interpreter. They are written for developers and tools. Imagine the software development process as three different actors.
Source Code
│
┌─────────┴─────────┐
│ │
Type Checker Python Interpreter
(Pyright, mypy) (CPython)
│ │
Finds mistakes Executes code
before running
The interpreter only cares about executing instructions. The type checker reasons about your program before it ever runs. Those are completely different jobs.
4. Linting vs Type Checking
These two terms are often confused, but they solve different problems.
Linting
A linter looks for suspicious code and style issues.
print(total)
x = 42
If total was never defined, the linter immediately complains. Likewise, an unused
import math is usually reported as unnecessary. Modern Python projects often use Ruff, which
performs linting extremely quickly. A linter asks questions like:
- Did you forget something?
- Is this variable unused?
- Is this code unnecessarily complicated?
- Does this follow the project’s coding conventions?
Type Checking
A type checker asks an entirely different question.
def add(a: int, b: int) -> int:
return a + b
add(1, "2")
Python will only discover the problem when that line executes. A type checker discovers it immediately:
Expected int
Received str
This feels almost like compiling a language such as Rust or Go. Except — Python itself remains completely dynamic.
5. Better Tooling Through Better Information
Imagine reading this function.
def search(users):
...
What is users? A list? A dictionary? A generator? Nobody knows. Now compare:
def search(users: list[str]) -> list[str]:
...
Without opening the implementation, your editor already understands that every element is a string and the return value is another list of strings. Suddenly autocomplete becomes dramatically smarter. Navigation becomes easier. Refactoring becomes safer.
6. Collections
Modern Python prefers precise descriptions. Instead of list[str], you’ll often see:
from collections.abc import Sequence
def search(users: Sequence[str]) -> Sequence[str]:
...
Why? Because the function only reads. It doesn’t care whether the caller passes a list, a tuple, or another sequence implementation. This is an example of programming against capabilities instead of concrete implementations.
7. Optional Values
Suppose a user might not exist. Instead of documenting this in a comment, modern Python expresses the possibility directly.
def find_user(id: int) -> User | None:
The function advertises its contract. Readers immediately know this function may legitimately return nothing.
8. Union Types
Sometimes a value can have multiple forms.
def parse(value: int | str):
This modern syntax is far easier to read than the older Union[int, str] and communicates the
same idea.
9. TypedDict
JSON data is everywhere. Without typing,
user = {
"name": "Dan",
"age": 44,
}
nothing prevents someone later writing user["age"] = "forty-four". Instead:
from typing import TypedDict
class User(TypedDict):
name: str
age: int
This describes exactly what the dictionary is supposed to contain.
10. Protocol: Duck Typing Meets Static Analysis
One of the most elegant modern Python features is Protocol. Python has always embraced duck
typing.
If it behaves like a duck, treat it as a duck.
Protocols simply make that idea explicit.
from typing import Protocol
class Retriever(Protocol):
async def search(
self,
query: str,
) -> list[str]:
...
Any class implementing search() automatically satisfies the protocol. No inheritance required.
No abstract base classes. Just behaviour.
11. Generics
Suppose we write:
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
Now first([1, 2, 3]) returns an int, while first(users) returns a User. One implementation.
Many concrete types.
12. Type Hints in AI Systems
Modern AI applications are built from many interacting components.
class Document(BaseModel):
text: str
score: float
async def retrieve(query: str) -> list[Document]:
...
Now every downstream function immediately understands what it receives. Your editor can
autocomplete document.score and document.text without opening another file. As systems grow
larger, this dramatically reduces cognitive load.
13. The Philosophy
Type hints are often presented as a feature. They’re better understood as documentation that computers can verify. Humans have always written comments explaining what functions expect. Comments become outdated. Type annotations can be checked automatically — they become living documentation.
Core idea: Python has not become less dynamic. Instead, modern tooling allows us to reason about dynamic programs before they execute. That is perhaps the biggest change in idiomatic Python over the last decade.
The language did not fundamentally change. Our ability to reason about it did.