TypeVar, Bounds, and Generic Classes
TypeVar, Bounds, and Generic Classes
A TypeVar is a placeholder that stays consistent across a signature:
from typing import Generic, TypeVar
T = TypeVar("T")
Num = TypeVar("Num", int, float) # constrained: only these two
Key = TypeVar("Key", bound=str) # bounded: any str subclass
def first(items: list[T]) -> T: # generic function
return items[0]
class Stack(Generic[T]): # generic class
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
Bounded TypeVars let the checker accept shared methods (bound=str allows .upper()), while constrained TypeVars keep the result type tied to whichever argument type was passed. Python 3.12 shortens this to def first[T](items: list[T]) -> T.
Protocols for Structural Typing
Protocols for Structural Typing
Inheritance says a class is a thing; a Protocol says it merely behaves like one:
from typing import Protocol, runtime_checkable
@runtime_checkable
class Writer(Protocol):
def write(self, data: bytes) -> None: ...
def persist(w: Writer, payload: bytes) -> None:
w.write(payload) # any object with a compatible write() works
File objects, sockets, and test doubles satisfy Writer with no shared base class — the checker just matches method signatures. Mark a Protocol @runtime_checkable only when you need isinstance(), which tests method presence alone and cannot verify signatures; it works on non-data protocols only.
Variance and Pitfalls
Variance and Pitfalls
Variance decides how generic types relate: TypeVar("T", covariant=True) allows DogOwner where AnimalOwner is expected; contravariant flips that; the default is invariant.
Rules of thumb:
- Read-only containers (
Sequence,Callable[..., R]return) can be covariant - Mutable containers (
list,dict,MutableSequence) must stay invariant — covariance would allowlist[Dog]wherelist[Animal]is written, breaking type safety Callableparameters are contravariant
Interview angle: show a Protocol with no @runtime_checkable in a function signature, explain that isinstance cannot confirm parameter types, and describe when cast or assert isinstance is the pragmatic bridge.
Quiz
1. What does TypeVar("T", bound=int) mean?
2. How does a Protocol differ from an ABC?
3. Why are mutable generic containers invariant by default?
Flashcards
Question
TypeVar bound vs constraint
Click to reveal answer
Answer
bound=str accepts str subclasses; TypeVar("T", int, float) accepts only exactly those types.
Question
Protocol
Click to reveal answer
Answer
Structural interface: an object qualifies by having matching methods, no inheritance needed.
Question
runtime_checkable
Click to reveal answer
Answer
Enables isinstance(obj, Proto); checks method names only, never signatures.
Question
Covariance in one line
Click to reveal answer
Answer
Subtype of generic can substitute where supertype generic expected — safe for read-only containers.
Revision Notes
Key Takeaways
- 1. TypeVars keep argument and return types linked across a generic signature
- 2. Protocols model capability (has .write) instead of lineage (is a Base)
- 3. Variance protects mutable containers: only read-only types can safely be covariant
Interview Tips
- • Contrast nominal ABC checks with structural Protocol matching and give a concrete example of each
- • Walk why list is invariant while Sequence can be covariant, using a write-through failure scenario
Cheat Sheet
TypeVar(bound=)/TypeVar("T",int,float); Generic[T] classes; Protocol = structural typing; runtime_checkable → isinstance checks names only; mutable generics invariant.