Defensive programming
Checking every argument in every function is not defense, it is noise that
buries the one check that matters and slows code that never sees bad input.
Real defense draws a line: validate untrusted data once at the boundary,
establish invariants, and let interior code trust them, then fail loudly if
one is ever violated.
Method
- Validate all external input at the entry point. HTTP handlers, CLI
parsers, file readers, queue consumers: reject malformed data here with
a clear error before it reaches a line of business logic. Use a schema
(zod, pydantic, JSON Schema) so the check and the type stay in sync.
- Trust your own interior. Once data has crossed a validated boundary,
do not re-validate it in every private helper. Re-checking known-good
data hides where the real contract lives and trains readers to skim past
checks that do matter.
- Assert invariants, do not handle them.
assert balance >= 0
documents a fact your code guarantees; if it fails, that is a bug, and
the program should stop, not recover. Never assert on user input, only
on conditions your own code promises.
- Fail fast at construction. Reject an invalid object in its
constructor or factory so an illegal instance never exists. A
Percentage that refuses values outside 0 to 100 means no downstream
code ever guards that range again.
- Guard the preconditions you cannot type. When the type system
cannot express "list must be non-empty" or "start before end", check it
at the top of the function and throw with the offending values. Keep the
check adjacent to the assumption it protects.
- Keep asserts in the build that matters. Python strips
assert under
-O; C strips under NDEBUG. If an invariant must hold in production,
use a real check that throws, not an assert that silently vanishes.
Checks
- Is every field validated exactly once, at the boundary, and trusted
after?
- Would a violated internal assumption crash immediately, or drift and
corrupt data first?
- Can an object of this type exist in an invalid state at all?
Boundaries
Security-critical trust boundaries (auth, deserialization of hostile
input) need validation even on paths you nominally own, because the threat
model, not the call graph, defines the boundary there. Where a framework
already validates at its edge, defer to it rather than building a redundant
second wall.
1---2name: defensive-programming3description: Validate untrusted data once at the boundary, then trust invariants inside and fail loudly if they ever break. Use when writing entry points, constructors, or any code that must not proceed on bad state.4---56# Defensive programming78Checking every argument in every function is not defense, it is noise that9buries the one check that matters and slows code that never sees bad input.10Real defense draws a line: validate untrusted data once at the boundary,11establish invariants, and let interior code trust them, then fail loudly if12one is ever violated.1314## Method15161. **Validate all external input at the entry point.** HTTP handlers, CLI17 parsers, file readers, queue consumers: reject malformed data here with18 a clear error before it reaches a line of business logic. Use a schema19 (zod, pydantic, JSON Schema) so the check and the type stay in sync.202. **Trust your own interior.** Once data has crossed a validated boundary,21 do not re-validate it in every private helper. Re-checking known-good22 data hides where the real contract lives and trains readers to skim past23 checks that do matter.243. **Assert invariants, do not handle them.** `assert balance >= 0`25 documents a fact your code guarantees; if it fails, that is a bug, and26 the program should stop, not recover. Never assert on user input, only27 on conditions your own code promises.284. **Fail fast at construction.** Reject an invalid object in its29 constructor or factory so an illegal instance never exists. A30 `Percentage` that refuses values outside 0 to 100 means no downstream31 code ever guards that range again.325. **Guard the preconditions you cannot type.** When the type system33 cannot express "list must be non-empty" or "start before end", check it34 at the top of the function and throw with the offending values. Keep the35 check adjacent to the assumption it protects.366. **Keep asserts in the build that matters.** Python strips `assert` under37 `-O`; C strips under `NDEBUG`. If an invariant must hold in production,38 use a real check that throws, not an assert that silently vanishes.3940## Checks4142- Is every field validated exactly once, at the boundary, and trusted43 after?44- Would a violated internal assumption crash immediately, or drift and45 corrupt data first?46- Can an object of this type exist in an invalid state at all?4748## Boundaries4950Security-critical trust boundaries (auth, deserialization of hostile51input) need validation even on paths you nominally own, because the threat52model, not the call graph, defines the boundary there. Where a framework53already validates at its edge, defer to it rather than building a redundant54second wall.