Advanced type hints go beyond annotating function parameters with basic types. They let you express relationships between types using generics, define interfaces with Protocol, and write type-safe code that static checkers can verify without changing Python's runtime behaviour.
The standard library typing module was introduced in Python 3.5 and has evolved significantly. Python 3.9 let you write list[int] instead of List[int] without importing from typing, and Python 3.12 added new syntax for declaring generic classes and functions directly, such as class Stack[T]. This article focuses on the patterns that type checkers like mypy and pyright understand.
Type hints are optional at runtime. Python never enforces them. But for large codebases, libraries, and team projects, they catch bugs before code runs and serve as living documentation that cannot go stale.
The article on building a Python library with advanced features shows type hints like these used throughout a real public API.
TypeVar: making functions generic
A TypeVar is a placeholder that represents any type. It lets you write a function that preserves the relationship between argument types and return types.
Without generics, a function that returns the first element of a list would lose type information. The return type would be Any or object.
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
num = first([1, 2, 3])
text = first(["a", "b", "c"])
print(num)
print(text)The type checker infers that num is int and text is str. It knows that first called on a list of integers returns an integer.
1
aYou can constrain a TypeVar to a fixed set of allowed types. The function will only accept arguments matching those specific types, and the type checker rejects anything else at analysis time.
Number = TypeVar("Number", int, float)
def double(value: Number) -> Number:
return value * 2
print(double(5))
print(double(3.5))The type checker accepts int and float arguments and rejects any other type at analysis time. The return type is inferred to match the specific argument type passed at each individual call site.
10
7.0Bounded TypeVars restrict to subtypes of a base class. This is useful when you need to call methods defined on the base class.
Generic classes
A Generic class is parameterized by one or more type variables. This lets you build reusable containers that preserve type information about their contents.
from typing import Generic, TypeVar
K = TypeVar("K")
V = TypeVar("V")
class Pair(Generic[K, V]):
def __init__(self, key: K, value: V):
self.key = key
self.value = value
pair: Pair[str, int] = Pair("age", 30)
print(pair.key, pair.value)The type checker knows that pair.key is a string and pair.value is an integer. Any mismatch between the type annotation and the actual values is flagged.
age 30Generic classes work naturally with Python's simplified generics syntax in Python 3.12 and later. The Generic base class is still the underlying mechanism.
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
stack: Stack[int] = Stack()
stack.push(1)
stack.push(2)
print(stack.pop())The Stack class works with any type. The type checker ensures you only push and pop values of the declared type.
2Protocol: structural subtyping
A Protocol defines an interface that any class can satisfy by having the right methods. No inheritance is required. This matches Python's duck-typing philosophy while giving type checkers the information they need.
from typing import Protocol
class Drawable(Protocol):
def draw(self) -> str: ...
class Circle:
def draw(self) -> str:
return "circle"
class Square:
def draw(self) -> str:
return "square"
def render(shape: Drawable) -> str:
return shape.draw()
print(render(Circle()))
print(render(Square()))Neither Circle nor Square inherits from Drawable. The type checker accepts both because they have the required draw method with the correct signature.
circle
squareProtocols are ideal for library APIs where you want to accept any object with the right methods. They allow callers to pass their own types without depending on your class hierarchy.
Practical patterns for library code
When writing a library, combine generics and protocols to create flexible, type-safe APIs. Use a TypeVar for the parts that vary across call sites and a Protocol for the interface your library requires.
from typing import Protocol, TypeVar
class Comparable(Protocol):
def __lt__(self, other) -> bool: ...
T = TypeVar("T", bound=Comparable)
def maximum(a: T, b: T) -> T:
return a if a > b else b
print(maximum(10, 20))
print(maximum("apple", "banana"))The function works with any type that supports comparison. The type checker ensures both arguments are the same type and that the type implements comparison.
20
bananaThe article on advanced Python mistakes to avoid covers common pitfalls with type hints, including over-annotation, runtime type checking confusion, and when to keep things simple rather than adding generics.
Rune AI
Key Insights
- TypeVar creates a type variable that can be used across a function or class signature.
- Bounded TypeVars restrict to subtypes; constrained TypeVars restrict to a fixed set of types.
- Generic classes parameterize over type variables for reusable, type-safe containers.
- Protocol defines structural interfaces: any class with matching methods satisfies the protocol.
- Type hints are optional at runtime but invaluable for tooling, documentation, and large codebases.
Frequently Asked Questions
What is a TypeVar in Python?
What is the difference between Protocol and ABC in Python?
Conclusion
Advanced type hints turn Python's dynamic nature into a statically verifiable contract without sacrificing flexibility. TypeVars with constraints and bounds let you express precise relationships between types. Generic classes provide reusable, type-safe containers. Protocols enable structural subtyping that matches Python's duck-typing philosophy. Used together, these tools make large Python codebases safer and more maintainable.
More in this topic
How to Test Python Code
Learn what testing means in Python, the core ideas behind automated tests, and the built-in and third-party tools that make testing practical.
Write Your First Python Unit Test
Write and run your first Python unit test step by step, using unittest to check a small function and understand the test output.
The Factory Pattern in Python
Learn how to use the factory pattern in Python to encapsulate object creation, making your code more flexible and easier to extend.