1. Values, control flow, and function contracts
Turn a specification into a small program whose behavior you can trace.
FREE COMPUTER SCIENCE STACK LEVEL · 2 OF 20
How do you turn a problem into a correct, testable program?
How do you turn a problem into a correct, testable program?
Build programs you can reason about. This layer connects Functions, Recursion, Data abstraction, Mutation, Testing to the rest of the computing stack.
Learn to design data and functions together, trace execution, control mutation, and test contracts. Build small complete programs before studying how their implementations reach the machine.
How do you turn a problem into a correct, testable program?
This section is a sequence of worked studies. Each study asks you to reconstruct an example, solve a new problem, and check the assumptions behind your answer. Keep your derivations and code so you can revisit them without looking at the solution.
Turn a specification into a small program whose behavior you can trace. Trace names, expressions, branches, and loop state. Write a function with an explicit input contract. Choose edge cases from the contract rather than guessing.
Follow recursive calls as separate computations with unfinished work. Trace separate local environments for recursive calls. Identify base cases and a decreasing measure. Compare recursion with an explicit work stack.
Predict when a change is visible through more than one name. Distinguish rebinding from mutation. Trace shared references and shallow copies. Specify whether a function may mutate its inputs.
Separate the promises of a data structure from the objects used to implement it. Specify operations independently of representation. State a representation invariant and abstraction function. Compare implementations using both semantics and cost.
Pass behavior as data and reason about closures without losing evaluation order. Write functions that accept or return functions. Trace a closure and its captured environment. Distinguish pure transformations from effects.
Use objects to protect state and make interfaces predictable. Define an object invariant and preserve it across methods. Separate class-level behavior from per-instance state. Recognize when inheritance violates a caller contract.
Handle expected failures while preserving useful errors and releasing resources. Distinguish failure categories and handle them at appropriate boundaries. Use context management to release resources. Separate parsing, validation, and side effects.
Use failures to locate causes and build evidence for a stated contract. Reduce a failure to a small reproducible case. Choose tests from boundaries and invariants. Separate a test oracle from the implementation being tested.
Arithmetic, comparisons, and the idea of an input and output. Each lesson names any additional prerequisites. Revisit the mathematics and programming sections whenever a calculation or execution trace becomes a barrier.
Implement a small command-line program with a documented data model, explicit contracts, a test suite, and a failure-handling policy. Explain one defect you found by tracing state. A correct multiple-choice answer is only one kind of evidence. Preserve the intermediate steps, test an edge case, and explain why an attractive alternative fails.
The primary readings provide fuller treatments and additional exercises. Work the section project after the guided problems, then return to a problem a few days later without notes. Use a failed attempt to identify the specific definition, assumption, or step that needs more practice.
Turn a specification into a small program whose behavior you can trace.
Follow recursive calls as separate computations with unfinished work.
Predict when a change is visible through more than one name.
Separate the promises of a data structure from the objects used to implement it.
Pass behavior as data and reason about closures without losing evaluation order.
Use objects to protect state and make interfaces predictable.
Handle expected failures while preserving useful errors and releasing resources.
Use failures to locate causes and build evidence for a stated contract.
About 35 minutes. Functions, lists, and exceptions.
A stack has capacity 2. push(x) adds x unless full; pop() removes and returns the most recent item unless empty. Failed operations leave the stack unchanged.
Record the list and return value after every operation.
States are [A], [A,B], [A,B], [A], [], []. The third push fails without mutation; the pops return B, A, then an empty-stack error. Keep the list private and expose only operations preserving length between zero and capacity. Test the state after an error, not only that an error occurred.
How would the contract change if push silently discarded the oldest item?
These problems require a contract, executable reasoning, and a failing case. Run code locally after predicting the result. Preserve the counterexamples that expose a mistaken design.
1. Trace and property testing · allow 35 minutes
Define encode(xs) to replace each maximal run of equal adjacent values with a pair (value,count). For [2,2,3,2,2], the answer is [(2,2),(3,1),(2,2)]. Write a one-pass implementation with no mutation of xs. State empty-input behavior and an invariant for the current run.
Propose three properties for testing it, including one that catches merging equal values across a different value. Then compare map(f,filter(p,xs)) with filter(p,map(f,xs)) for f(x)=x+1, p(x)=“x is even,” xs=[1,2,3]. Explain why a pipeline rewrite must specify which values its predicate sees.
Keep one unfinished run and a list of finished runs. For testing, write a decoder independently and check both reconstruction and maximality.
Start with an empty output. For each x, append (x,1) if the output is empty or the last value differs; otherwise replace the final pair by (x,lastCount+1). Before each iteration, decoding the output yields exactly the processed prefix, each count is positive, and adjacent output values differ. This invariant establishes the postcondition at the end; the empty list encodes as [].
Useful properties are decode(encode(xs))=xs, the sum of counts equals len(xs), and adjacent encoded values differ. Reconstruction on [2,3,2] detects an implementation that incorrectly groups all equal values, while maximality detects [(2,1),(2,1)] for input [2,2]. Neither total counts nor membership alone is sufficient.
The first pipeline filters to [2], then maps to [3]. The second maps to [2,3,4], then filters to [2,4]. The expressions are not equivalent. A valid transformation can use a predicate composed with f when the original specification calls for testing transformed values; side effects, exceptions, and evaluation order also need explicit treatment.
2. Debug and prove · allow 45 minutes
A sorted array is searched over the half-open interval [lo,hi). A buggy routine chooses m=(lo+hi)//2, returns m on equality, recursively searches [lo,m) when target<a[m], and searches [m,hi) otherwise.
Find the smallest nonempty failing input, show its repeated calls, and repair the right branch. State a precondition and a postcondition that allow duplicate values. Prove termination with a measure and prove that an absent target is reported correctly. For an array of length 8 with a target greater than all elements, trace every interval in the corrected algorithm.
The right branch must exclude the midpoint already shown too small. Use interval length as the measure.
For a=[1], target=2, the buggy call [0,1) chooses m=0 and repeats [0,1) forever. The corrected right branch is [m+1,hi). Return an absent sentinel when lo==hi before reading a[m]. Require a nondecreasing array and 0≤lo≤hi≤len(a). The result is either an index in the original interval whose value equals target or an absent sentinel exactly when none exists there. No promise of the first duplicate is made.
The measure hi−lo is a nonnegative integer and strictly decreases on both recursive branches. Sorted order excludes m and everything to its left when a[m]<target, and excludes m and everything to its right when target<a[m]. Thus the remaining interval contains every possible match from the original interval. An empty interval has no match, proving the absent case.
For length 8 with target above all values, the intervals are [0,8),[5,8),[7,8),[8,8). Midpoints are 4,6,7; the final call performs no array access. A first-occurrence contract needs different logic and cannot be inferred from this proof.
3. Heap trace · allow 35 minutes
Predict this Python program without running it:
row = [0, 0]
a = [row, row]
b = a[:]
b[0][1] = 7
b.append([9, 9])
Draw the list objects and references after every assignment. Give the final values of row, a, and b. Implement a constructor for an independent 3-by-2 zero matrix. Explain whether [r[:] for r in a] is sufficient when cells themselves contain mutable lists. Specify the depth of independence your API promises.
Copying an outer list copies references to its elements. The two occurrences of row are two references to one object.
row becomes [0,7]. Both elements of a refer to that same row, so a displays [[0,7],[0,7]]. b is a distinct outer list but its first two elements also refer to row; appending changes only b, which displays [[0,7],[0,7],[9,9]]. There are three list objects before the append: row, a, and b. The literal appended list creates a fourth.
Use [[0 for _ in range(2)] for _ in range(3)] to create three independent row lists. Integers are immutable, so sharing references to the integer 0 does not create a mutation problem. [[0,0]]*3 would reuse one row list.
Copying each row with r[:] is sufficient for independent row structure when cells are immutable. If a cell contains another mutable list, both copies still reference that nested list. A deep-copy policy, immutable cell values, or explicit ownership rule must be chosen. The correct answer depends on the declared data model; indiscriminate copying can also break intentional shared identity.
4. ADT design and counterexample · allow 45 minutes
Specify a FIFO queue with enqueue(x), dequeue(), and size(), including empty behavior. Implement it using two lists, incoming and outgoing: enqueue appends to incoming; dequeue pops outgoing, transferring all incoming elements by popping and appending when outgoing is empty.
Trace enqueue(A),enqueue(B),dequeue(),enqueue(C),dequeue(),dequeue(). State the abstract sequence represented by the two lists. Prove the transfer preserves that sequence. A proposed subclass silently drops the oldest element when full. Explain which base contract it violates, and design an explicit bounded-queue API instead.
The logical front is at the end of outgoing. Write the abstract queue as reversed(outgoing) followed by incoming.
The queue contract is: enqueue(x) appends x to the abstract sequence; dequeue() removes and returns its first element, or raises an EmptyQueue error without mutation when the queue is empty; size() returns the number of stored elements without mutation. With two lists, size() is len(incoming)+len(outgoing).
The abstract sequence is reverse(outgoing)+incoming. After enqueuing A,B, incoming=[A,B],outgoing=[]. The first dequeue transfers to outgoing=[B,A], then returns A, leaving [B]. Enqueue C gives incoming=[C],outgoing=[B]. The next dequeue returns B without transferring; the last transfers and returns C.
When outgoing is empty, repeatedly popping incoming reverses its order in outgoing. Reversing outgoing in the abstraction function restores the original incoming order, so transfer changes representation without changing the abstract queue. Every element crosses lists at most once before removal, giving constant amortized operations even though one dequeue may perform a linear transfer.
If enqueue promises to retain every accepted element until dequeued, silently discarding the oldest violates that postcondition. A bounded API can return Full without mutation, block under a stated policy, or explicitly advertise drop-oldest semantics with a returned eviction. Each is a different contract. Inheritance does not by itself establish substitutability.
5. Implementation and failure analysis · allow 45 minutes
A local command reads UTF-8 lines of nonnegative integers and writes their sum. The output must remain unchanged if any input line is malformed. Empty input is valid and produces 0. Whitespace-only lines are malformed. Design the stages and resource lifetimes.
Write tests for a malformed middle line, empty input, a very large integer, and an output-write failure. Explain why writing the running sum directly to the destination is unsafe. Distinguish an atomic rename within one filesystem from a guarantee that a power failure preserves the latest result. Do not claim crash durability without an explicit operating-system contract.
Validation and publication are separate stages. A temporary output can delay replacing the destination until the new result is complete.
Open the input with a context manager, parse each stripped line with an explicit nonempty check, reject negative values, and accumulate using a type whose range satisfies the contract. Python integers support the very-large-integer case within resource limits. Only after successful parsing create a temporary file beside the destination, write the final value and close it, then replace the destination using a same-filesystem operation with documented semantics. On failure, preserve the original destination where the API permits and remove the temporary file.
Use a preexisting sentinel output in the malformed-middle-line test and assert it is unchanged. Empty input must produce 0. Inject a write exception instead of depending on a machine-specific permission accident. Assert resources close and temporary files are handled. Directly overwriting the destination before validation loses the old result even if parsing later fails.
Atomic replacement concerns which name-to-file state other observers can see. Durability across power loss additionally depends on flush operations, directory persistence, filesystem behavior, and hardware. A classroom implementation should state which guarantee it tests; successful close and rename alone are not a universal durability proof.
5 to 7 hours. Build a command-line study planner using Python’s standard library. Inputs are commands to add a task, list tasks, mark one complete, reopen it, and export a summary. Each task has a stable identifier, title, subject, optional due date in a declared format, and completion status. Store data as versioned JSON in a caller-specified local file. Outputs are predictable command responses and a summary grouped by subject. Separate parsing, domain operations, and persistence so the same domain functions can be tested without a terminal or filesystem. Reject malformed commands and invalid dates before mutating state. Write through a temporary file and replace the destination only after a complete valid serialization, while describing the limits of crash durability. Provide a documented policy for missing files, duplicate identifiers, unknown tasks, and corrupted storage. This project should demonstrate function contracts, collection choice, mutation discipline, resource cleanup, exceptions, and testing together. A graphical interface is optional; the core acceptance criteria concern correct behavior and recoverable user data. Treat due dates as calendar dates, not implied midnight timestamps, and state how undated tasks sort. Demonstrate that changing display order does not change task identity or completion state.
Add a schema migration from an older storage version, preserving identifiers and producing a report of every transformed field.