Embercode
✕
Variables and Data TypesLesson 12 of 16

35 min read

Names with no value yet in Python

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.

Python · editable
10

The 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 defined

The 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 value

The 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 value

The 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.

Python · editable
1

The 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:

Python · editable
best = 9

Starting 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.

Python · editable
zero start on [-5, -3]: 0

The 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").

Python · editable
fixed on [-5, -3]: -3
fixed on [4, 9, 2]: 9

The 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 value

The 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.

Python · editable
1
-1

The 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 value

The 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".

Python · editable
42
-1

Attributes 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.

Python · editable
1 0
0

No 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 value

The 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.

  1. Does this code run without an error? Explain the reason in one sentence.
n += 1
print(n)
  1. 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)
  1. The function biggest_with_zero starts best at 0. Change it so that it returns -3 for the input [-5, -3], and still returns 9 for [4, 9, 2].

  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)
  1. 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.

  1. No. The line n += 1 reads n before it writes it, and n has never been given a value. Python raises NameError at the top level, and UnboundLocalError inside a function.

Show the answer

The output is a NameError at the first line, with the message name 'n' is not defined.

  1. It runs. Both branches of the if assign x, so every path sets it before the read. With no extra arguments, len(sys.argv) is 1, so flag is True and the program prints 1.

Show the answer

The output is 1.

  1. Start best from 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.

  1. The name limit is set only when ready is True. When ready is False, the read on the last line has no value. Add an else branch 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.

  1. The output is 0 None. Both reads come from lists, and each list was created with a value at every index. The names counts and names were 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 is None.

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 NameError at the top level and UnboundLocalError inside 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.get and Counter give a default when a missing value is normal.
  • Python never returns leftover memory. A read with no value stops the program with an error.
Practice this topic: coding problems for Variables and Data Types →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Floating-point comparisons in Python