Class 5 - Conditionals, Tests, and print
Date: September 17, 2026
Skills: 0, 1, 2
Notes (review before class)
Tests
Keep the examples a function was written from (Class 4), since software evolves: a function may stop matching them, on purpose or by mistake.
def moon_weight(earth_weight: float) -> float:
"""Compute weight on moon from weight on earth"""
return earth_weight * 1/6
def test_moon_weight():
assert moon_weight(100) == 100 * 1/6
assert moon_weight(150) == 150 * 1/6
assert moon_weight(90) == 90 * 1/6
- A test function's name starts with
test_; it goes right after the function it checks. - Each
assertchecks that the expression after it isTrue;==checks that two values are equal. - PLL runs the tests every time you run the file, and reports failures. A
test function stops at its first failing
assert. - Try: change the body to
return earth_weight * 1/3and run the file, to see what a failing test looks like. Then fix it, break a test instead, and compare.
Writing Good Tests
def test_pen_cost():
assert pen_cost(3, "wow") == 3 * (0.25 + len("wow")*0.02)
assert pen_cost(10, "smile") == 10 * (0.25 + len("smile")*0.02)
assert pen_cost(5, "") == 5 * 0.25
assert pen_cost(0, "huskies") == 0
- The last two are special (yet valid) inputs: an empty message, and no pens. (Ignore nonsensical ones, like negative pens, for now.)
- The right of
==just has to evaluate to the expected answer, written in some meaningful way. - Never run the function to get a test's answer! If a wrong body produces the answers, the tests can't catch it.
- Examples that repeat the body catch little, but later ones will be much simpler than the body: professionals write large collections of tests.
- Working on concrete values first checks that you understand the problem: if you can't write examples, you probably can't write the function.
- Tests are step 3 of the design recipe: (1) type annotations, (2) docstring, (3) tests, (4) the body.
Choosing Test Inputs
add_shipping adds shipping to an order: $4 for $10 or less, $8 above.
Tests first, with strategically chosen inputs:
def test_add_shipping():
assert add_shipping(10) == 10 + 4
assert add_shipping(3.95) == 3.95 + 4
assert add_shipping(20) == 20 + 8
assert add_shipping(10.01) == 10.01 + 8
10is at the boundary between the charges;10.01is just over it. Boundaries are a common source of bugs (mistakes that make a program misbehave).- Whole and decimal numbers, and examples for each charge.
4and8appear only in the answers: which one depends on the input.
Conditionals
An if statement asks a question about the input:
def add_shipping(order_amt: float) -> float:
"""add shipping costs to order total"""
if order_amt <= 10:
return order_amt + 4
else:
return order_amt + 8
- If the answer to the question (
order_amt <= 10) is true, Python runs the code under theif; if false, the code underelse(the else clause). - The
ifandelselines end with a colon; each case is indented under its line.
Booleans
3.95 <= 10 is True and 20 <= 10 is False: the only two values of the
type bool (for Boolean, after George Boole). (order_amt <= 10 at the
prompt is an error: order_amt only exists inside add_shipping.)
1 == 1 # True
"cat" == "CAT" # False
"a" < "b" # True
"that" < "this" # True
"a" >= "C" # True
"a" >= "A" # True
==checks equality (unlike=, which associates a name with a value).- Strings are case-sensitive.
<,>=, etc. order strings alphabetically, following ASCII, an old convention: every uppercase letter comes before every lowercase one (letters outside ASCII are far more complicated).- Any two values can be compared:
"a" == 1isFalse, not an error, so comparing a number with a string by mistake quietly gives nonsense. - Most kinds of data have operations producing booleans;
inchecks whether one string appears inside another:
wm = "will.i.am"
"will" in wm # True
"Will" in wm # False (note the capital W)
and, or, and not combine booleans (vote if a citizen and
old enough; board a bus with a ticket or on a free-ride day; drive if not
inebriated):
(1 < 2) and (3 < 2) # False
(3 < 2) or (1 < 2) # True
not (1 < 2) # False
Asking Multiple Questions
For a third charge ($12, over $30), elif ("else if") adds a question:
def add_shipping(order_amt: float) -> float:
"""add shipping costs to order total"""
if order_amt <= 10:
return order_amt + 4
elif order_amt <= 30:
return order_amt + 8
else:
return order_amt + 12
- Python asks the questions in order, and runs the code for the first that is
True(orelse, if none are). Any number ofelifs is allowed. - "Between $10 and $30" is implicit:
order_amt <= 30is only asked iforder_amt <= 10wasFalse.elif (order_amt > 10) and (order_amt <= 30):makes it explicit (comparisons happen beforeand, so the parentheses just make the order clear). - Both versions are correct; add tests for the new charge, like
assert add_shipping(30) == 30 + 8andassert add_shipping(32) == 32 + 12. - A redundant condition like
order_amt > 10shows readers which case it covers, and keeps a mistake in an earlier question (now, or after a later change) from silently giving surprising output.
Try: write show_ad, which takes a user's age and hair color and decides
whether to show them an ad for a skateboarding park: True if they're between
9 and 18 with pink or purple hair. Write it once with if, once with only
boolean operations.
Harms from reducing people to simple data. show_ad encodes a stereotype:
many skateboarders aren't young, or don't have colored hair. Real systems
deciding who sees an ad, is hired, gets a loan, or is released from jail also
compare data about people with criteria. Ask what data should represent
people, and what stereotypes it might encode: decisions based on it can be
inaccurate, and harmful.
Evaluating by Reducing Expressions
Substitution (Class 4) works with if too, step by step, as in
algebra. Pay is $10 an hour up to 40 hours, and $15 for each extra hour:
def wage(hours: float) -> float:
"""pay for hours worked: $10 an hour, and $15 an hour after 40"""
if hours <= 40:
return hours * 10
elif hours > 40:
return 40*10 + (hours-40)*15
wage(45)
=> if 45 <= 40:
return 45 * 10
elif 45 > 40:
return 40*10 + (45-40)*15
=> if False:
return 45 * 10
elif 45 > 40:
return 40*10 + (45-40)*15
=> if 45 > 40:
return 40*10 + (45-40)*15
=> if True:
return 40*10 + (45-40)*15
=> return 40*10 + (45-40)*15
=> return 400 + 5*15
=> return 475
45replaceshours; aFalsequestion's case is dropped and the next question tried; aTrueone's case is all that's left.- This is the best way to think about how Python evaluates, and to work through a program by hand or in your head.
Composing Functions
To price pens with shipping, send pen_cost's output into add_shipping:
we compose the functions, directly, or by naming the intermediate result:
add_shipping(pen_cost(10, "bravo"))
pens = pen_cost(10, "bravo")
add_shipping(pens)
- Both give
7.5: a function's inputs are evaluated before the call, so3.5is substituted fororder_amteither way. - The second also stores
3.5in the directory aspens: more steps, but some find it easier to read. - A function's body can compose functions too: a function that needs another's result calls it (a helper), rather than repeating its code.
Local Names and the Directory
A name defined in a body can make it more readable:
def pen_cost(num_pens: int, message: str) -> float:
"""total cost for pens, each 25 cents
plus 2 cents per message character"""
message_cost = len(message) * 0.02
return num_pens * (0.25+message_cost)
pens = pen_cost(10, "bravo") now makes two directory entries, message_cost
(inside the body of pen_cost) and pens, but using message_cost afterwards
is a NameError:
pens = pen_cost(10, "bravo")
cheap_message = message_cost > 0.5
add_shipping(pens)
- Entries made inside a function are local (private) to its body: each call gets a local directory that other code can't see (a library function's body may not even be visible).
- A body can use its local names (
message_cost) and the global directory (len), but never another call's local names. - When a call finishes, its local directory disappears.
Debugging with print
Debugging is finding and removing bugs. print shows values while code
runs: any number of them, separated by commas, shown with spaces between (so a
label can go before a value):
print("pens:", pens, "with shipping:", add_shipping(pens))
- This shows
pens: 3.5 with shipping: 7.5. - A
printin a body shows values each time the function runs; what's printed during a test is shown with that test's results. - To see a question's answer, either name it (
small = order_amt <= 10, a local name) and printsmall, or print the expression itself (print("small:", order_amt <= 10)), adding no name.
Recap (20 mins)
You wear a sun hat when it's 80F or above, and otherwise no hat. Here is
choose_hat, designed with the recipe, with a print added while testing it:
def choose_hat(temp_in_f: float) -> str:
"""determines appropriate head gear, with 80F or above a sun hat, below nothing"""
comparison = temp_in_f >= 80
print("temp_in_f:", temp_in_f, "comparison:", comparison)
if comparison:
return "sun hat"
else:
return "no hat"
def test_choose_hat():
assert choose_hat(50) == "no hat"
assert choose_hat(85) == "sun hat"
assert choose_hat(80) == "sun hat"
- When PLL runs the tests, what does the
printshow, line by line? - Why is there a test for
80? If>=were>, whichassertwould fail, and what would theprinthave shown just before it? - After the file runs, what happens if you enter
comparisonat the prompt? What aboutchoose_hat(95)?
Class Exercise (30 mins)
- Add another possibility to
choose_hat, that if the temperature is below50, you choose a winter hat. Any time you are changing code, you should revisit each step of the design recipe, in order, and change as appropriate (type signature, doc string, tests, code). To have more than one comparison, use:
if condition:
...
elif condition:
...
elif condition: # any number of times
...
else:
...
- Now imagine you got new sunglasses, and want to design a function
add_glassesthat takes an outfit (as a string) and always adds ", and glasses" to what you will wear. - Let's now design a new function,
choose_outfit, that takes as input the temperature in fahrenheit, and uses the two functions you just wrote to compute a final outfit. There are at least two ways of doing this; one defines a new local name with the result of callingchoose_hat; the other directly passes the result of callingchoose_hattoadd_glasses - Now, add a new function
choose_hat_or_visorthat takes not only atemp_in_f, but also an additional argument,has_visor, which is a boolean that indicates whether the person owns a visor. If they do, and the temperature is above 95F, they will wear that. Your function should be able to re-use logic fromchoose_hat. The boolean operatorand, writtenboolean1 and boolean2, and evaluates toTrueif both inputs to it areTrue, will probably be useful! (Python also hasor, which won't be useful in this problem, andnot, writtennot boolean1, that evaluates toFalsewhenboolean1isTrueand vice versa).
Wrap-up (10 mins)
-
There is a useful order we should carry out when defining new functions, and we can make this concrete as a Design Recipe for functions:
-
Step 1. Type Annotation - Once we have decided we need to write a function, the first thing we should think about, assuming we were not told it, is what type of inputs the function should take and what type of output it should produce. While we introduced type annotations as a way to get better error messages, and indeed they are, this use as part of a design process is indeed much more important: it helps us organize our thoughts!
-
Step 2. Doc String - After figuring out the types of data the function operates over, it is worth writing down what the function should do! This is written in English, and it doesn't need to include how the function works (and usually shouldn't); the purpose of the explanation is so that someone can understand how to use the function, not how it works internally. While this serves as useful documentation, it is even more useful as part of a design process, since like type annotations, it helps us organize our thoughts; in particular, it makes us think a little bit more concretely about how the code should work -- i.e., how is the output constructed from the input.
-
Step 3. Tests: The third step is writing tests in a test function (
test_...) right after the function. These tests (or examples) serve as the most concrete step of our design process, as it's the point where we think through concrete cases of inputs, and exactly the output they should produce. It may be, as we write them, that we realize we did not think of something in the earlier steps, which is fine -- we go back and fix those and then continue forward. In addition to forming a useful step in the design process, these tests will also be used to check that our function works as expected! -
Step 4. Code -- The last step, which we should only do after the first three, is writing down the code. There is a strong tendency for students, especially beginners, to want to jump ahead to this step, but the clarity that comes from doing the earlier steps will make writing the code easier, to the point that it is faster to not skip ahead.
-