Why this matters
A program often needs a name that gets its value later, inside an if or a loop. If one path through the code forgets to set it, the program reads a name that has no value. In some languages the read quietly returns whatever bits were in that memory. The program then prints a number that looks plausible and is wrong.
Python does not do this. It never gives you leftover memory. A name that has no value raises an error when the line that reads it runs. The error is clear, but it has a catch. Python checks only the paths that actually run. A branch that is never taken can hide the bug until a different input arrives. This lesson shows the errors Python gives, how to write code that sets the name on every path, and why a starting value must suit the data.
The lesson on variables covers how a name is created. This lesson covers only the case where a name is used before any line has given it a value.
The idea: a name exists when it is assigned
In Python, a name is created by an assignment. Until the first assignment runs, the name does not exist. Think of a name as a sticky note. The note does not exist until someone writes it and sticks it on a value. Reading a name that has no note is an error, not a read of an empty box.
10The assignment creates the name score, and the print reads it. If the two lines were swapped, the read would come first and the program would stop.
Reading a name before it exists
The smallest version of the bug is a read with no assignment at all. At the top level of a file, Python stops with NameError:
print(total)Traceback (most recent call last):
File ".../top.py", line 1, in <module>
print(total)
^^^^^
NameError: name 'total' is not definedThe message gives the name, total, and the caret points at the read. The fix is to give the name a value before the read, for example total = 0.
Inside a function, the same bug gives a different message. Python decides when it compiles the function that any name assigned somewhere in the function is local to it. A read before the assignment then fails with UnboundLocalError:
def show_total():
print(total)
total = 1
show_total()Traceback (most recent call last):
File ".../local.py", line 5, in <module>
show_total()
~~~~~~~~~~^^
File ".../local.py", line 2, in show_total
print(total)
^^^^^
UnboundLocalError: cannot access local variable 'total' where it is not associated with a valueThe word "local" tells you that the name belongs to the function. The message says the name is not "associated with a value" yet, which is the same idea as "not initialised" in other languages. The last line of the traceback names the function and the line that failed.
Assigning on only some paths
The bug is harder to spot when the assignment is inside a condition. The function below looks for the largest value above 5. It sets best only when a value is above 5, and then it returns best.
def best_above_five(values):
for v in values:
if v > 5:
best = v
return best
print(best_above_five([4, 9, 2]))
print(best_above_five([4, 2]))9
Traceback (most recent call last):
File ".../best.py", line 8, in <module>
print(best_above_five([4, 2]))
~~~~~~~~~~~~~~~^^^^^^^^
File ".../best.py", line 5, in best_above_five
return best
^^^^
UnboundLocalError: cannot access local variable 'best' where it is not associated with a valueThe first call works, because the list has a value above 5, so best is set. The second call fails, because no value is above 5, so the loop never assigns best. Python does not check this in advance. It runs the function, and the error appears only on the call that takes the path with no assignment. The program may run for a long time before a test uses such an input. This is why the rule is about every path, not about the one input you tried.
Writing code that always has a value
There are three common ways to make every path assign the name.
The first is to give the name a starting value before the branch. This is the simplest fix, and it is often the right one. The second is to add an else branch that assigns the name, so that both branches of the if set it. The third is to return a value in the path where nothing is found, such as None.
The next program uses the else form. Both branches of the if set x, so the read on the last line always works.
1The program prints 1 when it is run with no extra arguments. The variable sys.argv holds the name of the program and the words typed after it, so len(sys.argv) is 1 when nothing else is typed. Both branches set x, so the read is safe whatever the value of flag.
Here is the same idea for a loop. The safe pattern is to set a value before the loop, and then let the loop change it:
best = 9Starting best at values[0] works because the list is not empty. For a list that might be empty, the code must check for that case first, or start with None and handle it. This is a design choice, and the rule is the same: give the name a value before any path can read it.
A starting value can hide a wrong answer
Giving a name a value removes the error. It does not remove a wrong answer. The next program starts best at 0, which runs and gives the wrong answer for negative numbers.
zero start on [-5, -3]: 0The answer should be -3. The starting value 0 is larger than both numbers, so the loop never replaces it. The program has no error and prints 0. The fix is to start from a value that comes from the data, which is values[0] in the previous program, or from a value that every real number beats, such as float("-inf").
fixed on [-5, -3]: -3
fixed on [4, 9, 2]: 9The starting value float("-inf") is smaller than every number, so the first value always replaces it. An integer that is smaller than every expected value also works, but float("-inf") has no limit to worry about.
A search that may find nothing
A search is a common place for this bug. The function below looks for the position of the first negative number. It sets index only when it finds one, and then it returns index. If the list has no negative numbers, the loop never sets index.
def index_of_first_negative(values):
for i, v in enumerate(values):
if v < 0:
index = i
break
return index
print(index_of_first_negative([3, -1, 4]))
print(index_of_first_negative([3, 1, 4]))1
Traceback (most recent call last):
File ".../search.py", line 9, in <module>
print(index_of_first_negative([3, 1, 4]))
~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^^^^^
File ".../search.py", line 6, in index_of_first_negative
return index
^^^^^
UnboundLocalError: cannot access local variable 'index' where it is not associated with a valueThe usual fix is to start index at a value that means "not found". In this case -1 is a good choice, because no position in a list is negative. The caller then checks for -1 before using the result.
1
-1The second call returns -1 because no element is negative. This is a case where a starting value is the right design, unlike the 0 in the earlier example, because -1 is outside the range of valid positions.
The same search can return None instead, which is the more common Python style when "not found" is a normal answer. The caller then writes if result is None:, and the is keyword is correct there, because None is a single object.
try and except
A try block is code that might raise an error, and an except block runs if it does. Beginners often assign a name inside try and read it after the whole statement. Python does not assume the try succeeded, so the read fails when the except path runs.
def parse(text):
try:
n = int(text)
except ValueError:
print("not a number")
return n
print(parse("42"))
print(parse("abc"))42
not a number
Traceback (most recent call last):
File ".../parse.py", line 9, in <module>
print(parse("abc"))
~~~~~^^^^^^^
File ".../parse.py", line 6, in parse
return n
^
UnboundLocalError: cannot access local variable 'n' where it is not associated with a valueThe first call works. The second call takes the except path, which does not set n, so the return fails. Giving n a value in the except block fixes it. The program below prints 42 for the input "42", and -1 for "abc".
42
-1Attributes and collections
Names in a function are not the only place where a missing value appears. An object's attribute that was never set raises AttributeError. A dictionary key that is missing raises KeyError. A list index past the end raises IndexError. Python does not give these a default value.
class Reading:
pass
print(Reading().ratio)Traceback (most recent call last):
File ".../attr.py", line 4, in <module>
print(Reading().ratio)
^^^^^^^^^^^^^^^
AttributeError: 'Reading' object has no attribute 'ratio'The message names the attribute that is missing. The fix is to set it in the class, usually in the __init__ method, which the object-oriented lesson covers.
Python gives you explicit ways to say "use a default" for collections. The method dict.get returns a default when the key is missing, and collections.Counter returns 0 for any key it has not seen. Use them when a missing value is normal, not when it means a bug.
1 0
0No leftover values
Some languages let a local variable hold whatever bits were left in memory by an earlier call. The program then prints a number that depends on the machine and the compiler settings. Python does not work this way. Each name is created by an assignment, and each read looks up the value that the assignment stored. If there is no value, Python stops with an error, and it never prints memory that a previous call left behind.
This is a strong guarantee, but it does not replace careful code. Python checks only the path that runs, so a branch that is rarely taken can still hide the bug. Read every if and loop as a set of paths, and check that each path sets the names it later reads.
Common mistakes
The first mistake is assigning a name in only one branch of an if, then reading it after the if. The error is UnboundLocalError inside a function and NameError at the top level. The fix is an else branch, or a starting value before the if.
The second mistake is incrementing a name that has never been set. The line count += 1 reads the old value before it writes the new one, so it counts as a read. In a function, this gives the same error as before:
def count_up():
n += 1
return n
count_up()Traceback (most recent call last):
File ".../count.py", line 5, in <module>
count_up()
~~~~~~~~^^
File ".../count.py", line 2, in count_up
n += 1
^
UnboundLocalError: cannot access local variable 'n' where it is not associated with a valueThe fix is to give n a value first, for example n = 0.
The third mistake is a starting value that is wrong for the data. The function biggest_with_zero runs and prints 0 for [-5, -3], which is wrong. A starting value must be one that any real value can beat, or a value taken from the data. Choose it on purpose, and test it with negative numbers and with an empty input.
The fourth mistake is expecting a constant to stop a change. Python has no final keyword. The convention is to write the name in capital letters, as the variables lesson described. Python does not check that the name is assigned only once, so a second assignment runs without an error.
Practice
Work each exercise yourself before you open the answer. Predict the output on paper first.
- Does this code run without an error? Explain the reason in one sentence.
n += 1
print(n)- Does this program run, and what does it print when it is run with no extra arguments?
import sys
flag = len(sys.argv) == 1
if flag:
x = 1
else:
x = 2
print(x)-
The function
biggest_with_zerostartsbestat 0. Change it so that it returns -3 for the input[-5, -3], and still returns 9 for[4, 9, 2]. -
The code below fails on some inputs. Explain why, then fix it so that the program prints 10 when no arguments are given and 0 otherwise.
import sys
ready = len(sys.argv) == 1
if ready:
limit = 10
print(limit)- Predict the output of this program, and explain why the two reads cannot fail, even though reading a name that was never assigned would.
counts = [0] * 3
names = [None] * 2
print(counts[0], names[0])Each exercise has an answer below a "Show the answer" line. Try each one first, then read the answer.
- No. The line
n += 1readsnbefore it writes it, andnhas never been given a value. Python raisesNameErrorat the top level, andUnboundLocalErrorinside a function.
Show the answer
The output is a NameError at the first line, with the message name 'n' is not defined.
- It runs. Both branches of the
ifassignx, so every path sets it before the read. With no extra arguments,len(sys.argv)is 1, soflagisTrueand the program prints1.
Show the answer
The output is 1.
- Start
bestfrom the first element of the list, which is always a real value.
Show the answer
def biggest_fixed(values):
best = values[0]
for v in values:
if v > best:
best = v
return best
print(biggest_fixed([-5, -3]))
print(biggest_fixed([4, 9, 2]))The output is -3 and then 9. For an empty list, values[0] raises IndexError, so a real program must check the length first.
- The name
limitis set only whenreadyisTrue. WhenreadyisFalse, the read on the last line has no value. Add anelsebranch that also sets it.
Show the answer
import sys
ready = len(sys.argv) == 1
if ready:
limit = 10
else:
limit = 0
print(limit)The output is 10 when no extra arguments are given.
- The output is
0 None. Both reads come from lists, and each list was created with a value at every index. The namescountsandnameswere assigned, so the reads cannot fail. A name that was never assigned would be an error, but a list element that exists always has a value, even if that value isNone.
Show the answer
counts = [0] * 3
names = [None] * 2
print(counts[0], names[0])The output is 0 None.
What you now know
- A name exists when an assignment creates it. Reading a name before that raises
NameErrorat the top level andUnboundLocalErrorinside a function. - Python checks the path that runs, not every path. A bug in a branch that is not taken can stay hidden until a different input arrives.
- Fix the error by assigning on every path, or by giving the name a starting value that is correct for the data.
- A missing attribute, key or index raises an error.
dict.getandCountergive a default when a missing value is normal. - Python never returns leftover memory. A read with no value stops the program with an error.