Skip to content
advanced Phase 12 · Python Type Hints & Quality

Generics & Protocols

Write generic functions and classes with TypeVar, Generic, and Protocol.

1h
0 problems
Topic Progress 0%

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 allow list[Dog] where list[Animal] is written, breaking type safety
  • Callable parameters 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?

Question 1 options

2. How does a Protocol differ from an ABC?

Question 2 options

3. Why are mutable generic containers invariant by default?

Question 3 options

Flashcards

Question

TypeVar bound vs constraint

Answer

bound=str accepts str subclasses; TypeVar("T", int, float) accepts only exactly those types.

Question

Protocol

Answer

Structural interface: an object qualifies by having matching methods, no inheritance needed.

Question

runtime_checkable

Answer

Enables isinstance(obj, Proto); checks method names only, never signatures.

Question

Covariance in one line

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.

Continue Learning