Advanced Type Hints
Use generics, protocols, literals, and strict checking to describe behavior that ordinary annotations cannot capture.
By the end, you’ll be able to…
- Write generic functions and classes with TypeVar.
- Describe structural contracts with Protocol.
- Use Literal and TypedDict for constrained data.
- Adopt strict checking without rewriting a whole codebase at once.
01Generics preserve relationships
A TypeVar represents a type that varies between calls while preserving a relationship. A first(items: list[T]) -> T promises that the returned element has the same type as the list's elements, not merely some arbitrary type. The checker can then catch a caller that treats the result as the wrong type.
Generic classes use Generic[T] in Python 3.12, or the newer class Stack[T] syntax introduced by PEP 695. Bound a TypeVar when the type must support an operation, such as comparison. Keep generic helpers small; a type variable that appears once adds notation without adding a useful constraint.
02Protocols describe behavior
A Protocol describes the methods and attributes a caller needs. Any class that provides them satisfies the protocol, even without inheriting from it. This is structural typing: the contract is behavior, not ancestry. It fits Python's duck-typed style while still giving the checker something to verify.
Keep protocols small and focused on the caller's needs. A protocol with ten methods is usually a class in disguise and makes testing harder. Mark a protocol runtime_checkable only when you actually need isinstance at runtime; static checking does not require it.
03Constrain values with Literal and TypedDict
Literal restricts a value to specific constants, which is useful for modes, statuses, and command names. TypedDict describes a dictionary with known keys and value types, which is common at JSON boundaries. Both are erased at runtime; they exist to make the checker's job possible.
When a dictionary has optional keys, use total=False or NotRequired rather than weakening every value to object. A type alias gives a repeated complex type a name, and Python 3.12's type statement makes the alias explicit. Prefer a dataclass when the record has behavior; use TypedDict when the wire format is genuinely a dictionary.
04Adopt strict mode deliberately
Strict mode turns on a collection of checks: no implicit Any, return types required, and stricter handling of untyped definitions. It is valuable for a new package and overwhelming for a legacy module. Enable checks incrementally, starting with the files that have the clearest contracts.
Configure the checker in pyproject.toml so every developer and CI job uses the same rules. Use a targeted # type: ignore[code] only when the checker cannot express a real invariant, and leave a reason. A blanket ignore hides future errors along with the one you meant to suppress.
[tool.mypy]python_version = "3.12"strict = truewarn_unused_ignores = truewarn_redundant_casts = trueThe ideas worth keeping.
- Generics preserve type relationships across calls and containers.
- Protocols describe behavior and enable structural typing.
- Literal, TypedDict, and named aliases make wire formats explicit.
Make it yours.
Type and test your code right here in the interactive workspace. Run your solution, check the output, and compare with the worked guide when ready.
Write a generic stack
Implement a Stack[T] with push and pop. Pop should raise IndexError on an empty stack. Demonstrate it with strings and integers.
Explore a worked solution
from typing import Generic, TypeVarT = TypeVar("T")class Stack(Generic[T]): def __init__(self) -> None: self._items: list[T] = [] def push(self, item: T) -> None: self._items.append(item) def pop(self) -> T: if not self._items: raise IndexError("pop from empty stack") return self._items.pop()stack: Stack[str] = Stack()stack.push("hello")print(stack.pop())The TypeVar connects push and pop: a Stack[str] accepts strings and returns strings. The checker can catch an attempt to push an integer into that stack.
Type a configuration dictionary
Define a TypedDict for a service configuration with host: str, port: int, and an optional debug: bool. Write show(config) that returns 'host:port' and demonstrates the default.
Explore a worked solution
from typing import NotRequired, TypedDictclass Config(TypedDict): host: str port: int debug: NotRequired[bool]def show(config: Config) -> str: return f"{config['host']}:{config['port']}"print(show({"host": "localhost", "port": 8000}))NotRequired marks debug as optional without making every key optional. The function reads only the required keys, so the call is valid.
What stayed with you?
Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.
Write a generic stack
Implement a Stack[T] with push and pop. Pop should raise IndexError on an empty stack. Demonstrate it with strings and integers.
stack.pop()
Keep exploring.
The official references behind this lesson, when you want a second angle or the full details.
One lesson closer.
Reading is a start. Trying it yourself makes the connection.