Advanced Type Hints
Advanced type hints let you describe flexible, reusable, and precisely-shaped types — generics, structural protocols, and typed dictionaries — that static checkers verify before your code ever runs.
Learn Advanced Type Hints in our free Python course — an interactive lesson with runnable examples, a practice exercise and a quick reference.
Part of the free Python course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
These are the tools that make large Python codebases maintainable. The interpreter ignores them at runtime, but mypy and your editor turn them into a powerful safety net.
A value that could be one of several types is a Union . A value that could be a given type or None is Optional . Since Python 3.10 you can write both with a clean pipe:
Crucially, these annotations do not run. Python never checks them while executing — a function annotated -> int will return a string without complaint. They exist for tools like mypy and your editor:
A generic lets you write a container once and reuse it for any element type, while keeping the type information. The classic approach uses a TypeVar and Generic :
Python 3.12 added a much cleaner built-in syntax — no TypeVar import needed. It means the same thing:
Generics are why list[int] and dict[str, float] work: the built-in containers are generic too. Your own classes can join them with either syntax above.
A Protocol describes a shape : any object with the right methods fits, no inheritance required. This is duck typing made checkable. Add @runtime_checkable to also allow isinstance tests:
A TypedDict nails down the keys and value types of a dictionary, and Callable describes a function's signature when you pass functions around:
Complete the runtime-checkable Protocol so the isinstance checks work. Replace each ___ and match the expected output.
Decorator blank: runtime_checkable — so the class is decorated with @runtime_checkable , which lets isinstance work against the Protocol.
✅ Run a static checker — mypy file.py — to actually catch type mismatches.
❌ isinstance against a Protocol without @runtime_checkable
✅ Decorate the Protocol with @runtime_checkable before using it in isinstance .
❌ Thinking a TypedDict is a special class at runtime
✅ A TypedDict is just a dict at runtime; only a type checker flags the misspelled key. Verify with mypy.
Combine a Protocol, a TypedDict, and a Callable: render any object that has a label, store records as typed dicts, and apply a transform function.
Go deeper with the official Python documentation:
Lesson complete — your types are precise and reusable now!
You can express alternatives with Optional , Union , and X | Y , write generic containers with TypeVar (or 3.12's inline syntax), describe shapes with Protocol and TypedDict , type functions with Callable , and you understand that hints are checked by mypy, not the interpreter.
🚀 Up next: Checkpoint: The Standard Library — put it all together in a build challenge.
Practice quiz
Optional[int] is shorthand for which of these?
Optional[int] means 'int or None' — exactly Union[int, None]. In Python 3.10+ you can also write int | None.
Which is the modern (Python 3.10+) way to write 'int or None'?
- int | None
- int & None
- int + None
- Optional(int, None)
Answer: int | None. The pipe syntax int | None means the same as Optional[int] / Union[int, None] and is the cleaner modern form.
At runtime, what happens when an argument's value does not match its type hint?
- Python raises a TypeError immediately
- The value is auto-converted to the hinted type
- The function returns None
- Nothing — Python runs normally; only static checkers flag it
Answer: Nothing — Python runs normally; only static checkers flag it. Hints are not enforced by the interpreter. A function annotated -> int can return a string and run fine; mypy would catch the mismatch beforehand.
What does a TypeVar (e.g. T = TypeVar('T')) provide?
- A runtime type-conversion helper
- A placeholder for 'some type' used to build generics
- A constant that must always be int
- A way to disable type checking
Answer: A placeholder for 'some type' used to build generics. A TypeVar is a stand-in for an arbitrary type. Combined with Generic[T], it lets a class like Stack[T] preserve element type information.
How do you make a class generic using the classic typing syntax?
Subclassing Generic[T] with a TypeVar T is the classic approach. The inline form was added in Python 3.12.
What does a Protocol describe?
- A required base class every conforming object must inherit
- A network message format
- A shape — the methods/attributes an object must have, no inheritance required
- A frozen set of constants
Answer: A shape — the methods/attributes an object must have, no inheritance required. A Protocol enables structural typing: any object with the right methods fits, without inheriting from the Protocol.
What must you add to a Protocol to use it with isinstance() at runtime?
- @dataclass
- @runtime_checkable
- @staticmethod
- Nothing — Protocols always support isinstance
Answer: @runtime_checkable. Decorating the Protocol with @runtime_checkable lets isinstance(obj, MyProtocol) work; otherwise it raises a TypeError.
What does a TypedDict let you specify?
- A dict whose keys are checked for spelling at runtime
- An immutable dictionary
- A dict that can only contain integers
- The exact keys of a dict and the type of each value
Answer: The exact keys of a dict and the type of each value. A TypedDict declares each key and its value type (e.g. class Movie(TypedDict): title: str; year: int). At runtime it is just a normal dict.
What does Callable[[int], int] describe?
- A list containing an int
- A function taking one int and returning an int
- A callable that returns nothing
- An int that is also callable
Answer: A function taking one int and returning an int. Callable[[arg types], return type] describes a function's signature — here one int parameter and an int return value.
When using @runtime_checkable, why does isinstance('hello', Sized) return True if Sized requires __len__?
- All objects pass runtime-checkable Protocols
- Strings explicitly inherit from Sized
- Structural typing — str has a __len__ method, so it fits the Protocol
- isinstance ignores the Protocol's methods
Answer: Structural typing — str has a __len__ method, so it fits the Protocol. Protocols match on structure: because str defines __len__, it satisfies a Sized Protocol — no inheritance from Sized needed.