Python 3.14 ·
✓ verified by execution on 2026-07-31
Python is a dynamically typed language, but adding static type hints enables tools like mypy to catch bugs before you even run your code. Beyond basic int or list hints, Python’s typing module unlocks incredibly powerful abstractions for library authors and massive codebases.
Type hints change nothing at runtime. Python will happily pass a string to a parameter annotated int. Their value is entirely in what reads them: type checkers, editors, and the next person to open the file.
python
def func(x: int) -> int: return x# Python ignores the type mismatch at runtime:print(func("hello") == "hello")
Output
True
Your output
TypeVar and Generic
A TypeVar is a placeholder that says the same type, wherever it appears. It is what lets you write a function whose return type depends on what you passed in, instead of collapsing everything to Any.
What if you have a class that holds items, but you want to statically guarantee that if you put an int in, you get an int out? You can’t just type hint it as Any. Instead, you use a TypeVar and inherit from Generic.
python · visualize
from typing import TypeVar, GenericT = TypeVar('T')class Box(Generic[T]): def __init__(self, item: T): self.item = item# We instantiate a Box explicitly telling static checkers it holds an intb = Box[int](5)print(Box.__parameters__ == (T,))
True
Structural Subtyping with Protocol
A Protocol types things by shape rather than ancestry: anything with the right methods qualifies, no inheritance required. That is what makes it usable with classes you did not write.
Python developers love “duck typing” (if it walks like a duck and quacks like a duck, it is a duck). But how do you type hint a duck? You use a Protocol.
A Protocol doesn’t require explicit inheritance. If an object implements the methods defined in the Protocol, the static type checker accepts it!
python
from typing import Protocolclass Duck(Protocol): def quack(self) -> str: ...# RealDuck does NOT inherit from Duckclass RealDuck: def quack(self) -> str: return "quack"def make_it_quack(d: Duck) -> str: return d.quack()print(make_it_quack(RealDuck()) == "quack")
Output
True
Your output
Function Overloading
@overload describes a function whose return type depends on its arguments — passing an int gives you an int back, passing a str gives you a str. You write the signatures for the checker, then one real implementation underneath.
Sometimes a function’s return type completely depends on its input type. The @overload decorator lets you define multiple valid type signatures for a single function. Note that you must still write the actual single implementation underneath them!
python
from typing import overload@overloaddef process(x: int) -> int: ...@overloaddef process(x: str) -> str: ...# The actual implementation handles bothdef process(x: int | str) -> int | str: if isinstance(x, int): return x * 2 return x + xprint(process(3) == 6)print(process("a") == "aa")
Output
True
True
Your output
ParamSpec for Decorators
ParamSpec solves the decorator problem: it forwards a function’s entire parameter list to the wrapper, so a decorated function keeps its real signature instead of degrading to (*args, **kwargs).
Typing decorators used to be a nightmare because decorators wrap functions of any signature. ParamSpec solves this by capturing the exact parameter signature (*args and **kwargs) of the wrapped function so the type checker knows the returned wrapper requires those exact arguments!
typing.get_type_hints() reads annotations back at runtime and resolves string forward references. It is the entry point for tools that act on hints — validators, serialisers, dependency injectors.
If you are building a framework (like FastAPI or Pydantic) that actually uses type hints at runtime to do validation, you can extract the resolved annotations using get_type_hints().
python
from typing import get_type_hintsdef func(x: int) -> str: return str(x)hints = get_type_hints(func)print(hints['x'] is int)
Output
True
Your output
Check yourself
Does applying type hints in Python actively enforce type correctness when your code is run?
Reveal answer
No, type hints are completely ignored by the Python runtime. — Type hints are completely ignored by the standard Python runtime; they are only used by static analysis tools like mypy or pyright before the code executes.
What is the primary difference between nominal subtyping and structural subtyping using Protocol?
Reveal answer
Protocol checks if an object implicitly matches the required methods (duck typing) without needing explicit inheritance. — Protocols are structural; an object is implicitly compatible if it has the required methods. Explicit inheritance is nominal subtyping.
What does the @overload decorator do?
Reveal answer
It allows defining multiple type signatures for static checkers, but you must still provide a single unified runtime implementation. — @overload is purely for static type checkers; you must still provide a single implementation that handles all types using isinstance() at runtime.
Challenges
Challenge 1 +100 XP
Create a Protocol named 'Flyer' with a 'fly(self) -> None:' method. Then create a class 'Bird' with a 'fly' method that prints 'Flap'. Do not inherit Flyer. Finally, call a function 'make_fly(f: Flyer)' passing a Bird instance.