Back to Insights

Python Engineering · Static Analysis

Type Hints: Under the Hood

Type hints are not for the interpreter. They are documentation that computers can verify.

Python type hints — what the interpreter sees vs what static analysers see
Python type hints — what the interpreter sees vs what static analysers see
PY Type Hints

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
Dan Stativa

Adding type hints to an existing Python codebase?

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.

Dan Stativa

Adding type hints to an existing Python codebase?