Skip to main content

Class 17 - Testing and variables that change

Date: October 21, 2026

⚠️Homework
Homework 6 due October 25, 2026
‼️Ending Skills
Last week for skills: 3
🆕New Skills
First week for skills: 4

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 intermediate only 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 cnt lets the function change the outside cnt; without it, assigning to cnt would make a new, local variable. PLL only allows global at #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 assert fails. 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 count after the test runs? What would every_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. Which assert fails, and why? What happens with the second version?
  • Both versions change count inside 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.

  1. 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)
  2. Trace through these function calls. What does process_data return 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()
  3. Trace execution. What does calculate return 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)
  4. Now, pick one of the examples above, and try to write tests for it using a test function. What goes wrong?

  5. Try using print to 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.