Functions with a contract
Give each piece of behavior a name, inputs, an output, and a promise.
BY THE END, YOU CAN
- Separate a return value from printed output
- Write a function with explicit inputs
- Explain preconditions, postconditions, and side effects
Diagram description: attempt: int → retry_delay() (input); retry_delay() → seconds: float (returns); retry_delay() → ValueError (raises); seconds: float → sleep, log (caller acts)
A useful boundary
A function packages a behavior behind a name.
Its contract explains what inputs it accepts, what it returns, and what state it changes. Start with a pure calculation such as a retry delay: the same inputs produce the same output, with no network calls or hidden state. The caller can use a returned value in another calculation.
Printing only displays a value; it does not return that value to the caller. Functions without an explicit return value return None, the value Python uses for “nothing”.
def retry_delay(attempt):
if attempt < 0:
raise ValueError("attempt must be nonnegative")
return 60 if attempt >= 6 else 2 ** attempt
print(retry_delay(3))
Make assumptions inspectable
A precondition describes what must hold before the function runs.
A postcondition describes what the function guarantees afterward. Our delay function requires a nonnegative attempt count and guarantees a delay no greater than sixty. It compares the attempt with six before computing 2 ** attempt, so a huge attempt count never builds a huge number.
Decide whether invalid input should be rejected, converted, or handled specially; silent guesses tend to hide bugs. Type annotations (hints such as attempt: int) help tools and readers understand intent, but ordinary Python annotations do not enforce runtime validation. A precise function name often communicates more than a long comment.
Separate decisions from effects
Code that decides whether to retry is easier to test separately from code with side effects (changes beyond the returned value), such as sleeping, writing a file, or sending an email.
Keep the decision in a small function and perform the effect at an outer boundary. That does not mean every line needs its own function. Extract a boundary when it names a meaningful concept or isolates uncertainty.
Avoid mutable default arguments for per-call state: Python creates default argument objects once when the function is defined, which can accidentally share data across calls.
COMMON FAILURE MODE
A default such as jobs=[] shares the same list between calls. Use None and create a fresh list inside when independent state is intended.
TRY IT YOURSELF
Write eligible(status, attempts, limit) returning a Boolean. Specify its contract, then test completed jobs, zero attempts, and attempts exactly equal to the limit.
Make the call in the laboratory.
The two-question checkpoint, your notes, and your saved progress are in the laboratory. Free, with no account.
Open in the laboratory ↗CONNECT IT TO RELAY
Extract Relay's retry decision and delay calculation into functions that do not read global state.