The split
def compute(n):
total = 0
for i in range(1, n + 1):
if i % 2 == 0:
total += i * i
else:
total -= i
return total
def run():
n = int(input("Enter a positive integer: "))
result = compute(n)
print(f"Result for {n}: {result}")
compute(4) → 2² + 4² − 1 − 3 = 4 + 16 − 1 − 3 = 16.
compute is pure: same input, same output, always, no matter how many times you call it. run is
the only place that touches I/O.
Why the split matters
Testing. The original run is nearly untestable without mocking input and capturing stdout.
After the split:
assert compute(4) == 16
assert compute(1) == -1
assert compute(0) == 0
Three lines. No mocking, no test harness magic. The pure core is fully exercised.
Reuse. Suppose you later want a web endpoint, a batch pipeline, or a REPL that calls the same
logic. You call compute(n) directly. You don’t have to unpick the I/O from the math.
Parallelism. Because compute is referentially transparent, you can run a thousand calls
concurrently — compute(1), compute(2), …, compute(1000) — and they share no state. No
locks, no coordination. A sequential I/O loop on the outside is totally fine; the expensive math
goes wide.
The general principle
Real programs have a shape:
[Input / I/O] ──► [Pure core] ──► [Output / I/O]
Input at the left edge parses the outside world into plain values (integers, strings, records). The pure core transforms them — all the interesting logic lives here. Output at the right edge writes results back to the world (print, file, network).
The more of your program that lives in the pure core, the more of it you can reason about, test, and parallelize without ceremony. Keep the edges thin.
What “thin shell” looks like in practice
In larger systems the shell might:
- Parse command-line arguments into a config struct
- Read a file and decode it into domain types
- Call the pure core with those types
- Serialize the output and write it back
The shell does no logic beyond translation. That discipline is what makes large functional codebases tractable. In Scala or Haskell you’d see this called “IO at the boundary” — the type system enforces that effectful code lives separately. Python doesn’t enforce it, but the discipline still pays.
Next: enough foundations — back to recursion. We’ll use the contains search and then build
map-by-hand (applying a function to each element of a list), the move that eventually becomes one
of the most powerful tools in Stage 2 (Lesson 9).