Traceback (most recent call last):
File "ledger.py", line 12, in <module>
print(running_totals([100, -30, 45]))
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^
File "ledger.py", line 9, in running_totals
return [add(amount) for amount in amounts]
~~~^^^^^^^^
File "ledger.py", line 6, in add
total += amount
^^^^^
UnboundLocalError: cannot access local variable 'total' where it is not associated with a valueThe same rule with no nested function in sight. Adding one line changes what the first line means.
count = 0
def show():
print(count) # fine: count is only read, so it is the outer one
def bump():
count += 1 # an assignment, so count is local to bump
show() # 0
bump()
# UnboundLocalError: cannot access local variable 'count' where it is not
# associated with a value
- Decided before the function runs
Python works out which names are local when it reads the function, not line by line as it runs. One assignment anywhere, even further down, makes the name local everywhere in that function.
- += reads before it writes
count += 1 means count = count + 1. The right-hand side reads the local count, which does not have a value yet, so it fails on the read.
- "local variable" is the clue
The message says local. When you meant the outer variable, that word tells you Python did not agree, and the assignment is why.
The error is in add, and it says total is a local variable there. Which line inside add makes total local?
total += amount assigns to total, so inside add total is add’s own variable, and it has no value yet. The total set to 0 outside add is never used.
The simplest fix removes the inner function: a plain loop with total in running_totals, adding each amount and appending the new balance. (nonlocal total inside add also works, and is worth knowing.)
def running_totals(amounts): balances = [] total = ____ for amount in amounts: total += ____ balances.append(____) return balances
def running_totals(amounts):
"""The balance after each amount, starting from nothing."""
balances = []
total = 0
for amount in amounts:
total += amount
balances.append(total)
return balances
With the loop directly in running_totals there is only one scope, so there is only one total and no question of which one += means.
Writing nonlocal total as the first line of add tells Python that total belongs to the enclosing function. It works, and it is the right tool when a nested function genuinely has to keep state. Here the nesting was not buying anything.
With no amounts, add never runs, so the broken line never runs either and the starter returns []. A test with only the empty list would have passed this bug straight through.
itertools.accumulate(amounts) already produces running totals, and list() of it is the whole answer. Write the loop when you need something accumulate cannot do in one step, like stopping when the balance goes negative.
- Start from an opening balance passed as a second argument, defaulting to 0.
- Mark the first transaction that takes the balance below zero.
- Write it with itertools.accumulate and then with nonlocal, and say which you would keep.