Subroutine Contracts in Program Design
A precondition is a condition that must be true when a subroutine is called if it is to work correctly, and it is an obligation of the caller.
Try it: The math Module: Logs, Roots and Angles
What math.log(x), math.log10(x), math.log(x, base), math.sqrt(x) and math.sin / cos / tan really compute: which logarithm base each call uses, why 0 and negative inputs must be checked before the call (ValueError: math domain error), why trigonometric functions need radians, and that math.pi and every result are binary64 approximations (math.log(1000, 10) prints 2.9999999999999996).
How it works
- math.log(x) is the natural logarithm (base e); math.log10(x) is base 10; math.log(x, base) computes log(x) / log(base) with two rounded natural logs.
- Logarithms need x > 0 and math.sqrt needs x >= 0; otherwise the call raises ValueError: math domain error, so a guard checks the input first.
- math.sin, math.cos and math.tan read their argument as radians: convert degrees with degrees / 360.0 * 2 * math.pi.
- math.pi is a float value (no parentheses) that approximates pi to about 16 digits, so sin(180 degrees) is 1.2246467991473532e-16, not 0.
- Every result is the nearest binary64 value; print shows the shortest repr that reads back as the same float, while the stored value has more digits.
Default run (6 steps): Ready: the program calls math.log10(x). … print(result) shows 3.0.
Simplified: One-call programs over int / float literals you type (|x| up to 1e15). The results come from the browser's IEEE-754 math library: math.sqrt and the radians arithmetic are identical to CPython; log / sin / cos / tan match CPython 3.12 (Windows) exactly for about 91% of the tested inputs and are within 2 units in the last place otherwise, because C math libraries differ in the last bit. Error messages are CPython 3.12's (3.14 says 'expected a positive input').
Loading the simulation…
Why Trusting a Subroutine Blindly Falls Short
When a program is built out of subroutines, each subroutine becomes a piece you rely on without re-reading its internals every time you use it. That reliance raises a practical question: what exactly does a subroutine need from the code that calls it, and what does it promise in return? A tempting first answer is simple. If I call a subroutine, it should just do its job, so I can hand it whatever values I have and trust the result.
This answer is incomplete. A subroutine is written with certain circumstances in mind. When a caller steps outside those circumstances, the subroutine has no obligation to behave well, and the reason is not that the subroutine is poorly written. The caller has not held up its side of the arrangement. To see the gap clearly, we need to describe the agreement between a subroutine and its caller using two kinds of conditions: one that the caller must make true before the call, and one that the subroutine guarantees afterward.
Tracing a Valid Call to Math.sqrt
Before defining any terms, follow one call through from start to finish. The caller has a value it wants to take the square root of, and it passes that value as the parameter x. The state of the program is what matters at each step, so we will track it explicitly.
A valid call: square root of 9
The caller wants the square root of 9 and calls Math.sqrt(x) with x set to 9. Track the state before the call, during the call, and after the call.
- 1Before the call
The caller has supplied the parameter x = 9. This value is greater than or equal to zero, so the precondition that x is greater than or equal to zero is satisfied at the moment of the call.
- 2During the call
Control passes from the caller to
Math.sqrt. The subroutine relies on the precondition the caller has established and computes the square root of 9. - 3After the call
The return value is 3. The postcondition says the square of the returned value equals the parameter that was supplied, so we check that 3 multiplied by 3 equals 9. It does, so the postcondition holds.
The call returns 3, and the caller can rely on the postcondition because the precondition was satisfied before the call.
Preconditions: The Caller's Obligation
A precondition is a condition that must be true when a subroutine is called if it is to work correctly, and it is an obligation of the caller.
The word that matters most in that definition is when. A precondition is judged at the moment of the call, not at some later point. It describes the state the caller must establish before handing control to the subroutine. Because the caller is the one who arranges that state, a precondition is an obligation of the caller rather than of the subroutine.
The source gives a concrete illustration. For the built-in function Math.sqrt(x), the precondition is that the parameter x is greater than or equal to zero. This requirement is not an arbitrary rule imposed by the library. It follows from what the subroutine is trying to compute, because it is not possible to take the square root of a negative number.
What happens when a precondition is violated? The source states plainly that if you call a subroutine without meeting its precondition, there is no reason to expect it to work properly. The program might crash or give incorrect results. The point is about accountability. In that situation the fault lies with the caller, because the caller did not live up to its side of the arrangement.
The sequence above reads from top to bottom. The caller's obligation ends at the moment of the call: by then the precondition must already be true. Only after the subroutine runs does the subroutine's obligation, the postcondition, come into play.
Postconditions: The Subroutine's Promise
A postcondition is a condition that will be true after the subroutine has run, assuming its preconditions were met and there are no bugs, and it is an obligation of the subroutine.
Where the precondition is about what the caller must provide, the postcondition is about what the subroutine delivers. Once the subroutine has run under the agreed circumstances, the postcondition describes a result the caller can rely on.
Notice the two qualifications packed into the definition. The postcondition is only guaranteed if the precondition was met, and it assumes the subroutine itself contains no bugs. These qualifications are not decoration. They make explicit that a postcondition is a promise made under conditions, not an unconditional fact about the function.
Reading the Built-in Call Math.sqrt(x)
The contract of a built-in subroutine can be located in its call. Each part of the call corresponds to one side of the agreement. The name identifies the subroutine, the parameter carries the input that the precondition constrains, and the returned value carries the output that the postcondition describes.
Putting the Contract Together
Taken together, the precondition and the postcondition describe the contract of a subroutine. The interaction between a subroutine and the rest of the program is specified by that contract, and a convenient way to express the contract is in terms of preconditions and postconditions. The two conditions attach to the two parties. The caller is responsible for the precondition, which must hold at the moment of the call. The subroutine is responsible for the postcondition, which describes its result afterward.
The arrow of dependency runs from the precondition to the postcondition. The postcondition is only a dependable promise when the precondition has been satisfied. That dependency is what makes the contract a contract rather than two independent statements.
Comparing a Satisfied and a Violated Precondition
Apply the same reasoning to a second call, Math.sqrt(-4). The precondition fails, because -4 is not greater than or equal to zero. The postcondition is no longer something the caller can rely on, because the source makes its validity conditional on the precondition being met. The subroutine has not failed the contract. The caller has.
| Question | Precondition | Postcondition |
|---|---|---|
| When is it judged? | At the moment of the call | After the subroutine has run |
| Who is responsible? | The caller | The subroutine |
| What does it describe? | The state the caller must establish | The result the caller can rely on |
| What does it depend on? | Nothing else; it is the starting condition | That the precondition was met and there are no bugs |
The two kinds of conditions, separated by the questions a programmer should ask about each
Mistakes in Reading a Contract
Common mistake Assuming a subroutine works correctly no matter what it is given
What it looks likePassing Math.sqrt(-4) and expecting a meaningful square root
Why it failsA subroutine is written with certain circumstances in mind. Outside them it has no obligation to behave well.
Do this insteadCheck the precondition at the moment of the call and make sure the caller has established it.
Common mistake Blaming the subroutine when a precondition fails
What it looks likeConcluding that Math.sqrt is broken after it gives an unreliable result for a negative input
Why it failsThe source places the fault with the caller, because the caller did not meet the precondition.
Do this insteadTrace the input back to the caller and ask whether the required condition was true before the call.
Common mistake Treating the postcondition as unconditional
What it looks likeRelying on the squared result equaling the parameter for every input
Why it failsThe postcondition is only guaranteed when the precondition was met and the subroutine has no bugs.
Do this insteadAlways pair a postcondition with the precondition that makes it valid.
Common mistake Confusing the two kinds of conditions because both describe the same call
Why it failsBoth describe one call, but from different positions: one before the subroutine runs, one after.
Practice: Reading a Contract
A caller wants the square root of a value stored in a variable and calls Math.sqrt(x). State the precondition that the caller must establish before the call, identify the postcondition the subroutine promises, and say which of the two is the caller's obligation and which is the subroutine's. Then decide what you should check first if the returned result looks wrong.
Hints
- Ask which condition is judged at the moment of the call.
- The source ties the square root's validity to whether x is greater than or equal to zero.
- If the precondition fails, consider whether the fault lies with the subroutine or with the caller.
Key Takeaways
- A precondition is a condition that must be true when a subroutine is called, and it is an obligation of the caller.
- A postcondition is a condition that will be true after the subroutine has run, assuming its precondition was met and there are no bugs, and it is an obligation of the subroutine.
- Preconditions and postconditions together express the contract that specifies how a subroutine interacts with the rest of the program.
- For
Math.sqrt(x), the precondition is that x is greater than or equal to zero, and the postcondition is that the square of the returned value equals x, which holds only when the precondition is met.