python 53 lines · 8 steps

A runtime type checker from annotations

A decorator reads a function's type hints and enforces them on arguments and return values at call time.

Explained by highlit
1import functools
2import inspect
3from typing import get_type_hints, get_origin, get_args, Union
4 
5 
6def _check(value, expected):
7 origin = get_origin(expected)
8 if origin is Union:
9 return any(_check(value, arg) for arg in get_args(expected))
10 if origin in (list, tuple, set, frozenset):
11 if not isinstance(value, origin):
12 return False
13 item_types = get_args(expected)
14 if not item_types:
15 return True
16 if origin is tuple and len(item_types) != 2 or (item_types and item_types[-1] is not Ellipsis):
17 return all(_check(v, item_types[0]) for v in value)
18 return all(_check(v, item_types[0]) for v in value)
19 if origin is dict:
20 if not isinstance(value, dict):
21 return False
22 kt, vt = get_args(expected) or (object, object)
23 return all(_check(k, kt) and _check(v, vt) for k, v in value.items())
24 if expected is inspect.Parameter.empty or expected is None:
25 return True
26 return isinstance(value, expected)
27 
28 
29def typechecked(func):
30 hints = get_type_hints(func)
31 sig = inspect.signature(func)
32 
33 @functools.wraps(func)
34 def wrapper(*args, **kwargs):
35 bound = sig.bind(*args, **kwargs)
36 bound.apply_defaults()
37 for name, value in bound.arguments.items():
38 expected = hints.get(name)
39 if expected is not None and not _check(value, expected):
40 raise TypeError(
41 f"{func.__qualname__}: argument '{name}' expected {expected}, "
42 f"got {type(value).__name__}"
43 )
44 result = func(*args, **kwargs)
45 ret = hints.get("return")
46 if ret is not None and not _check(result, ret):
47 raise TypeError(
48 f"{func.__qualname__}: return value expected {ret}, "
49 f"got {type(result).__name__}"
50 )
51 return result
52 
53 return wrapper
01 / 01
STEP 01

Walkthrough

Space play step click any line
Three takeaways
  1. 1Type hints are ordinary runtime data you can inspect and act on, not just static-analysis metadata.
  2. 2Recursing on typing origins and args lets one function validate arbitrarily nested generic types.
  3. 3Binding a signature and applying defaults gives you a clean name-to-value map for validating every argument uniformly.

Related explainers

Share this explainer

Here's the card — post it anywhere.

A runtime type checker from annotations — share card
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code