LESSON 36 · INTERMEDIATE4 min read

Advanced Type Hints

Use generics, protocols, literals, and strict checking to describe behavior that ordinary annotations cannot capture.

Python 3.12+Free to readModule 7 of 19
YOUR NEXT SMALL WINS

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.

generic_first.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

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.

reader_protocol.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

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.

typed_payload.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.

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.

pyproject.tomlTOML
[tool.mypy]python_version = "3.12"strict = truewarn_unused_ignores = truewarn_redundant_casts = true
LET IT CLICK

The 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.
TURN UNDERSTANDING INTO SOMETHING YOU CAN DO

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.

01

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.

your_turn_1.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
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.

02

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.

your_turn_2.py🐍Python
Ctrl + ↵
Output
Click Run or press Ctrl+Enter
Output will appear here after you run the code.
Explore a worked solution
one_possible_solution.pyPython
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.

A SMALL QUESTION. A USEFUL NEXT STEP.

What stayed with you?

Check the ideas while they’re fresh. Your answers stay in this page and feedback appears as you go.

A LITTLE ACTIVE RECALL0 / 3 answered
01What does TypeVar preserve that a plain object annotation does not?
02When does a class satisfy a Protocol?
03What is the safest way to adopt strict typing in a legacy codebase?
⚡CODING CHALLENGE
🔒Required to Mark Lesson Complete

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.

EXPECTED OUTPUT
stack.pop()
main.pyPython 3
Ctrl + ↵
Execution Console
Click "Run & Verify" (or press Ctrl + Enter) to test your code against the challenge.

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.

Solve the hands-on coding challenge in the knowledge section to verify your code and unlock lesson completion.