UnboundLocalError in Python: a local variable used too early

UnboundLocalError reports a local variable read before it was given a value. Where the error comes from, why Python raises it, how to fix it.
5 min read
Believemy logo

Definition

A function can look like it is reading a variable that already exists, defined further up in the same file, and Python still refuses to run it. That is what UnboundLocalError reports: the function reads a local variable before any value has been given to it. The name does exist as far as Python is concerned, since it was spotted while the body was analysed, but it points at nothing yet: the slot is reserved, and it is empty.

The simplest case looks like this, a function trying to increment a counter defined just above it.

PYTHON
counter = 0

def increment():
    # the assignment below is enough to make "counter" local
    counter = counter + 1
    return counter

increment()

# UnboundLocalError: cannot access local variable 'counter'
# where it is not associated with a value

The message is puzzling, since counter is indeed defined two lines above. That definition plays no part here: the assignment written inside the function body was enough to turn counter into a local name, and it is that local name, still empty, that the read asks for.

Good to know

The wording changed with Python 3.11. Older versions show "local variable 'counter' referenced before assignment", a drier phrasing that points at the same error.


Python decides scope before running anything

This mechanism explains all the rest. When it is defined, a function is analysed as a single block: any name receiving an assignment somewhere in its body becomes local for the whole body, including on the lines that come before the assignment. The decision is made once and for all, before a single call.

A plain read triggers nothing. With no assignment inside the function, Python looks the name up outside and finds it without complaining. So it really is the assignment that flips the scope, wherever it sits, including on a line never reached at run time.

The table below sums up these three situations, and what happens when the name is read before the assignment that concerns it.

Inside the function bodyScope of the nameRead before assignment
Read onlyGlobalWorks
An assignment somewhereLocalUnboundLocalError
Assignment plus globalGlobalWorks


The disguised forms

The textbook case is spotted quickly in code. The lost hours come from assignments that do not look like assignments.

The first is the augmented assignment operator. counter += 1 is shorthand for counter = counter + 1: it reads the current value then writes it back to the same name, and it is that read, just before, that fails.

The second comes from the variable of a loop, which takes a fresh value on every pass: that counts as an assignment like any other. Read again after the loop, it falls under the same rule, local and empty if the loop never ran.

The third is the sneakiest of the three, because it only produces an error on certain data. Here is the same trap, with an assignment sitting inside a single branch of a test.

PYTHON
def message(score):
    if score >= 10:
        # text only exists if this branch runs
        text = "Passed"
    return text

message(5)

# UnboundLocalError: cannot access local variable 'text'

Above ten, all is well. Below it, the branch never runs, text never receives a value, and the return fails. A condition with no else leaves that hole open, and the hole only opens on certain data: the tests pass, production falls over.


The three fixes

The first is to set a default value before the test. A line placed at the top of the function closes the hole without changing the logic, and it documents along the way what the variable holds when no branch applies.

The second is to state the intent. global lets the function reassign a module-level name, nonlocal does the same for a name belonging to an enclosing function. Both clear the error, but both also open the door to changes made at a distance, hard to follow as soon as the program grows.

The third, often the better choice, is to share nothing at all: the function takes what it needs as an argument and hands back its result. This error is sometimes the first sign that global state is travelling where passing a value would do.


Frequently asked questions

Question

What is the difference with NameError?

UnboundLocalError inherits from NameError, so it is a special case of it. The distinction lies in what Python knows about the name. Here it knows it and has filed it as local, but no value has been attached yet, whereas a NameError reports a name found nowhere at all, often a typo.

Question

Why can a global variable be read without declaring anything?

Because reading creates no local name. Python walks the function body, finds no assignment there, and therefore looks the name up at module level. The global keyword only becomes necessary when that name is reassigned, never to consult it.

Question

How can it be tracked down quickly?

The traceback gives the offending line and the name involved. All that is left is to look for that name further down in the same function: the assignment sitting there is the cause, even when it looks unrelated to the line that failed.

Related terms

Discover our python glossary

Browse the terms and definitions most commonly used in development with Python.

Share this article

Want to help us? Share this article on your networks or even better: on your site, in an article or in your newsletter.