| name | defensive-programming-python |
| description | Tiger Style defensive programming principles for Python. Covers assertions, readability, error handling, and exhaustiveness checks. Use when writing or reviewing Python code, adding assertions, handling errors, or when the user mentions "defensive programming", "tiger style", "assertions", "exhaustive match", "handle exceptions", or needs a code review.
|
Defensive Programming — Python
Inspired by Tiger Style.
Assertions
Assert preconditions, postconditions, and invariants at every boundary.
Minimum 2 assertions per function. Note: assert is stripped with -O.
Prefer a custom invariant helper for production code.
Custom invariant helper
def invariant(condition: bool, msg: str = "") -> None:
if not condition:
raise AssertionError(f"Invariant: {msg}")
Pair assertions
For every property, assert before writing and again after reading from a boundary.
def persist(data: bytes) -> None:
assert len(data) > 0, "cannot persist empty data"
write_to_disk(data)
def load() -> bytes:
data = read_from_disk()
assert len(data) > 0, "corrupt data on disk"
return data
Split compound assertions
assert user is not None
assert user.id > 0
assert user is not None and user.id > 0
Function boundary assertions
def calculate_total(items: list[Item]) -> float:
assert len(items) > 0, "expected at least one item"
total = 0.0
for item in items:
assert item.price > 0, "item price must be positive"
total += item.price
assert total > 0, "total must be positive"
return total
Readability
70-line function limit
Every function must fit on one screen. Push control flow up, push computation down.
snake_case (language convention)
Python uses snake_case — this aligns with Tiger Style.
def parse_user_input(raw: str) -> Result: ...
def parseUserInput(raw: str) -> Result: ...
Variable names with units
timeout_ms = 5000
max_retries = 3
latency_ms_max = 1000
timeout = 5000
max = 3
latency_max = 1000
State invariants positively
if index < length:
else:
if index >= length:
else:
Split compound conditions
if user.is_active:
if user.has_permission("write"):
if user.is_active and user.has_permission("write"):
Smallest scope for variables
def process(order: Order) -> None:
items = order.fetch_items()
assert len(items) > 0
order.submit()
Error Handling
All errors must be handled. 92% of catastrophic production failures
come from unhandled non-fatal errors.
Explicit exception types only
try:
result = risky_operation()
except ValueError:
handle_value_error()
except KeyError:
handle_key_error()
try:
result = risky_operation()
except Exception:
handle_all()
Custom exception hierarchy
class AppError(Exception): ...
class NetworkError(AppError): ...
class TimeoutError(NetworkError): ...
class DnsError(NetworkError): ...
Crash on programmer errors, handle operational errors
def read_config(path: str) -> Config:
with open(path) as f:
raw = f.read()
parsed = json.loads(raw)
assert "host" in parsed, "config missing host"
return parsed
Try/except/else/finally
try:
data = fetch_resource()
except TimeoutError:
retry()
except DnsError:
log_and_fallback()
else:
process(data)
finally:
cleanup()
Exhaustiveness Checks
Python has no compile-time exhaustiveness. Use runtime enforcement.
match + assert_never (3.10+)
from typing import Never
def assert_never(x: Never) -> Never:
raise AssertionError(f"unreachable: {x}")
from dataclasses import dataclass
@dataclass
class Circle:
radius: float
@dataclass
class Rect:
width: float
height: float
@dataclass
class Triangle:
base: float
height: float
type Shape = Circle | Rect | Triangle
def area(shape: Shape) -> float:
match shape:
case Circle(radius=r):
return math.pi * r ** 2
case Rect(width=w, height=h):
return w * h
case Triangle(base=b, height=h):
return b * h / 2.0
case _:
assert_never(shape)
Never return type for mypy enforcement
def never(x: Never) -> Never:
raise AssertionError(f"unreachable: {x}")
def handle(value: str | int) -> str:
match value:
case str(s):
return s.upper()
case int(n):
return str(n)
case _:
never(value)
From Tiger Style:
"The rules act like the seat-belt in your car: initially they are perhaps a little uncomfortable, but after a while their use becomes second-nature and not using them becomes unimaginable."