What happened when handling 'x = x + 1'?

Hey everyone,

I’m new to Python and I’m getting a bit confused about namespaces and scopes. Can someone help me understand why the following code throws an UnboundLocalError?

x = 1
def foo():
    x = x + 1
    print(x)
foo()

When the interpreter processes the line x = x + 1, shouldn’t it do something like this:

  1. Look at the right side of the =: find the name x in the global namespace (which is 1), and get the result 1+1=2.
  2. Create a new object (<int>2).
  3. Create the name x in the local namespace.
  4. Bind the local name x to the object <int>2 (the assignment).

In C++, similar code would work just fine and output the expected result. Here’s how that looks:

#include <iostream>

int x = 1;

void foo()
{
    x = x + 1;
    std::cout << x;
}

int main()
{
    foo();
    return 0;
}

I see there are some differences between how Python and C++ handle variables. Anyway, any insights you can offer would be super helpful!

Thanks in advance!

In a python function you can use a global variable directly, but to change it you need the global keyword.
Without that keyword, setting a variable value in a function implicitly declares it as a new local variable.
So by writing x = x + 1 you are creating a new, uninitialized, variable x local to the function. Since is not initialized, you can’t add 1 to it and the interpreter complains.

You should change your function to

def foo():
    global x
    x = x + 1
    print(x)

This comes down to how Python is interpreted + specific choices around the handling of globals in python which differ from C++ (which inherited and has kept its particular behavior from C). Python chooses that by default, you can’t modify things outside of your “local scope”. If you want to modify things outside of the local scope you need to use the global or nonlocal keywords (PEP 3104 – Access to Names in Outer Scopes | peps.python.org, 7. Simple statements — Python 3.12.4 documentation).

Using the global keyword as @rgh will make the behavior match.

Python Programming FAQ: Programming FAQ — Python 3.12.4 documentation

Python Reference around this behavior: 4. Execution model — Python 3.12.4 documentation

Thanks @cmaloney @rgh

However, if we dig deeper and focus specifically on the statement x = x + 1:

According to your explanation, the Python interpreter should first process the left-hand side of the assignment, creating x (since there’s no global keyword, it interprets this as a local name), and then try to compute x + 1 on the right-hand side. Since the interpreter follows the “LEGB” rule when resolving names, it would interpret x as local at this point. However, since x has not yet been bound to any object, an error occurs.

But following my understanding, if the Python interpreter processes the right-hand side first, shouldn’t it resolve ‘x’ in ‘x + 1’ as global? This way, the value on the right-hand side would be 2, which could then be assigned.

If my understanding does not reflect how the Python interpreter actually works, please let me know, as that would solve the issue.

Thank you again!

A closer parallel would be:

void foo()
{
    int x;
    x = x + 1;
    std::cout << x;
}

(Note that I didn’t say int x = x + 1; as this can actually have quirky but extremely useful semantics in some languages, and I want to avoid that. Not sure if C++ behaves that way or not.)

The name x is local throughout the function. There’s no point prior to it becoming local, no point at which referencing x will give you the global. So, what happens when you try to read x before it’s been given a value? Well, different languages disagree, but in Python, it’s an immediate exception. (Definitely better than reading uninitialized memory, especially since there’s no guarantee you’re getting an integer.)

If you feel like messing around further, a class statement in Python actually introduces a slightly different kind of scope - what I and others have described here is a function scope, but a class scope is different. New things to explore! :slight_smile:

I think 4. Execution model — Python 3.12.4 documentation has a better explanation than I have currently of the order of operations, including what happens with eval() and the like. Particularly its parts around “free variables”, which I think in your x = x + 1 applies to the right hand side. When executing that line x in x + 1 is “free” / unbound to a scope at compile time, which means it binds at execution time. x = is “bound” to the local scope at compile time, but won’t be resolved to because that only becomes available after the x + 1 evaluates

Received your replies @Rosuav @cmaloney, I’ll study that documentation. Thanks for the responses! :wink:

Hello everyone,

I’ve skimmed through some literature and now gained a new understanding of this issue:

Firstly, in 4.2.2. Resolution of names:

The local variables of a code block can be determined by scanning the entire text of the block for name binding operations.

and “name binding operation” includes “assignment expression” (see 4.2.1. Binding of names).

And in Why am I getting an UnboundLocalError when the variable has a value? - Programming FAQ, there is an example:

x = 10
def foo():
    print(x)
    x += 1
Traceback (most recent call last):
  ...
UnboundLocalError: local variable 'x' referenced before assignment

This is because when you make an assignment to a variable in a scope, that variable becomes local to that scope and shadows any similarly named variable in the outer scope.

According to my understanding yesterday, since print(x) comes before x += 1, it should first resolve x as global. However, this example made me realize that the Python interpreter has a step where it scans the entire code before executing it.

Then I learned that the execution of a Python program includes steps such as lexical analysis, syntax analysis, compilation, and execution. Therefore, the interpreter would find the assignment statement x += 1 first and resolve x as a local variable (shadowing the global x), ultimately reporting an error at the print statement during execution.

So @rgh, you were right, and thank you to @cmaloney for providing all the reference materials. The Python documentation is a great resource, and I will refer to it more often in the future.

And then you learn about dicts (maps), and you find out that these can be updated inside and outside a function, without needing the global statement.

Take g as a dict that is updated globally in a program:

g = {}
g['x'] = 0
def sub():
  g['x'] = g['x']+1
  print('in',g['x'])
sub()
print('out',g['x'])

Result:

in 1
out 1

This is explained by writing the code in an older format:

g = dict(x=0)
def sub():
  g.update(dict(x=g.get('x')+1))
  print('in',g['x'])
sub()
print('out',g['x'])

So assigning a value to g[‘x’] is not an assignment that requires a bound name, but rather an update to an existing object (variable). This behavior can be useful for writing routines with complex data structures, but sure is surprising at first.

The behavior @robvh describes is probably more familiar to C++ devs, because they’re used to working with const references and pointers. Non-local container objects in Python are immutable, but their contents are not, so effectively g in the example above is a const std::map<std::string, int>& g — you can’t assign to g itself, but you can manipulate its members freely.