01 · Programs as contracts: values, state and tests
Build a small event normalizer, expose aliasing, and distinguish a test from a proof. Python foundations without a catalogue of syntax.
Editorial review: · What review means
Stored in this browser only. No account, no sync. Clearing browser data removes your record.
By the end, you should be able to
- Write an input and output contract
- Predict shallow-copy aliasing
- Test failure cases without hiding exceptions
Listen to this article
Browser / device speech · no paid TTS integration. Voice quality depends on your device.
Choose a local device voice to avoid a remote speech service. This site adds no TTS service, account or API calls.
Checking browser speech support…
Pause saves your segment; resume repeats that short segment. Changing voice or speed pauses playback. Stop resets to the beginning. Progress counts finished text segments, not audio time. Leaving or hiding this page stops or pauses speech.
What gets read aloud?
Reads the article body as it appears when you press Listen. Navigation, controls and closed sections are skipped. Expand a section, then Stop and Listen to include it. Code and equations get brief notices; figures use available labels or captions, not their visual details. This narration does not teach omitted mathematics or replace reading examples on the page.
For better sound at no added site cost, try installed English voices, including enhanced voices offered by your device. We cannot guarantee a best voice on every browser. Use Stop or your device’s audio controls if its speech engine misbehaves.
In this article · 5 sections
A useful program is a promise about what changes when it receives an input. Before learning thirty patterns, learn to make one promise precise. Our running project is a local job-event notebook: accept small records, summarize them, order dependent work, and store results without counting retries twice. It needs no account, network service or machine-learning model.
You need an installed Python 3.10 or later with its standard library. Save each Python block in a separate scratch file and run it with python3 filename.py; blocks on this page are independent. These examples were executed with Python 3.12.3. No dependency installation or system configuration is necessary. A script is text that Python evaluates; an assertion that succeeds prints nothing. A traceback ending in AssertionError means its claimed condition was false.
A name refers to an object
An integer such as 3 is a value. A list is an ordered, mutable collection. A dictionary associates unique keys with values. Assignment binds a name; it does not promise to clone the referenced object. A function receives object references too. This explains a bug more directly than saying Python “passes everything by reference,” which can misleadingly suggest that rebinding a parameter rebinds the caller's variable.
original = [[1], [2]]
alias = original
shallow = original.copy()
alias.append([3])
shallow[0].append(9)
assert original == [[1, 9], [2], [3]]
assert shallow == [[1, 9], [2]]
assert alias is original
assert shallow is not original
def rebind(items):
items = []
return items
assert rebind(original) == []
assert len(original) == 3Draw three names and two outer list boxes. alias and original point at the same outer box. shallow points at a different outer box, but its first two arrows still point at the original inner lists. Appending to the outer list changes its length; mutating an inner list is visible through both outer lists. Rebinding the function's local name changes no caller-owned box.
The Python data-structures tutorial explicitly specifies that list.copy() is shallow. A tuple prevents replacing its own slots; a tuple containing a list does not freeze that list. “Immutable outer container” and “immutable object graph” are different contracts.
Define a small boundary before a large algorithm
Our notebook accepts an event with two fields: a nonblank string job name and a nonnegative integer duration in milliseconds. Extra fields are ignored. Whitespace at the ends of the name is removed. Booleans are rejected even though Python treats bool as a subclass of int; True is not a useful duration for this interface. The input must be a dictionary. The function returns a new dictionary and does not mutate the input.
def normalize_event(raw: dict) -> dict:
if not isinstance(raw, dict):
raise TypeError("event must be a dictionary")
name = raw.get("job")
duration = raw.get("duration_ms")
if not isinstance(name, str) or not name.strip():
raise ValueError("job must be a nonblank string")
if type(duration) is not int or duration < 0:
raise ValueError("duration_ms must be a nonnegative integer")
return {"job": name.strip(), "duration_ms": duration}
raw = {"job": " ingest ", "duration_ms": 0, "debug": True}
clean = normalize_event(raw)
assert clean == {"job": "ingest", "duration_ms": 0}
assert raw["job"] == " ingest "
assert clean is not raw
for bad in [
{"job": " ", "duration_ms": 1},
{"job": "x", "duration_ms": -1},
{"job": "x", "duration_ms": True},
{"job": "x", "duration_ms": 1.5},
{"job": "x"},
]:
try:
normalize_event(bad)
except ValueError:
pass
else:
raise AssertionError(f"accepted invalid event: {bad!r}")
try:
normalize_event([])
except TypeError:
pass
else:
raise AssertionError("accepted a non-dictionary")def creates a function. Indentation groups its statements. return stops that call with a result; raise stops it with an exception unless a caller handles it. .get() returns None for a missing dictionary key here, letting the same validation reject both missing and invalid values. The for loop runs the negative cases. Its try block expects a particular exception: the else branch deliberately fails if no exception occurred. Catching every exception would accidentally let a programming bug masquerade as successful input validation.
The hints raw: dict and -> dict document intent; they are not enforcement. The official typing documentation says Python does not enforce annotations at runtime. The checks above are what enforce this boundary. Conversely, not every internal helper needs defensive validation at every line: establish where trusted data begins, then state internal preconditions.
Functions should reveal their state
A default argument is evaluated when the function is defined, not freshly on every call. The Python control-flow tutorial demonstrates the resulting shared-list trap. Use None when the default means “create a new collection.”
def add_job(name, jobs=None):
if jobs is None:
jobs = []
jobs.append(name)
return jobs
assert add_job("a") == ["a"]
assert add_job("b") == ["b"]
existing = ["old"]
assert add_job("new", existing) is existing
assert existing == ["old", "new"]This API still mutates an explicitly supplied list. That is intentional and must be documented. To promise no mutation, return list(jobs) + [name] instead, handling None separately. Neither policy is universally correct: hidden policy is the problem.
Tests, explanations and limits
A normal example establishes one observed behavior. Boundary examples challenge a choice: is zero allowed, does missing differ from empty, should booleans count as integers? An invariant is a statement intended to hold across a whole class of executions. A proof explains why it holds; testing samples or exhaustively enumerates a bounded domain. Neither successful compilation nor five passing examples proves correctness for arbitrary objects and inputs.
Assertions here are test checks, not production input validation: Python can disable assert with optimization, as specified by the official assert statement reference. The normalizer uses explicit exceptions for that reason. This small interface does not handle maliciously large strings, Unicode normalization, maximum durations, nested records or a serialized input format. A public service would need byte limits and a separate parsing policy. Do not infer security from the absence of an exception in these examples.
Exercises
- Change the normalizer to reject unknown fields. Does that improve all APIs, or can it break forward compatibility?
- Write a test that distinguishes a shallow copy from a deep copy without using
is. - A function catches
Exceptionand returns{}for every failure. What information does its caller lose?
Answer sketches
- Compare
set(raw)with the allowed keys and reject their difference. Strictness finds misspellings, but older clients may reject new optional fields; the compatibility policy must be explicit. - Copy a list of lists, mutate an inner list in one copy, and inspect the other. A shallow copy shares that inner object; a deep copy of this simple acyclic integer example does not.
- The caller cannot distinguish invalid input from a missing key inside the implementation or another programming error. Catch the errors the boundary intends to translate, preserving other tracebacks.
Exit artifact: a function, a written mutation policy, and tests for valid, missing, zero, negative and wrong-type input. This is the first piece of the capstone.
Pause / Recall / Apply
Can you explain it without the page?
Close the example. Reconstruct the core idea, then change one assumption. Mark complete when you’re ready; you can always undo it.
Stored in this browser only. No account, no sync. Clearing browser data removes your record.