Runtime Validation
When Static Types Are Not Enough
Static type checkers verify that types are consistent, but they cannot
check that a matrix has the right shape, that eigenvalues are positive,
or that a probability sums to one. These are value constraints that
require runtime checks. This section covers the spectrum from simple
assert statements to structured validation with beartype.
Definition: Assertion
Assertion
An assertion is a runtime check that a condition holds:
assert H.shape[0] == y.shape[0], (
f"Dimension mismatch: H has {H.shape[0]} rows, "
f"y has {y.shape[0]} elements"
)
If the condition is False, Python raises AssertionError with the
given message. Assertions are disabled when Python runs with the
-O (optimize) flag, so they must never be used for input validation
in production code β only for internal consistency checks.
Definition: Exception Hierarchy for Scientific Code
Exception Hierarchy for Scientific Code
Python's built-in exceptions map to common scientific programming errors:
| Exception | Use case |
|---|---|
ValueError |
Wrong value: negative variance, invalid SNR range |
TypeError |
Wrong type: passing a list where ndarray expected |
RuntimeError |
Algorithm failure: convergence not reached |
OverflowError |
Numerical overflow in computation |
IndexError |
Out-of-bounds antenna or subcarrier index |
Raise specific exceptions with descriptive messages:
if n_antennas <= 0:
raise ValueError(f"n_antennas must be positive, got {n_antennas}")
Definition: beartype β O(1) Runtime Type Checking
beartype β O(1) Runtime Type Checking
beartype is a Python library that enforces type hints at runtime
with near-zero overhead. Unlike isinstance checks, beartype
understands generic types, unions, and nested containers:
from beartype import beartype
@beartype
def compute_ber(
tx_bits: np.ndarray,
rx_bits: np.ndarray,
) -> float:
return float(np.mean(tx_bits != rx_bits))
If you pass a list instead of np.ndarray, beartype raises a
BeartypeCallHintParamViolation immediately β no need to wait
for a cryptic error three functions deep.
Historical Note: The assert Statement's Dual Identity
1998The assert statement was added to Python 1.5 (1998) inspired by
C's assert.h macro. Like its C ancestor, Python's assert was
designed for debugging aids that can be stripped from production
builds. The -O flag (optimize) sets __debug__ to False and
removes all assert statements from the bytecode. This dual
identity β present in development, absent in production β has been
a source of bugs when developers use assertions for input validation.
Theorem: Assertion vs. Exception: Decision Rule
Use assert for conditions that should never be false if the code
is correct (internal invariants). Use exceptions (raise ValueError)
for conditions that can legitimately be false due to user input or
external data. Formally:
- assert: The violation indicates a bug in the program.
- raise: The violation indicates invalid input or environment.
An assertion is a note to yourself: "if this fails, I have a bug." An exception is a note to the user: "the input you gave me is wrong." The key test: would the check still be needed if the code were perfect? If yes, use an exception. If no, use an assertion.
If removing assert with -O could cause data corruption, it should be an exception.
Shape checks between internal arrays are good assertion candidates.
Shape checks on user-supplied arrays must be exceptions.
Example: Shape Validation for Matrix Operations
Write a function that validates matrix dimensions before performing a MIMO detection, using assertions for internal invariants and exceptions for user-facing checks.
User-facing validation with exceptions
def mimo_detect(
H: np.ndarray,
y: np.ndarray,
method: str = "zf",
) -> np.ndarray:
# User input validation β always runs
if H.ndim != 2:
raise ValueError(f"H must be 2-D, got shape {H.shape}")
if y.ndim != 1:
raise ValueError(f"y must be 1-D, got shape {y.shape}")
if H.shape[0] != y.shape[0]:
raise ValueError(
f"Dimension mismatch: H is {H.shape}, y is {y.shape}"
)
Internal invariant with assertion
Nr, Nt = H.shape
if method == "zf":
# Internal: pinv always returns (Nt, Nr)
H_pinv = np.linalg.pinv(H)
assert H_pinv.shape == (Nt, Nr), "pinv shape invariant"
x_hat = H_pinv @ y
assert x_hat.shape == (Nt,), "output shape invariant"
return x_hat
else:
raise ValueError(f"Unknown method: {method!r}")
Example: beartype for Scientific Function Interfaces
Decorate a simulation pipeline with beartype to catch type errors
at function boundaries rather than deep inside numerical code.
Annotate and decorate
from beartype import beartype
from typing import Literal
import numpy as np
@beartype
def run_simulation(
n_tx: int,
n_rx: int,
snr_db: list[float],
modulation: Literal["BPSK", "QPSK", "16QAM"],
n_trials: int = 1000,
) -> dict[str, np.ndarray]:
results: dict[str, np.ndarray] = {}
for snr in snr_db:
ber = _simulate_one(n_tx, n_rx, snr, modulation, n_trials)
results[f"snr_{snr}"] = ber
return results
See immediate error messages
# This raises BeartypeCallHintParamViolation immediately:
run_simulation(4, 4, [10, 20], "bpsk", 100)
# Error: "bpsk" not in Literal["BPSK", "QPSK", "16QAM"]
# This also fails immediately:
run_simulation(4, 4, "10,20", "BPSK", 100)
# Error: str is not list[float]
Example: Custom Validators for Array Shapes
Write a decorator that validates array shapes at runtime, using a compact syntax to specify expected dimensions.
Create the validator
import functools
import numpy as np
def check_shapes(**expected):
"""Decorator: check_shapes(H="(Nr, Nt)", y="(Nr,)")."""
def decorator(fn):
@functools.wraps(fn)
def wrapper(*args, **kwargs):
import inspect
sig = inspect.signature(fn)
bound = sig.bind(*args, **kwargs)
bound.apply_defaults()
for name, shape_str in expected.items():
arr = bound.arguments[name]
ndim = shape_str.count(",") + 1
if arr.ndim != ndim:
raise ValueError(
f"{name}: expected {ndim}-D ({shape_str}), "
f"got {arr.ndim}-D {arr.shape}"
)
return fn(*args, **kwargs)
return wrapper
return decorator
@check_shapes(H="(Nr, Nt)", y="(Nr,)")
def zf_detect(H: np.ndarray, y: np.ndarray) -> np.ndarray:
return np.linalg.pinv(H) @ y
Numerical Tolerance Explorer
Explore how different tolerance levels (atol, rtol) affect
pass/fail outcomes when comparing floating-point arrays.
Parameters
Common Mistake: Using assert for Input Validation
Mistake:
Using assertions to validate user input or file data:
assert len(data) > 0, "Empty dataset"
assert snr_db >= 0, "SNR must be non-negative"
These checks disappear when running with python -O, silently
accepting invalid inputs.
Correction:
Use explicit exceptions for external input:
if len(data) == 0:
raise ValueError("Empty dataset")
if snr_db < 0:
raise ValueError(f"SNR must be non-negative, got {snr_db}")
Runtime Validation Methods Compared
| Method | Overhead | Disabled by -O? | Best for |
|---|---|---|---|
assert | Zero (removed by compiler) | Yes | Internal invariants, debugging |
raise ValueError | Negligible | No | User input validation |
beartype | O(1) per call | No | Type contract enforcement at boundaries |
isinstance checks | Negligible | No | Simple type guards |
| Pydantic models | Moderate (validation + coercion) | No | Complex config/parameter validation |
Quick Check
What happens to assert statements when Python runs with the -O flag?
They still execute but do not raise errors
They are completely removed from the bytecode
They are converted to warnings
They are moved to a log file
The -O flag sets __debug__ to False and strips all assert statements.
assertion
A debugging statement (assert condition, message) that raises
AssertionError if the condition is False. Removed by python -O.
Related: beartype
beartype
A Python library providing O(1) runtime type checking by validating type hints at function call boundaries via a decorator.
Related: assertion
Runtime Validation Patterns
# Code from: ch04/python/runtime_validation.py
# Load from backend supplements endpoint