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
‹ swipe to step through ›
Walkthrough
Space play
←→ step
click any line
Three takeaways
- 1Type hints are ordinary runtime data you can inspect and act on, not just static-analysis metadata.
- 2Recursing on typing origins and args lets one function validate arbitrarily nested generic types.
- 3Binding a signature and applying defaults gives you a clean name-to-value map for validating every argument uniformly.
Related explainers
python
from fastapi import FastAPI, WebSocket, WebSocketDisconnect app = FastAPI()
Building a WebSocket chat with FastAPI
websockets
broadcast
connection-management
Intermediate
9 steps
python
import time import uuid from django.utils.deprecation import MiddlewareMixin
Attaching per-request context in Django
middleware
request lifecycle
multi-tenancy
Intermediate
7 steps
javascript
function evaluate(expression) { const tokens = tokenize(expression); let pos = 0;
Building a recursive descent calculator
parsing
recursion
operator-precedence
Intermediate
8 steps
python
import random from typing import Iterator, List
How reservoir sampling picks k items
reservoir-sampling
streaming
randomness
Intermediate
5 steps
python
import secrets from django.contrib.auth import authenticate, login from django.core.cache import cache
Two-factor login with OTP in Django
two-factor-auth
one-time-passwords
caching
Intermediate
9 steps
python
import re from functools import total_ordering from typing import Optional
Parsing and comparing semantic versions
regex
operator-overloading
sorting
Intermediate
7 steps
Share this explainer
Here's the card — post it anywhere.
Made with highlit — turn any snippet into a walkthrough like this in about a minute.
Explain your code
Embed this explainer
Drop the interactive walkthrough into a blog or docs. Views never cost a credit.
<iframe src="https://highlit.co/explainers/a-runtime-type-checker-from-annotations-explained-python-419b/embed?autoplay=1" width="100%" height="520" loading="lazy" style="border:0"></iframe>
Autoplay is on by default — add ?autoplay=0 to start paused.