Python typing Module: Generics and Protocols That Catch Real Bugs at Compile Time

Python typing Module: Generics and Protocols That Catch Real Bugs at Compile Time

Python type hints are not just documentation. When used correctly with mypy, they catch bugs at “compile time” that would otherwise surface as runtime TypeErrors in production. The typing module is far more powerful than most developers realize — most stop at List[str] and miss the real power entirely.

TL;DR: Use TypeVar with bounds for generic functions, Protocol instead of ABCs for structural typing, ParamSpec for decorator typing, and TypeGuard for narrowing. These four tools catch an entire class of runtime errors at development time.

TypeVar: Generic Functions Done Right

from typing import TypeVar, Generic, Sequence

T = TypeVar('T')
N = TypeVar('N', int, float)  # Constrained TypeVar
Comparable = TypeVar('Comparable', bound='SupportsLessThan')

# Generic function — preserves type through transformation
def first(seq: Sequence[T]) -> T:
    return seq[0]

first([1, 2, 3])    # int
first(['a', 'b'])   # str — mypy knows the return type

# Constrained TypeVar
def add(a: N, b: N) -> N:
    return a + b

add(1, 2)      # OK — both int
add(1.0, 2.0)  # OK — both float
add(1, 2.0)    # mypy error — mixed types not allowed

# Generic class
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:
        return self._items.pop()

stack = Stack[int]()
stack.push(1)       # OK
stack.push('hello') # mypy error — str not allowed in Stack[int]

Protocol: Structural Typing Without Inheritance

from typing import Protocol, runtime_checkable

# Protocol defines a structural interface — any class that has these
# methods satisfies the protocol, no explicit inheritance needed
@runtime_checkable
class Drawable(Protocol):
    def draw(self) -> None: ...
    def resize(self, factor: float) -> None: ...

class Circle:  # No Drawable inheritance!
    def draw(self) -> None: print("Drawing circle")
    def resize(self, factor: float) -> None: self.radius *= factor

class Square:
    def draw(self) -> None: print("Drawing square")
    def resize(self, factor: float) -> None: self.side *= factor

def render(shape: Drawable) -> None:
    shape.draw()

render(Circle())  # OK — Circle satisfies Drawable structurally
render(Square())  # OK — Square satisfies Drawable structurally
render("hello")   # mypy error — str does not satisfy Drawable

# Runtime check (due to @runtime_checkable)
print(isinstance(Circle(), Drawable))  # True

ParamSpec: Type Decorators Correctly

from typing import TypeVar, Callable, ParamSpec
import functools

P = ParamSpec('P')
R = TypeVar('R')

# Decorator that preserves function signature
def retry(times: int) -> Callable[[Callable[P, R]], Callable[P, R]]:
    def decorator(func: Callable[P, R]) -> Callable[P, R]:
        @functools.wraps(func)
        def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
            for attempt in range(times):
                try:
                    return func(*args, **kwargs)
                except Exception:
                    if attempt == times - 1: raise
        return wrapper
    return decorator

@retry(times=3)
def fetch_user(user_id: int, timeout: float = 5.0) -> dict:
    return {}

# mypy knows fetch_user takes (int, float=5.0) -> dict
fetch_user(123)           # OK
fetch_user(123, 10.0)     # OK
fetch_user("wrong")       # mypy error — str not int

TypeGuard: Narrowing in Custom Validators

from typing import TypeGuard, Union

# Without TypeGuard — mypy can't narrow the type
def is_str_list(val: list[object]) -> bool:
    return all(isinstance(x, str) for x in val)

def process(val: list[object]) -> None:
    if is_str_list(val):
        val[0].upper()  # mypy error — still list[object]

# With TypeGuard — mypy narrows type in if branch
def is_str_list_tg(val: list[object]) -> TypeGuard[list[str]]:
    return all(isinstance(x, str) for x in val)

def process_typed(val: list[object]) -> None:
    if is_str_list_tg(val):
        val[0].upper()  # OK — mypy knows val is list[str] here

Python Typing Cheat Sheet

  • TypeVar('T') for generic functions that preserve input type
  • TypeVar('N', int, float) for constrained generics
  • Protocol instead of ABC for structural typing
  • ParamSpec to type decorators that preserve function signatures
  • TypeGuard[T] for custom type narrowing functions
  • ✅ Run mypy in CI — catches bugs tests miss
  • ❌ Don’t annotate everything — focus on public API surfaces
  • ❌ Don’t use Any — it disables type checking entirely

Python typing pairs naturally with Python dataclasses@dataclass generates type-safe __init__ methods automatically. For runtime performance of typed code, see how __slots__ optimization reduces the memory overhead of heavily-typed class hierarchies. External reference: mypy generics documentation.

Master Python type hints and static analysis

View Course on Udemy — Hands-on video course covering every concept in this post and more.

Sponsored link. We may earn a commission at no extra cost to you.


Discover more from CheatCoders

Subscribe to get the latest posts sent to your email.

Comments

No comments yet. Why don’t you start the discussion?

Leave a Reply