This lesson adds parameters to make functions general, argues about what belongs in an interface and what does not, factors shared code out into polyline, states the development plan as five steps, and documents a function's contract with a docstring and its pre- and postconditions.
Subject: Python · 65 slides · code lesson
Open the interactive version of this deck
Title
Python · Chapter 4 — Case study: interface design
§4.5-4.10, pp. 32-36
Objectives
Five things, each one you can check yourself at an interpreter prompt.
Think Python, 2nd edition — Allen B. Downey §4.5-4.10, pp. 32-36 — the pages these objectives are drawn from
Warm-up
Last lesson ended with a working function. Find its limitation.
Discussion prompt
The function square(t) draws a square with sides of 100 pixels. Name two things a caller might reasonably want that this function cannot give them, and say what would have to change.
Hint: Look at the numbers written inside the body.
Answer:
A different size, and a different shape. Both are impossible because 100 and 4 and 90 are written into the body where no caller can reach them.
Each of those numbers is a decision the function made on the caller's behalf. Moving one out into a parameter hands that decision back.
That move is called generalization, and this lesson does it twice — and then asks the harder question of when NOT to do it, which is interface design.
Concept
The interface of a function is a summary of how it is used: what are the parameters, what does the function do, and what is the return value. An interface is clean if it allows the caller to do what they want without dealing with unnecessary details.
interface — A summary of how a function is used: its parameters, what it does, and what it returns.
The practical test the book offers is unusually concrete: a well-designed interface should be simple to explain, and if you have a hard time explaining one of your functions, maybe the interface could be improved.
Figure (svg): A diagram showing the three questions that make up a function's interface: what are the parameters, what does it do, what does it return
Think Python, 2nd edition — Allen B. Downey §4.5-4.10, pp. 33-33
Section
Section 1
Concept
The next step is to add a length parameter to square. Adding a parameter to a function is called generalization, because it makes the function more general: in the previous version the square is always the same size, and in this version it can be any size.
generalization — The process of making a function more widely applicable by replacing a fixed value with a parameter.
def square(t, length):
for i in range(4):
t.fd(length)
t.lt(90)
square(bob, 100)| Version | The moving line | Effect |
|---|---|---|
| before | t.fd(100) | always 100 pixels |
| after | t.fd(length) | whatever the caller says |
| the call | square(bob, 100) | supplies the value that used to be built in |
Notice how small the change is: one number becomes a name, and the header gains a parameter. The 100 has not disappeared — it has moved from the body, where nobody could reach it, to the call, where every caller chooses it.
Think Python, 2nd edition — Allen B. Downey §4.5-4.10, pp. 32-32
Picture it
Generalization does not add capability to the body. It moves a decision to the caller.
Figure (svg): Two columns showing the hard-coded version with 100 in the body and the generalized version with length in the header
The same move, applied again, turns square into polygon: the 4 and the 90 move out too, and become n and an angle computed from it.
Worked example
Two more decisions move out of the body. Watch what has to be computed rather than passed.
def polygon(t, n, length):
angle = 360 / n
for i in range(n):
t.fd(length)
t.lt(angle)
polygon(bob, 7, 70)| What changed | How | Result |
|---|---|---|
| 4 becomes n | the number of sides is now a parameter | range(n) |
| 90 becomes angle | but angle is COMPUTED, not passed | 360 / n |
| 100 is already length | generalized in the previous step | t.fd(length) |
Move the number of sides out.
Why: The 4 becomes the parameter n, used both in range(n) and in computing the angle.
Do NOT move the angle out.
Why: It is determined by n — the exterior angles of an n-sided regular polygon are 360/n degrees — so passing it as well would be redundant and would allow an inconsistent pair.
Check the example.
Why: polygon(bob, 7, 70) draws a seven-sided polygon with side length 70.
Figure (svg): A turtle path showing a seven-sided regular polygon drawn with equal sides and equal turns
A function that draws any regular polygon. Two of the three hard-coded numbers became parameters; the third is computed from one of them, because it is not an independent choice.
Verify: Call it with n = 4 and length = 100 and compare with the old square.
Why: polygon(bob, 4, 100) draws exactly what square(bob, 100) drew. A generalization must reproduce the special case it came from, and checking that is the cheapest way to confirm the new version is right.
Prediction
The arguments are in the wrong order. Reason about what happens rather than assuming an error.
Predict first
polygon takes (t, n, length). What does polygon(bob, 70, 7) do?
Correct: A 70-sided polygon with sides of length 7 — a small, nearly circular shape.
Why: Python matches arguments to parameters by position, not by plausibility, so n gets 70 and length gets 7. Both are perfectly valid numbers, so there is no error at all — just a completely different drawing. This is a semantic error in the sense of lesson 2b, and it is exactly the failure mode keyword arguments exist to prevent.
Worked example
Three numeric arguments is enough to forget the order. There is a syntax for that.
polygon(bob, 7, 70)
polygon(bob, n=7, length=70)| Style | How the arguments are matched | Note |
|---|---|---|
| positional | the order decides which is which | easy to get wrong |
| keyword | the names say which is which | order-independent and readable |
| both | identical effect | the same call |
Notice the problem.
Why: When a function has more than a few numeric arguments, it is easy to forget what they are or what order they should be in. polygon(bob, 70, 7) is a legal call that draws something very different.
Use the parameter names in the call.
Why: These are called keyword arguments because they include the parameter names as keywords — not to be confused with Python keywords like while and def.
Notice the second benefit.
Why: This syntax makes the program more readable, and it is also a reminder about how arguments and parameters work: when you call a function, the arguments are assigned to the parameters.
Figure (svg): The state of the program after each line of Worked example keyword arguments, drawn as a ladder with one rung per traced line
Both calls are identical in effect. The keyword form names each argument, which makes the call readable and removes any risk of getting the order wrong.
Verify: Swap the two keyword arguments and check the result is unchanged.
Why: polygon(bob, length=70, n=7) draws the same shape, because the names rather than the positions decide. Swapping the positional version's arguments gives a 70-sided polygon with sides of length 7 instead — a completely different picture with no error, which is exactly the mistake keyword arguments prevent.
Trap
Having seen that parameters make a function more useful, a student adds one for every value in the body.
Treat generalization as an unconditional improvement
Why: Each parameter genuinely does give the caller more control, so more must be better.
polygon(t, n, length, angle) would let a caller pass an angle inconsistent with n, producing a shape that does not close — a new way to be wrong that did not exist before.
Generalize a value only when it is an independent decision.
Ask whether the value can be computed from the others
Why: The angle can: it is 360/n. Computing it is better than accepting it, because then it cannot disagree.
Ask whether the caller would ever want to choose it
Why: If nobody would, the parameter is clutter that every caller must fill in.
This is the reasoning that the next section turns into a principle. Every parameter is a question the function asks its caller, and a good function asks as few as it can.
Faded example
One number moves out of the body. Two blanks, and they must agree.
Fill in the blanks
def triangle(t, length):
for i in range(3):
t.fd(length)
t.lt(120)
Why: The parameter is named in the header and used in the body, and the two must match each other. Note what is NOT generalized here: the 3 and the 120 stay, because this function is specifically a triangle — and if you generalized those too you would have written polygon instead. Deciding how far to generalize is deciding what the function IS.
Discrimination
A value that can be derived should be derived, not demanded.
Sort into buckets
For a function that draws a regular polygon, should each of these be a parameter?
Socratic
Parameters are not free. Work out what the price is.
Discussion prompt
Every parameter you add gives callers more control. Name three separate costs of adding one, and say who pays each.
Hint: Think about the caller, the reader, and the person maintaining the function.
Answer:
The caller pays: they must now supply a value on every call, including the ones where they had no opinion about it.
The reader pays: a call with five arguments is harder to understand than one with two, and it is easier to get the order wrong.
The maintainer pays: every parameter is a new combination of inputs that could be inconsistent or invalid, and therefore new behaviour to think about and test.
This is why the next section's question — should n be a parameter of circle? — has a real answer rather than being a matter of taste. More control is a benefit, and it has a price, and the two have to be weighed.
Section
Section 2
Concept
The next step is circle, which takes a radius as a parameter. A simple solution uses polygon to draw a 50-sided polygon — and the number 50 is where the interesting question is.
import math
def circle(t, r):
circumference = 2 * math.pi * r
n = 50
length = circumference / n
polygon(t, n, length)| Line | What it computes | Note |
|---|---|---|
| circumference | 2 pi r | the distance round the circle |
| n = 50 | a fixed number of segments | the limitation |
| length | circumference / n | so the perimeter comes out right |
| polygon(t, n, length) | draw it | an approximate circle |
One limitation of this solution is that n is a constant, which means that for very big circles the line segments are too long, and for small circles we waste time drawing very small segments. One solution would be to generalize the function by taking n as a parameter — which would give the caller more control, but the interface would be less clean.
Think Python, 2nd edition — Allen B. Downey §4.5-4.10, pp. 33-34
Picture it
Both work. The question is which one a caller should have to think about.
Figure (svg): Two columns comparing circle taking a radius alone against circle taking a radius and a segment count
The book's criterion decides it: r belongs in the interface because it specifies the circle to be drawn; n is less appropriate because it pertains to the details of how the circle should be rendered.
Worked example
Rather than fixing n or asking for it, compute it. Work out what the formula guarantees.
def circle(t, r):
circumference = 2 * math.pi * r
n = int(circumference / 3) + 3
length = circumference / n
polygon(t, n, length)| Radius | Circumference | Resulting n and segment length |
|---|---|---|
| r = 10 | circumference about 63 | n = 24, length about 2.6 |
| r = 100 | circumference about 628 | n = 212, length about 3.0 |
| r = 1 | circumference about 6.3 | n = 5, length about 1.3 |
Read what the formula targets.
Why: The number of segments is an integer near circumference/3, so the length of each segment is approximately 3 — small enough that the circles look good, but big enough to be efficient.
Check it scales.
Why: A big circle gets many segments and a small one gets few, so the segment length stays near 3 whatever the radius. That is what makes it acceptable for any size circle.
Understand the plus 3.
Why: Adding 3 to n guarantees that the polygon has at least 3 sides — otherwise a tiny radius would produce a polygon with one or two sides, which is not a shape.
Figure (svg): A ladder showing the circle function computing circumference, then n, then segment length, then calling polygon
n is computed so that each segment is about 3 pixels long, whatever the radius, with a floor of three sides. The caller supplies only the radius, and the rendering quality takes care of itself.
Verify: Check the edge case the plus 3 protects against.
Why: With r = 0.1 the circumference is about 0.63, so int(circumference / 3) is 0 — and without the plus 3, n would be zero, giving a division by zero on the next line. The guard is not decorative; it prevents a crash on small inputs.
Elimination
A function to draw a regular polygon. Three of these belong in its interface.
Eliminate the wrong options
Which of these should NOT be a parameter of polygon(t, ...)?
Survives elimination: C
Why: The angle is determined by n — it is 360/n for a regular polygon — so accepting it as a parameter adds no capability and creates a failure mode: a caller could supply an angle inconsistent with n, producing a shape that does not close. A value that can be computed from the other parameters should be computed, both to keep the interface small and to make an inconsistent call impossible.
Worked example
The reasoning generalizes. Use it on a function the book does not write.
# candidate: a function that draws a star
# which of these belong in the interface?
# t the turtle to draw with
# points how many points the star has
# size how big it is
# pen_width how thick the lines are
# step_angle the turn between segments| Candidate | Specification or rendering? | Verdict |
|---|---|---|
| t | specifies WHAT to draw with | in |
| points | specifies WHICH star | in |
| size | specifies WHICH star | in |
| pen_width | how it is rendered | arguable — probably out |
| step_angle | derivable from points | out |
Apply the criterion to each candidate.
Why: Does this specify WHICH star to draw, or HOW to render it? Specification belongs in; rendering detail usually does not.
Notice the arguable one.
Why: Pen width is a rendering detail by the criterion, but unlike n it is not something the function can sensibly choose for the caller — so it might belong in after all, or better, be set on the turtle beforehand.
Notice the clear-cut one.
Why: The step angle is derivable from the number of points, exactly as polygon's angle was, so it should be computed rather than demanded.
Figure (svg): The state of the program after each line of Worked example applying the criterion to a new case, drawn as a ladder with one rung per traced line
The turtle, the number of points and the size go in; the step angle is computed; the pen width is genuinely arguable and is a good candidate for being set on the turtle rather than passed. The criterion decides three of the five cleanly and tells you where the real judgement is needed.
Verify: Test the interface by explaining it in one sentence.
Why: Draw a star with this many points, this big, using this turtle is a sentence. The five-parameter version needs two sentences and a caveat, which is the book's own test: a well-designed interface should be simple to explain.
Trap
Faced with the choice, a student adds n as a parameter, reasoning that more control is strictly better and the caller can always ignore it.
Assume optional-in-practice is the same as optional
Why: In this version of the language it is not: every parameter must be supplied on every call.
Now every caller who does not care about segment counts still has to pick one, and picking badly makes the drawing worse. The function has exported a decision that it was better placed to make.
Ask which decisions the FUNCTION is better placed to make.
The caller knows what they want drawn
Why: A circle of radius 50. That is specification, and it belongs to them.
The function knows how to draw it well
Why: About three pixels per segment, whatever the size. That is rendering, and it belongs to the function.
Chapter 13 shows the syntax for genuinely optional parameters, which resolves some of this tension. Even then the criterion holds: a default value is a decision the function makes on your behalf, and it should make it well.
Trade off
Fill the blanks. Neither column is simply better.
Comparison matrix
| Question | circle(t, r) | circle(t, r, n) |
|---|---|---|
| How many decisions for the caller? | one | two |
| Can the caller tune the quality? | no | yes |
| Can the caller draw a bad circle by mistake? | no | yes — by choosing n badly |
| Simple to explain? | yes, in one sentence | needs a caveat about n |
The book chooses the left column, and the reason is in the last two rows: the extra control is rarely wanted and the extra way of going wrong is always present.
Prediction
The guard exists for a reason. Find the input that needs it.
def circle(t, r):
circumference = 2 * math.pi * r
n = int(circumference / 3)
length = circumference / n
polygon(t, n, length)| Radius | What n becomes | Outcome |
|---|---|---|
| r = 50 | circumference 314, n = 104 | fine |
| r = 1 | circumference 6.3, n = 2 | a 2-sided polygon |
| r = 0.2 | circumference 1.3, n = 0 | division by zero |
Predict first
With the plus 3 removed, what happens for a very small radius?
Correct: A ZeroDivisionError — for a small enough radius, int(circumference / 3) is zero, and the next line divides by n.
Why: Adding 3 to n guarantees the polygon has at least 3 sides, and as a side effect it guarantees n is never zero. This is a good example of a guard that protects two things at once: the shape stays a shape, and the arithmetic stays legal. Note that the failure is a runtime error in the sense of lesson 2b — the program parses fine and fails only for particular inputs.
Explain it to yourself
The criterion is short. Make sure you can apply it to something new.
Discussion prompt
The book's criterion is that r belongs in the interface because it specifies the circle, and n does not because it pertains to how the circle is rendered. Apply the same criterion to a function that prints a table of numbers: name one thing that specifies the table and one that is a rendering detail.
Hint: What makes it THIS table rather than another one?
Answer:
The numbers themselves specify the table — change them and it is a different table. So they belong in the interface.
The column width, the padding character, whether the border is drawn with dashes or equals signs: all rendering. Change any of them and it is the same table, presented differently.
The test that usually settles it: if you changed this value, would the caller say that is a different thing or that is the same thing, formatted differently? The first is specification and belongs in; the second is rendering and probably does not.
Section
Section 3
Concept
circle could re-use polygon, because a many-sided polygon is a good approximation of a circle. But arc is not as cooperative: you cannot use polygon or circle to draw an arc, because an arc does not close.
def arc(t, r, angle):
arc_length = 2 * math.pi * r * angle / 360
n = int(arc_length / 3) + 1
step_length = arc_length / n
step_angle = angle / n
for i in range(n):
t.fd(step_length)
t.lt(step_angle)| Part | What it does | Note |
|---|---|---|
| lines 2-5 | work out how many steps and how big | arithmetic |
| lines 6-8 | the drawing loop | identical in shape to polygon's |
| the problem | that loop is duplicated | two copies of one idea |
The second half of this function looks like polygon, but polygon cannot be re-used without changing its interface. You could generalize polygon to take an angle as a third argument — but then polygon would no longer be an appropriate name.
Think Python, 2nd edition — Allen B. Downey §4.5-4.10, pp. 34-34
Picture it
Two functions containing the same loop, and no honest way to share it under either name.
Figure (svg): Two columns showing polygon and arc, with the identical drawing loop highlighted in both
So the shared part needs its own name — and it cannot be polygon, because it no longer only draws polygons. Calling it polyline solves the naming problem and the sharing problem at once.
Worked example
Give the shared loop a name of its own, then rewrite both callers in terms of it.
def polyline(t, n, length, angle):
for i in range(n):
t.fd(length)
t.lt(angle)
def polygon(t, n, length):
angle = 360.0 / n
polyline(t, n, length, angle)| Function | What it now does | Effect |
|---|---|---|
| polyline | the shared loop, with everything as parameters | knows nothing about shapes |
| polygon | computes its angle, then delegates | 3 lines instead of 5 |
| arc | computes its values, then delegates | 5 lines instead of 8 |
Name the shared thing honestly.
Why: It draws n line segments with a given length and turn between them. That is a polyline, not a polygon — and choosing the honest name is what makes the sharing possible.
Make every varying value a parameter.
Why: polyline takes the turtle, the count, the length and the angle, because all four differ between its two callers.
Rewrite the callers.
Why: polygon computes 360.0/n and delegates; arc computes its four values and delegates. Neither contains a loop any more.
Figure (svg): The state of the program after each line of Worked example factoring out polyline, drawn as a ladder with one rung per traced line
One loop instead of two, in a function whose name describes exactly what it does. polygon and arc each become a small piece of arithmetic followed by a single call.
Verify: Check that polygon still draws what it drew before.
Why: polygon(bob, 7, 70) produces the same heptagon as the pre-refactoring version. Refactoring is supposed to change structure without changing behaviour, so a different picture would mean the move introduced a bug — which is why you check rather than assume.
Ranking
Put the moves in the order that keeps the program working at every stage.
Put in order
Why: Notice the duplication, create the shared function, redirect the callers to it, then verify nothing changed. The order matters because it keeps the program runnable throughout: after step three you have a working program, and step four confirms it. Deleting the duplicated loops before writing polyline would leave a broken program in between, with two changes to debug at once.
Worked example
Once arc exists, circle is a special case of it. Find the special case.
def arc(t, r, angle):
arc_length = 2 * math.pi * r * angle / 360
n = int(arc_length / 3) + 1
step_length = arc_length / n
step_angle = float(angle) / n
polyline(t, n, step_length, step_angle)
def circle(t, r):
arc(t, r, 360)| Observation | What follows | Result |
|---|---|---|
| a circle | is an arc of 360 degrees | one call |
| circle(t, r) | arc(t, r, 360) | the whole body |
| the chain | circle -> arc -> polyline | three levels |
Notice the relationship.
Why: An arc of 360 degrees is a complete circle. That is not a coincidence — it is what the angle parameter means.
Write circle in terms of arc.
Why: One line. Everything circle used to compute is now computed by arc, correctly, for any angle.
Look at the resulting structure.
Why: circle calls arc calls polyline. Each layer adds one idea and delegates the rest, which is what a set of functions that work together looks like.
Figure (svg): A call diagram showing circle calling arc calling polyline, with each level adding a computation
circle becomes a single line: an arc of 360 degrees. The earlier version's circumference and segment arithmetic is not lost — it lives in arc, in a more general form.
Verify: Check that circle still produces the same drawing as the earlier version.
Why: It does, and it now handles every radius through the same code path as arc. Confirming that a refactor preserved behaviour is what makes it a refactor rather than a rewrite — and it is also the moment you find out whether the generalized arithmetic really does cover the old special case.
Trap
Wanting to share the loop, a student adds an angle parameter to polygon and calls it from arc.
Solve the sharing problem by widening an existing function
Why: It avoids inventing a new name, and the code really is shared.
Now polygon(t, n, length, angle) can draw things that are not polygons at all. The book is direct about this: then polygon would no longer be an appropriate name.
If generalizing makes the name wrong, the generalized thing needs a new name.
Name the general operation for what it actually does
Why: It draws n line segments with a given turn between them: polyline.
Keep the specific function, now written in terms of the general one
Why: polygon still exists and is still called polygon, because it still draws polygons — it simply delegates.
A name that has stopped describing its function is worse than a duplicated loop, because it misleads silently. Refactoring is supposed to improve interfaces, and an interface includes the name.
Prediction
The angle parameter is a fraction of a circle, in degrees.
Predict first
What does arc(bob, 50, 90) draw?
Correct: A quarter of a circle of radius 50 — 90 degrees is a quarter of 360.
Why: arc's angle parameter determines what fraction of a circle to draw, in degrees, so that when angle is 360 it draws a complete circle. Ninety degrees is one quarter of that. Note how the arithmetic follows: arc_length is the circumference scaled by angle/360, so a quarter turn produces a quarter of the perimeter, and step_angle divides the 90 rather than the 360 among the segments.
Matching
Four functions in a chain. Each adds exactly one thing.
Match the pairs
Why: This is what a well-refactored set of functions looks like: each layer is one idea thick. The bottom layer knows only about drawing segments and nothing about circles; the top layer knows only that a circle is a full sweep and nothing about segments. Being able to state each function's contribution in one clause is a good sign that the division is in the right place.
Counterexample
The book is enthusiastic. Push back honestly.
Discussion prompt
Describe a situation where factoring out shared code would make a program WORSE, and say what distinguishes it from the polyline case.
Hint: Consider two pieces of code that look the same but mean different things.
Answer:
When two pieces of code are identical by coincidence rather than because they express the same idea. Factoring them out couples them, so a later change needed by only one of them has to be made by adding a parameter — and you end up with a function full of flags.
The polyline case is not like that: polygon and arc genuinely both draw a run of segments with a fixed turn, so the shared function expresses a real shared idea and has an honest name.
The test is whether you can name the shared thing. If the best name you can find is a list of its callers, the code is similar rather than shared, and leaving it duplicated is the better choice.
Section
Section 4
Concept
A development plan is a process for writing programs. The process used in this case study is encapsulation and generalization, and it has five steps.
This process has some drawbacks, and the book promises alternatives later, but it is useful if you do not know ahead of time how to divide the program into functions. This approach lets you design as you go along.
Think Python, 2nd edition — Allen B. Downey §4.5-4.10, pp. 35-35
Picture it
Step 4 sends you back to step 1. That is the point.
Figure (svg): A flow chart showing the five development plan steps with a loop from step 4 back to step 1 and refactoring at the end
Refactoring is last for a reason: you cannot factor out what two functions share until both functions exist and work.
Worked example
Map what you have actually done onto the plan. Every step is there.
# step 1: bare loop drawing a square (lesson 4a)
# step 2: encapsulate it as square(t) (lesson 4a)
# step 3: generalize to square(t, length) (this lesson)
# step 3: generalize to polygon(t, n, length)
# repeat: circle(t, r), then arc(t, r, angle)
# step 5: factor polyline out of polygon and arc| Plan step | The move | What came out |
|---|---|---|
| step 1 | a small program, no functions | the loop |
| step 2 | encapsulation | square(t) |
| step 3 | generalization, twice | square(t, length), polygon(t, n, length) |
| step 5 | refactoring | polyline |
Check that step 1 really happened.
Why: It did: the bare loop in lesson 4a, with no function anywhere. The plan insists on this and it is the step people are most tempted to skip.
Find the repeat.
Why: Steps 1 to 3 ran several times: once for square, once for polygon, once for circle, once for arc. Each pass produced a working function before the next began.
Notice when refactoring happened.
Why: Last, and only once arc existed. Before that there was nothing to factor — polygon's loop was not shared with anything.
Figure (svg): The state of the program after each line of Worked example the case study as five steps, drawn as a ladder with one rung per traced line
Every step of the plan appears in the case study, in order, with steps 1 to 3 repeated four times. The plan is a description of what the chapter did rather than advice added afterwards.
Verify: Ask whether the refactoring could have come earlier.
Why: It could not: polyline is only worth writing once two functions need it, and until arc existed, factoring the loop out of polygon alone would have added a layer for no benefit. The plan's ordering is not arbitrary — each step needs the previous ones to have produced something.
Ranking
Five steps. Two of them are easy to put in the wrong place.
Put in order
Why: Working code first, then encapsulate, then generalize, then refactor last. The two that people misplace are the first and the last: skipping the bare program means having nothing to check against, and refactoring early means factoring out something only one caller needs. Note that steps two and three repeat many times before you reach the fourth.
Worked example
The book says something unusual here. It is worth taking seriously.
# 'If we had planned ahead, we might have written polyline
# first and avoided refactoring, but often you don't know
# enough at the beginning of a project to design all the
# interfaces. Once you start coding, you understand the
# problem better. Sometimes refactoring is a sign that
# you have learned something.'| Claim | Content | Why it matters |
|---|---|---|
| the ideal | design the interfaces first | sometimes possible |
| the reality | you do not know enough yet | usually true |
| the conclusion | refactoring is learning, not failure | the useful part |
Notice the concession.
Why: The book admits polyline could have been written first. It is not claiming the messy path was optimal.
Notice the reason it usually is not.
Why: Often you do not know enough at the beginning of a project to design all the interfaces. Once you start coding, you understand the problem better.
Notice the reframing.
Why: Sometimes refactoring is a sign that you have learned something. That turns a rewrite from evidence of poor planning into evidence of progress.
Figure (svg): The state of the program after each line of Worked example the honest sentence about planning, drawn as a ladder with one rung per traced line
That refactoring is normal and informative rather than a symptom of having planned badly. The knowledge needed to design the right interfaces often only exists after you have built the wrong ones.
Verify: Check the claim against your own experience of the exercises.
Why: If you attempted the exercises in order, you almost certainly did not foresee polyline while writing square — because at that point there was nothing to suggest it. The claim is testable on your own work, which is the strongest kind of evidence for it.
Trap
An experienced-feeling student starts by writing polyline, then polygon, then arc, without ever running a bare loop.
Treat the finished design as the starting point
Why: The book prints the finished design, so it looks like the thing to aim at.
If the drawing then comes out wrong, there is no working version to compare against and three functions to suspect at once.
Get something on the screen before you organise anything.
Write the smallest program that draws anything at all
Why: Then you know the turtle works, the window opens, and your fd and lt calls do what you expect.
Add one layer at a time, checking the drawing after each
Why: Every step of the plan leaves you with a working program, which is what makes the next step debuggable.
The plan's first step is not there for beginners. It is there because a working program is the only reliable reference point for whether the next change broke anything.
Prediction
The plan puts it last. Work out what triggers it.
Predict first
According to the plan, what tells you it is time to refactor?
Correct: When you have similar code in several places — that is the plan's own trigger.
Why: The fifth step says: if you have similar code in several places, consider factoring it into an appropriately general function. The trigger is duplication that has actually appeared, not a length threshold and not a tidying impulse. That is also why it cannot come first: before the duplication exists, there is nothing to factor and no way to know what the shared function should look like.
Step zero
The plan is meant to be used, not admired. Run it on a fresh problem.
Discussion prompt
You are asked to write a program that prints a formatted receipt: a header, several item lines, and a total. Describe your first three moves under this development plan, and say what you would have on the screen after each.
Hint: The first move produces something ugly that works.
Answer:
First: print one hard-coded receipt with no functions at all — header, three fixed item lines, a fixed total. Ugly, specific, and on the screen.
Second: encapsulate the header into print_header(), because it is a coherent piece you can name. Run it and check the receipt looks identical.
Third: generalize the item line into print_item(name, price), because those two values are what varies between the lines. Now three lines of the original become three calls.
Notice that after every one of those moves you still have a working program that prints a receipt. That is the property the plan is designed to preserve, and it is why each step is small.
Real world
It is a general strategy for building something you do not yet fully understand.
Discussion prompt
Describe a non-programming task where you would rather build a rough working version first and reorganise it afterwards, than design the whole thing before starting. What makes it that kind of task?
Hint: What do you learn by building the rough version?
Answer:
Writing an essay is the usual example: a rough draft that covers the ground, then reorganised into sections once you can see what the argument actually is.
What makes it that kind of task is that the structure is not knowable in advance. You discover what the piece is about by writing it, exactly as you discover what functions a program needs by writing it.
The contrast case is a task whose structure is fixed and known — filling in a form, following a legal procedure. There, planning first is straightforwardly better, and the book's promise of alternatives later is a promise of methods for exactly those cases.
Section
Section 5
Concept
A docstring is a string at the beginning of a function that explains the interface. By convention, all docstrings are triple-quoted strings, also known as multiline strings because the triple quotes allow the string to span more than one line.
docstring — A string at the beginning of a function that explains its interface.
def polyline(t, n, length, angle):
"""Draws n line segments with the given length and
angle (in degrees) between them.
t is a turtle.
"""
for i in range(n):
t.fd(length)
t.lt(angle)| Part of the docstring | What it supplies | Note |
|---|---|---|
| what it does | draws n line segments | concisely, not how |
| what each parameter means | length, angle in degrees, t is a turtle | including units and types |
| what it does not say | anything about the loop | the how is not the caller's business |
It is terse, but it contains the essential information someone would need to use this function. It explains concisely what the function does without getting into the details of how, and it explains what effect each parameter has and what type each should be if that is not obvious.
Think Python, 2nd edition — Allen B. Downey §4.5-4.10, pp. 35-36
Picture it
Two parties, two sets of obligations, and a rule for deciding whose fault a bug is.
Figure (svg): Two columns showing the caller's obligations as preconditions and the function's obligations as postconditions
The value of writing them down is not ceremony. It is that when something goes wrong, the contract says which side broke it.
Worked example
Terse is the goal. Decide what earns its place.
def arc(t, r, angle):
"""Draws an arc of a circle of radius r,
sweeping through angle degrees.
t is a turtle; r is a length in pixels;
angle is in degrees, so 360 draws a full circle.
"""| Element | Content | Why it is worth including |
|---|---|---|
| what it does | draws an arc | one sentence |
| the parameters | types and units for each | especially the units |
| a worked hint | 360 draws a full circle | resolves an ambiguity |
Say what it does, not how.
Why: Drawing an arc is the caller's concern. Whether it does so with fifty segments or five hundred is not, and mentioning it would tie the docstring to an implementation that may change.
Give each parameter its type and units.
Why: The book says: explain what effect each parameter has and what type each should be, if it is not obvious. Units are the part people omit and the part that causes bugs.
Resolve the ambiguity a reader would actually have.
Why: Does angle mean the sweep or the turn per step? Saying that 360 draws a full circle answers it in five words.
Figure (svg): The state of the program after each line of Worked example what belongs in a docstring, drawn as a ladder with one rung per traced line
What the function does, what each parameter means including its units, and an answer to the one question a reader would otherwise have to guess at. Nothing about the loop, the segment count, or the arithmetic.
Verify: Apply the book's test: is the interface simple to explain?
Why: It took two sentences, with no caveats. The book says a well-designed interface should be simple to explain, and if you have a hard time explaining one of your functions, maybe the interface could be improved — so writing the docstring is itself a test of the design.
Sorting
The test is whether a caller needs it to use the function correctly.
Sort into buckets
For a docstring on arc(t, r, angle), sort each candidate.
Worked example
This is what pre- and postconditions are for. Assign blame with them.
# polyline's preconditions:
# t is a Turtle, n is an integer,
# length is a positive number, angle is a number in degrees
polyline(bob, 4.5, 100, 90) # case A
polyline(bob, 4, 100, 90) # case B, but nothing is drawn| Case | What is true | Whose bug |
|---|---|---|
| case A | n is 4.5, not an integer | precondition violated: caller's bug |
| case B | all preconditions satisfied | postcondition not met: function's bug |
| the rule | check the preconditions first | then you know where to look |
State the contract.
Why: The caller agrees to provide certain parameters and the function agrees to do certain work. polyline requires four things: t a Turtle, n an integer, length a positive number, angle a number in degrees.
Check case A against the preconditions.
Why: n is 4.5, which is not an integer. Preconditions are the responsibility of the caller — if the caller violates a properly documented precondition and the function does not work correctly, the bug is in the caller.
Check case B.
Why: Every precondition holds, and the postcondition — n segments drawn — does not. If the preconditions are satisfied and the postconditions are not, the bug is in the function.
Figure (svg): A flow chart showing a bug being diagnosed by checking preconditions first and then postconditions
Case A is the caller's bug and case B is the function's. The contract turns whose fault is this from an argument into a question you can answer by checking a list.
Verify: Ask what the caveat properly documented is doing in the rule.
Why: It is carrying real weight: a precondition nobody wrote down cannot be violated knowingly, so an undocumented requirement that a caller breaks is arguably the function author's fault. That is precisely why the docstring matters — it is what makes the contract enforceable.
Trap
A student writes: loops n times, calling fd and then lt on the turtle each time.
Describe what the code does, line by line
Why: It is accurate, and it is the easiest thing to write because the code is right there.
It fails the caller twice. It does not say what the function is FOR, and it goes out of date the moment the implementation changes — which is exactly what happened to circle when arc took over its arithmetic.
Describe the interface: what it does, what to supply, what comes back.
Say what a caller gets, in their terms
Why: Draws n line segments with the given length and angle between them — no mention of the loop.
Give the types and units of each parameter
Why: Especially the units. Degrees or radians is the single most common unstated assumption in this chapter.
A docstring written this way survives refactoring. polyline's would be unchanged if its body were rewritten completely, because it describes the contract rather than the code.
Prediction
Apply the contract rule.
Predict first
polyline's docstring says n must be an integer. A caller passes 4.5 and gets a TypeError from range(). Whose bug is it?
Correct: The caller's — preconditions are the responsibility of the caller, and this one was properly documented.
Why: The rule is explicit: if the caller violates a properly documented precondition and the function does not work correctly, the bug is in the caller, not the function. The word properly is doing real work — had the requirement not been written down, the caller could not have known, and the blame would shift. This is why writing the contract down is a practical act rather than a formality.
Faded example
One sentence saying what the function does, in the caller's terms.
Fill in the blanks
def polygon(t, n, length):
"""Draws a regular polygon with n sides of the given length.
t is a turtle; n is the number of sides;
length is the length of each side in pixels.
"""
Why: The first line says what the caller gets, in words that mention no implementation. It does not say loops n times or calls polyline, because neither is the caller's concern and both could change. Note that the parameter descriptions below carry the types and units — pixels for length, and the fact that t is a turtle — which is the part the book says to include when it is not obvious.
Explain it
The most common objection, and it has a good answer.
Discussion prompt
A classmate says docstrings are pointless for a program nobody else will read. Give them two reasons to write one anyway — and make one of them about the design rather than about documentation.
Hint: What happens while you are writing the docstring?
Answer:
The first reason is that you are the other reader. In three weeks you will not remember whether angle was degrees or radians, and the docstring is faster than re-deriving it from the body.
The second reason is about design, and it is the stronger one: writing the docstring TESTS the interface. The book says a well-designed interface should be simple to explain, and if you have a hard time explaining one of your functions, maybe the interface could be improved.
So a docstring you struggle to write is telling you something before anybody has read it. That makes it a design tool that happens to leave documentation behind, rather than paperwork.
Comparison
Fill the blanks. Each move has a trigger, and knowing the trigger is what makes the move usable.
Comparison matrix
| Move | What it does | What triggers it |
|---|---|---|
| encapsulation | wraps working code in a named function | a coherent piece you can name |
| generalization | replaces a fixed value with a parameter | a value the caller should choose |
| interface design | decides what belongs in the parameter list | a value that is a rendering detail rather than a specification |
| refactoring | factors shared code into a more general function | similar code in several places |
The third row has no equivalent in the first two: it is the only move that can result in doing nothing, and deciding to leave n out is as much a design act as putting r in.
Pattern
Six questions, in order. The last one is the test.
Step 6 is the book's own test, and it is worth taking literally. A docstring that is hard to write is evidence about the function, not about your writing.
Python documentation — turtle — Turtle graphics turtle — Turtle graphics
Check
One number moves out of the body. Decide which parameter it becomes.
def polygon(t, n, length):
angle = 360 / n
for i in range(n):
t.fd(length)
t.lt(angle)| Value | Where it comes from | Note |
|---|---|---|
| n | a parameter | the caller chooses |
| length | a parameter | the caller chooses |
| angle | computed from n | not a parameter |
Check your understanding
Why is angle computed rather than passed as a parameter?
Answer: B
Why: For a regular polygon the exterior angle is 360/n, so the angle is not an independent decision. Accepting it as a parameter would add no capability and would create a new way to be wrong: a caller could pass n = 5 with an angle of 90, producing a shape that never closes. A value derivable from the other parameters should be derived.
Check
The criterion is specification versus rendering.
Check your understanding
By the book's criterion, why does n not belong in circle's interface while r does?
Answer: B
Why: r belongs in the interface because it specifies the circle to be drawn; n is less appropriate because it pertains to the details of how the circle should be rendered. Change r and it is a different circle; change n and it is the same circle, drawn more or less finely — which is exactly the distinction the criterion turns on.
Check
Preconditions belong to the caller; postconditions belong to the function.
Check your understanding
A function's preconditions are all satisfied and its postconditions are not. Where is the bug?
Answer: B
Why: The rule is stated directly: if the preconditions are satisfied and the postconditions are not, the bug is in the function. The caller kept its side of the contract — it supplied everything of the right kind — so whatever went wrong happened inside. This is the payoff of writing the contract down: the question has an answer instead of being a matter of opinion.
Real world
Interfaces and contracts are how any service is described, not just a function.
Discussion prompt
Think of a service you use where somebody documents what they need from you and what you will get back — a form, an order, a request at work. Identify its preconditions and its postconditions, and describe a case where it was unclear whose fault a failure was.
Hint: The unclear cases are always undocumented preconditions.
Answer:
A print shop is a clean example. Preconditions: a file in a stated format, at a stated resolution, by a stated deadline. Postconditions: printed copies, this many, by that date.
The disputes are always about undocumented preconditions. If nobody said the file needed particular margins and the print comes out wrong, the blame is genuinely unclear — which is the book's point about properly documented.
This is why writing the contract down is not bureaucracy. It converts an argument about intentions into a check against a list, and it does so before anything goes wrong, which is the only time it is cheap.
Commit first
Answer, then rate your confidence. This one is about a real design judgement.
Predict first
You are writing a function to draw a rectangle. Which of these should NOT be a parameter?
Correct: The number of sides — a rectangle always has four, so it is not a decision anybody makes.
Why: Width, height and the turtle are all genuine caller decisions and none is derivable from the others. The number of sides is fixed by what a rectangle IS: accepting it as a parameter would let a caller ask for a five-sided rectangle, which is not a capability but a contradiction. This is the same reasoning that kept angle out of polygon's interface, applied to a value that is fixed rather than derived — and it shows that the question is this an independent decision? covers both cases.
Explain it
The n-in-circle question is a real design argument, and being able to argue both sides is the skill.
Discussion prompt
A classmate argues that circle should take n as a parameter, because more control is always better and the caller can pass 50 if they do not care. Give the strongest version of their case, then say why the book decides against it.
Hint: Their case is not silly. State it properly before answering it.
Answer:
Their case: a caller drawing a huge circle might genuinely want more segments than the formula chooses, and a caller drawing thousands of tiny circles might want fewer for speed. Both are real needs the fixed formula cannot serve.
The book's answer: those needs are rare, and the cost is paid by every caller on every call. Rather than clutter up the interface, it is better to choose an appropriate value of n depending on circumference — which serves the common case well and the rare case adequately.
The honest resolution is that this is a trade, not a theorem, and chapter 13's optional parameters largely dissolve it: n can have a sensible default that most callers never see and an unusual caller can override. Being able to say that is better than winning the argument.
Exit ticket
One honest answer. It decides what the next lesson opens with.
Predict first
Which of these is still least solid for you?
Correct: Whichever you picked is the right answer — this one is for you, not for a mark.
Why: These are ordered roughly by how long they take to learn. Generalization is a mechanical move you will make hundreds of times. Interface design is a judgement that improves for years, and the n-in-circle argument is worth revisiting whenever you write a function with more than two parameters. Refactoring depends on recognising that two pieces of code express the same idea, which is genuinely hard and gets easier with exposure. And the contract framing is the one that pays off immediately in debugging: knowing whether to look inside a function or at its caller halves the search.
Connect it up
One page, from memory.
Draw it
Draw the four functions of the finished case study — polyline, polygon, arc, circle — as boxes, with arrows showing which calls which. Label each arrow with the ONE thing the calling function adds before delegating. Then, beside the diagram, write the five steps of the development plan and mark which step produced each function. Finally, pick any one of the four and write its docstring from memory, including the units of every parameter.
Recap
Four pages, and the second half of a process you can apply to any program.
| If you remember one thing | It is this |
|---|---|
| From generalization | A value the other parameters determine should be computed, not passed. |
| From interface design | Specification belongs to the caller; rendering belongs to the function. |
| From refactoring | If generalizing makes the name wrong, the general thing needs a new name. |
| From the plan | Get something working first. Refactoring last, when there is something to factor. |
| From the contract | Preconditions violated: the caller's bug. Postconditions unmet: the function's. |
Chapter 5 returns to the language itself, with the two constructs that let a program choose between alternatives and repeat itself by calling itself: conditional execution and recursion.
Think Python, 2nd edition — Allen B. Downey §4.5-4.10, pp. 32-36 — everything on these slides traces back here
Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.