Class 17 - Testing and variables that change
Date: October 21, 2026
Skills: 4
Notes (review before class)
Changing Variables Only Inside Functions
- Our pattern (Class 15) only changes a variable made inside the function: each call adds a new one to the directory, removed when the call returns, so calls never share it.
#level intermediateonly allows changing variables inside functions.
Shared Variables
This function returns the prefix of a list (its elements from the start) that
sums to less than 100, with the running sum cnt outside it:
#level advanced
cnt = 0
def prefix_under_100(lst: list[float]) -> list[float]:
"""returns the elements at the start of lst that sum to less than 100"""
global cnt
output = []
for elt in lst:
cnt = cnt + elt
if cnt < 100:
output = output + [elt]
return output
global cntlets the function change the outsidecnt; without it, assigning tocntwould make a new, local variable. PLL only allowsglobalat#level advanced, which skips our levels' extra checks.
The test fails:
def test_prefix_under_100():
assert prefix_under_100([1, 2, 3]) == [1, 2, 3]
assert prefix_under_100([20, 30, 40]) == [20, 30, 40]
assert prefix_under_100([80, 20, 10]) == [80]
- The last
assertfails. Move it to the top, and it passes, but the one for[1, 2, 3]fails! - The directory has one
cnt, shared by every call and never reset: a call's result depends on every call before it, which the code doesn't show.
Try: trace cnt through the test, then fix prefix_under_100 by following the
pattern.
Side Effects
- Changing something outside the function is a side effect. These make code hard to test and understand, so bug-prone, often with bugs that only appear when calls happen in some rare order.
- They're sometimes needed, but almost all code can avoid them, and is more reliable for it.
Recap (15 mins)
A function every_third_element returns the first, fourth, seventh, ...
elements of a list, keeping track of the position in count. Read both
versions: the first keeps count outside the function, the second follows the
pattern.
#level advanced
count = 0
def every_third_element(lst: list) -> list:
"""returns the first, fourth, seventh, ... elements of lst"""
global count
output = []
for elt in lst:
if count % 3 == 0:
output = output + [elt]
count = count + 1
return output
def test_every_third_element():
assert every_third_element([]) == []
assert every_third_element([1, 3, 5]) == [1]
assert every_third_element([1, 3, 5, 7, 11]) == [1, 7]
def every_third_element(lst: list) -> list:
"""returns the first, fourth, seventh, ... elements of lst"""
count = 0
output = []
for elt in lst:
if count % 3 == 0:
output = output + [elt]
count = count + 1
return output
- With the first version, what is
countafter the test runs? What wouldevery_third_element([2, 4, 6, 8])return next? - Adding the line
every_third_element([1, 3])at the end of the file makes the test fail with the first version. Whichassertfails, and why? What happens with the second version? - Both versions change
countinside the loop. Which one could run at#level intermediate, and why is it safe to test?
Class Exercises (40 mins)
For these exercises, use #level advanced, since they all use global.
-
What does each call return? (Trace very carefully through the execution order):
score = 10
def bonus_points(amt: int) -> int:
global score
score = score + amt
return penalty_check()
def penalty_check() -> int:
global score
if score > 15:
score = score - 3
return score
bonus_points(2)
bonus_points(4)
bonus_points(1) -
Trace through these function calls. What does
process_datareturn each time?dta = [1, 2, 3]
position = 0
def advance() -> int:
global dta, position
result = dta[position]
position = position + 1
if position >= len(dta):
position = 0
dta = [0] + dta
return result
def process_data() -> int:
first = advance()
second = advance()
return first + second
process_data()
process_data()
process_data() -
Trace execution. What does
calculatereturn for each call?multiplier = 1
base = 5
def update_multiplier(n: int) -> int:
global multiplier, base
multiplier = multiplier + n
if multiplier % 3 == 0:
base = base - 1
multiplier = 1
return multiplier
def calculate(x: int) -> int:
update_multiplier(x)
return x*multiplier + base
calculate(2)
calculate(1)
calculate(4)
calculate(2) -
Now, pick one of the examples above, and try to write tests for it using a test function. What goes wrong?
-
Try using
printto inspect the values of appropriate variables and the effect the changes to the variables have across function calls.
Wrap-up (5 mins)
- While mutation can be very useful, it also can make programs much, much harder to understand.
- Ease of testing is often a good way of approximating ease of understanding -- if it is very hard (or impossible) to set up test cases, that's an indication to try to figure out a different way of organizing the program.