4b Generalization, Interface Design, Refactoring, and Docstrings

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

What this lesson covers

The lesson, slide by slide

1. Lesson 4b Generalization, Interface Design, Refactoring, and Docstrings

Title

Python · Chapter 4 — Case study: interface design

§4.5-4.10, pp. 32-36

2. By the end of this lesson you can

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

3. Before we start: what is wrong with square(t)?

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.

4. The one idea behind this lesson: a good function is easy to explain

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

If you can answer all three in a sentence each, the interface is probably clean.

Think Python, 2nd edition — Allen B. Downey §4.5-4.10, pp. 33-33

5. Generalization: adding a parameter

Section

Section 1

6. Making a function more general by making it know less

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)
VersionThe moving lineEffect
beforet.fd(100)always 100 pixels
aftert.fd(length)whatever the caller says
the callsquare(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

7. Picture it: a number moving out of the body

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 number moved from the left column's body to the right column's call.

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.

8. Worked example: generalizing square into polygon

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 changedHowResult
4 becomes nthe number of sides is now a parameterrange(n)
90 becomes anglebut angle is COMPUTED, not passed360 / n
100 is already lengthgeneralized in the previous stept.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

polygon(bob, 7, 60). The turn is 360/7, about 51.4 degrees.

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.

9. Predict: what does polygon(bob, 70, 7) draw?

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?

  • An error, because the arguments are the wrong way round
  • A 70-sided polygon with sides of length 7 — a tiny circle-ish blob
  • A 7-sided polygon with sides of length 70, since Python sorts it out
  • Nothing is drawn

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.

10. Worked example: keyword arguments

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)
StyleHow the arguments are matchedNote
positionalthe order decides which is whicheasy to get wrong
keywordthe names say which is whichorder-independent and readable
bothidentical effectthe 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

The whole run at once: each drop is one line of the program.

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.

11. Trap: generalizing everything you can

Trap

The 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.

The fix

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.

12. Complete it: generalize the side length

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.

13. Discriminate: parameter or computed value?

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?

a parameter: an independent decision
the turtle to draw with; the number of sides; the length of each side; the colour of the pen
computed from the others
the turn angle at each corner; the number of times to repeat the loop
param
Each of these is something a caller genuinely chooses and nothing else determines. Which turtle, how many sides, how big, what colour — none can be worked out from the others.
derived
The angle is 360 divided by the number of sides, and the number of loop passes IS the number of sides. Accepting either as a parameter would let a caller supply a value that contradicts n, which creates a failure mode rather than a capability.

14. Think it through: what does a parameter cost?

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.

15. Interface design: what to leave out

Section

Section 2

16. The argument about n

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)
LineWhat it computesNote
circumference2 pi rthe distance round the circle
n = 50a fixed number of segmentsthe limitation
lengthcircumference / nso the perimeter comes out right
polygon(t, n, length)draw itan 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

17. Picture it: the two candidate interfaces

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

r specifies the circle. n is about how it is rendered.

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.

18. Worked example: choosing n from the circumference

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)
RadiusCircumferenceResulting n and segment length
r = 10circumference about 63n = 24, length about 2.6
r = 100circumference about 628n = 212, length about 3.0
r = 1circumference about 6.3n = 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

One argument in, four values computed, one call out.

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.

19. Eliminate: which parameter does not belong?

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, ...)?

  • A. n, the number of sides
  • B. length, the length of each side
  • C. angle, the turn at each corner
  • D. t, the turtle

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.

20. Worked example: applying the criterion to a new case

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
CandidateSpecification or rendering?Verdict
tspecifies WHAT to draw within
pointsspecifies WHICH starin
sizespecifies WHICH starin
pen_widthhow it is renderedarguable — probably out
step_anglederivable from pointsout

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 whole run at once: each drop is one line of the program.

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.

21. Trap: mistaking control for quality

Trap

The 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.

The fix

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.

22. Trade-off: what each interface buys and costs

Trade off

Fill the blanks. Neither column is simply better.

Comparison matrix

Questioncircle(t, r)circle(t, r, n)
How many decisions for the caller?onetwo
Can the caller tune the quality?noyes
Can the caller draw a bad circle by mistake?noyes — by choosing n badly
Simple to explain?yes, in one sentenceneeds 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.

23. Predict: what happens without the plus 3?

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)
RadiusWhat n becomesOutcome
r = 50circumference 314, n = 104fine
r = 1circumference 6.3, n = 2a 2-sided polygon
r = 0.2circumference 1.3, n = 0division by zero

Predict first

With the plus 3 removed, what happens for a very small radius?

  • A tiny circle is drawn correctly
  • A ZeroDivisionError, because n becomes zero
  • A TypeError, because n is a float
  • Nothing is drawn, but no error occurs

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.

24. Explain it yourself: specification versus rendering

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.

25. Refactoring: factoring out what two functions share

Section

Section 3

26. When arc will not cooperate

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)
PartWhat it doesNote
lines 2-5work out how many steps and how bigarithmetic
lines 6-8the drawing loopidentical in shape to polygon's
the problemthat loop is duplicatedtwo 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

27. Picture it: the shared part, and the name problem

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

The bottom three lines are the same idea twice. Only the arithmetic above differs.

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.

28. Worked example: factoring out polyline

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)
FunctionWhat it now doesEffect
polylinethe shared loop, with everything as parametersknows nothing about shapes
polygoncomputes its angle, then delegates3 lines instead of 5
arccomputes its values, then delegates5 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

The whole run at once: each drop is one line of the program.

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.

29. Rank: the refactoring, step by step

Ranking

Put the moves in the order that keeps the program working at every stage.

Put in order

  1. notice that polygon and arc contain the same loop
  2. write polyline, with every varying value as a parameter
  3. rewrite polygon and arc to call polyline
  4. check that polygon and arc still draw what they drew before

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.

30. Worked example: circle becomes one line

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)
ObservationWhat followsResult
a circleis an arc of 360 degreesone call
circle(t, r)arc(t, r, 360)the whole body
the chaincircle -> arc -> polylinethree 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.

31. Trap: generalizing a function past its own name

Trap

The 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.

The fix

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.

32. Predict: what does arc(t, r, 90) draw?

Prediction

The angle parameter is a fraction of a circle, in degrees.

Predict first

What does arc(bob, 50, 90) draw?

  • A quarter of a circle of radius 50
  • A complete circle of radius 90
  • A 90-sided polygon
  • A straight line 50 pixels long

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.

33. Match each function to the one idea it adds

Matching

Four functions in a chain. Each adds exactly one thing.

Match the pairs

  • a. polyline
  • b. polygon
  • c. arc
  • d. circle
  • r1. draws n segments with a given turn between them
  • r2. adds: the turns should sum to a full revolution
  • r3. adds: work the step size out from a radius and a sweep
  • r4. adds: the sweep is 360

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.

34. Find the counterexample: is refactoring always worth it?

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.

35. The development plan, and why you could not have planned it

Section

Section 4

36. Encapsulation and generalization, as five steps

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

37. Picture it: the plan is a loop, not a line

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.

38. Worked example: the case study as five steps

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 stepThe moveWhat came out
step 1a small program, no functionsthe loop
step 2encapsulationsquare(t)
step 3generalization, twicesquare(t, length), polygon(t, n, length)
step 5refactoringpolyline

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

The whole run at once: each drop is one line of the program.

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.

39. Rank: the development plan in order

Ranking

Five steps. Two of them are easy to put in the wrong place.

Put in order

  1. write a small program with no function definitions
  2. encapsulate a coherent piece and name it
  3. generalize the function by adding parameters
  4. look for opportunities to refactor

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.

40. Worked example: the honest sentence about planning

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.'
ClaimContentWhy it matters
the idealdesign the interfaces firstsometimes possible
the realityyou do not know enough yetusually true
the conclusionrefactoring is learning, not failurethe 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

The whole run at once: each drop is one line of the program.

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.

41. Trap: skipping step 1 because you can see the design

Trap

The 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.

The fix

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.

42. Predict: when is it time to refactor?

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?

  • When the program is finished and you want to tidy it
  • When you have similar code in several places
  • As soon as any function is longer than five lines
  • Before writing anything, so the design is right from the start

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.

43. Step zero: applying the plan to something new

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.

44. Where this plan applies outside programming

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.

45. Docstrings and the contract between a function and its caller

Section

Section 5

46. Documenting the interface, and who is to blame

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 docstringWhat it suppliesNote
what it doesdraws n line segmentsconcisely, not how
what each parameter meanslength, angle in degrees, t is a turtleincluding units and types
what it does not sayanything about the loopthe 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

47. Picture it: the interface as a contract

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

An interface is like a contract between a function and a caller.

The value of writing them down is not ceremony. It is that when something goes wrong, the contract says which side broke it.

48. Worked example: what belongs in a docstring

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.
    """
ElementContentWhy it is worth including
what it doesdraws an arcone sentence
the parameterstypes and units for eachespecially the units
a worked hint360 draws a full circleresolves 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

The whole run at once: each drop is one line of the program.

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.

49. Sort: does this belong in the docstring?

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.

belongs in the docstring
angle is in degrees; t must be a Turtle; an angle of 360 draws a full circle
an implementation detail
it approximates the arc with straight segments about 3 pixels long; internally it calls polyline; the loop variable is called i
in
Each of these is something a caller must know to use the function correctly: the units of a parameter, the required type of another, and the answer to the obvious question about what the angle means at its maximum.
out
Each of these describes HOW the function works. A caller does not need any of them, and all three could change without changing what the function does — which is exactly what happened when the case study was refactored.

50. Worked example: using the contract to locate a bug

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
CaseWhat is trueWhose bug
case An is 4.5, not an integerprecondition violated: caller's bug
case Ball preconditions satisfiedpostcondition not met: function's bug
the rulecheck the preconditions firstthen 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.

51. Trap: a docstring that describes the implementation

Trap

The 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.

The fix

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.

52. Predict: whose bug is it?

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?

  • The function's, because it should handle any number
  • The caller's, because a documented precondition was violated
  • Nobody's — it is a Python limitation
  • The function's, because the error message is unhelpful

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.

53. Complete it: write the docstring's first line

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.

54. Explain it: why write a docstring for a function only you will use?

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.

55. Compare: the four process moves

Comparison

Fill the blanks. Each move has a trigger, and knowing the trigger is what makes the move usable.

Comparison matrix

MoveWhat it doesWhat triggers it
encapsulationwraps working code in a named functiona coherent piece you can name
generalizationreplaces a fixed value with a parametera value the caller should choose
interface designdecides what belongs in the parameter lista value that is a rendering detail rather than a specification
refactoringfactors shared code into a more general functionsimilar 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.

56. The procedure: designing a function's interface

Pattern

Six questions, in order. The last one is the test.

  1. Say in one sentence what the function does, from the caller's point of view.
  2. List every value the body needs that is not computable from the others.
  3. For each, ask: does it SPECIFY what to produce, or does it control HOW it is produced?
  4. Keep the specifications as parameters; compute or choose the rendering details inside.
  5. Write the docstring: the one-sentence summary, then each parameter with its type and units.
  6. Read the docstring back. If it needs caveats, or you cannot write it, redesign the interface rather than the docstring.

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

57. Check yourself 1 of 3: generalization

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)
ValueWhere it comes fromNote
na parameterthe caller chooses
lengtha parameterthe caller chooses
anglecomputed from nnot a parameter

Check your understanding

Why is angle computed rather than passed as a parameter?

  • A. Because passing floats as arguments is not allowed
  • B. Because it is determined by n, so passing it would allow an inconsistent pair (correct)
  • C. Because the turtle module computes it automatically
  • D. Because parameters must be integers

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.

Why A tempts people
Floats are perfectly acceptable as arguments, and indeed 360/n is a float. Nothing about the type is the issue.
Why C tempts people
The turtle module knows nothing about polygons. Every angle it receives is one your code computed.
Why D tempts people
Parameters can be any type at all — polygon's own t parameter is a Turtle object, and length is often a float.

58. Check yourself 2 of 3: interface design

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?

  • A. Because r is a float and n is an integer
  • B. Because r specifies which circle to draw, while n is a detail of how it is rendered (correct)
  • C. Because n can be computed from r exactly, like polygon's angle
  • D. Because circles do not have a number of sides

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.

Why A tempts people
Types have nothing to do with it. A parameter's type does not determine whether it belongs in an interface.
Why C tempts people
n is CHOSEN from the circumference rather than determined by it — any n would draw a valid circle, so this is a quality decision, not a derivation like polygon's angle.
Why D tempts people
The approximation does have a number of sides, and that number is genuinely used. The question is whose decision it should be, not whether it exists.

59. Check yourself 3 of 3: the contract

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?

  • A. In the caller
  • B. In the function (correct)
  • C. It cannot be determined
  • D. In the docstring

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.

Why A tempts people
The caller is at fault only when it violates a properly documented precondition. Here it did not.
Why C tempts people
It can be determined, and that is precisely why pre- and postconditions are worth stating. Without them it would indeed be a matter of argument.
Why D tempts people
A wrong docstring is possible, but the case as described says the preconditions were satisfied — meaning the documented contract was honoured and still the function misbehaved.

60. Where this shows up outside this course

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.

61. Confidence wager: commit before you check

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?

  • The width
  • The height
  • The number of sides
  • The turtle to draw with

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.

62. Explain it to someone else

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.

63. Exit ticket

Exit ticket

One honest answer. It decides what the next lesson opens with.

Predict first

Which of these is still least solid for you?

  • Generalization: which values should become parameters
  • Interface design: deciding what to leave out, and why
  • Refactoring: spotting shared code and naming what it does
  • Docstrings, preconditions and postconditions

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.

64. Synthesis: draw the map of this lesson

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.

65. What you can do now

Recap

Four pages, and the second half of a process you can apply to any program.

If you remember one thingIt is this
From generalizationA value the other parameters determine should be computed, not passed.
From interface designSpecification belongs to the caller; rendering belongs to the function.
From refactoringIf generalizing makes the name wrong, the general thing needs a new name.
From the planGet something working first. Refactoring last, when there is something to factor.
From the contractPreconditions 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

Sources

  1. Think Python, 2nd edition — Allen B. Downey — Allen B. Downey, Think Python: How to Think Like a Computer Scientist, 2nd edition (Green Tea Press, 2015), §4.5-4.10, pp. 32-36
  2. Python documentation — turtle — Turtle graphics
  3. Python documentation — More Control Flow Tools

Want this taught 1-on-1? Alexander tutors Python — $55/session, free consultation.

Book on Wyzant · Text (657) 465-8108