Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Python

Python vs C++ Series: Variable Scope

5.00/5 (6 votes)
25 Oct 2021CPOL4 min read 7K  
Some Python scope rules that are not intuitive for people from a C++ background
Every programming language has its way of defining scope, and most of them work similarly and have similar scope levels such as block scope and function scope. However, in some cases, Python scope rules that are not intuitive for people from a C++ background.

Every programming language has its way to define scope, and most of them work similarly and have similar scope levels such as block scope and function scope. This article is part of the Python vs. C++ Series and will focus on specific Python scope rules that are not intuitive for people from a C++ background.

(Note that the Python code in the series assumes Python 3.7 or newer.)

Variable Scope in C++

Scopes in C++ have several levels, but there are local, global, and block scopes in general.

C++
#include <iostream>

int global_variable = 0; // global variable

int myFunction(int parameter=0)
{
    // local variable can only be accessed within block.
    int local_variable = 0;

    if (parameter > 0)
    {
        local_variable += parameter;
    }
    else
    {
        // global variable can be accessed everywhere
        local_variable += global_variable; 
    }

    // update the global variable within a function
    global_variable = local_variable; 

    return local_variable;
}

double myFunction2()
{
    // local variable but has the same name as the global_variable.
    // In this case, the local one takes higher priority.
    double global_variable = 1.23;
    return global_variable;
}

void main()
{
    std::cout << global_variable << std::endl;
    // 0     The global global_variable
    std::cout << myFunction(10) << std::endl;
    // 10    The local_variable
    std::cout << global_variable << std::endl;
    // 10    The global global_variable updated by myFunction()
    std::cout << myFunction2() << std::endl;
    // 1.23  The local global_variable inside myFunction2()
    std::cout << global_variable << std::endl;
    // 10    The global global_variable was not affected by myFunction2()
}

Variable Scope in Python

Python also has several scope levels, and most of them work similarly to C++ and many other languages. However, as discussed in the previous article (Mutable, Immutable, and Copy Assignment), copy assignment does not create a new object; instead, it binds to an object. Therefore, using the assignment operator in Python leads to another question: does the assignment create a new object to bind to, or just update the binding to another object, and which one?

According to the official document – Python Scopes and Namespaces, the search order for a named variable is the following (quote from the document):

  • the innermost scope, which is searched first, contains the local names
  • the scopes of any enclosing functions, which are searched starting with the nearest enclosing scope, contains non-local, but also non-global names
  • the next-to-last scope contains the current module’s global names
  • the outermost scope (searched last) is the namespace containing built-in names

Due to this rule, some nonintuitive scenarios happen, and we will discuss these cases in the following subsections.

Control Statements

The search order mentioned above does not include control statements such as if-statement. Therefore, the following code is valid and works.

Python
# if-statement does not define a scope
condition = True
if condition:
    result = 1
else:
    result = 2

print(result)
# 1

Likewise, for-loop (and while-loop), with-statement, and try-except do not define a scope either.

Python
# for-loop does not define a scope
for i in range(10):
    x = 1 + i

print(x)
# 10

# with-statement does not define a scope
with open("example.txt") as file:
    data = file.read()

print(data)
# Output from the example.txt

# try-except does not define a scope
try:
    raise ValueError("Test exception")

except ValueError:
    message = "Catch an exception"

print(message)
# Catch an exception

Global Variable

The second scenario happens when using global variables. As we would expect, we can access a global variable from everywhere.

Python
global_variable = [1, 2, 3]   # global variable

def function1():
    print(global_variable)
    # [1, 2, 3]

function1()

However, if we try to update the global variable from a function or an inner scope, the behavior changes. For instance:

Python
global_variable = [1, 2, 3]   # global variable

def function2():
    global_variable = [2, 3, 4]   # local variable
    print(global_variable)
    # [2, 3, 4]
    print(hex(id(global_variable)))
    # 0x7f32763a4780

function2()
print(global_variable)
# [1, 2, 3]
print(hex(id(global_variable)))
# 0x7f32763f7880

In this example, when we set the value of global_variable inside function2 to [2, 3, 4], it actually creates a new local object to bind to in the scope of function2 and does not affect anything of the global global_variable. We can also use a built-in function id to verify that the two global_variable variables are different objects. (See the example output.)

Global Keyword

If a variable is assigned a value within a function in Python, it is a local variable by default. If we want to access a global variable within an inner scope such as function, we have to use the global keyword and explicitly declare the variable with it. See the example below:

Python
global_variable = [1, 2, 3]   # global variable

def function3():
    global global_variable
    global_variable = [3, 4, 5]
    print(global_variable)
    # [3, 4, 5]
    print(hex(id(global_variable)))
    # 0x7f32763a4780

function3()
print(global_variable)
# [3, 4, 5]
print(hex(id(global_variable)))
# 0x7f32763a4780

This time, the global keyword tells that the global_variable in function3 is binding to the global global_variable, and their addresses show they are the same object.

Besides, we can also use the global keyword to define a global variable from a function or inner scope.

Python
def function4():
    global new_global_variable
    new_global_variable = "A new global variable"
    print(new_global_variable)
    # A new global variable
    print(hex(id(new_global_variable)))
    # 0x7f32763a25d0

function4()
print(new_global_variable)
# A new global variable
print(hex(id(new_global_variable)))
# 0x7f32763a25d0

In function4, we define new_global_variable with the global keyword, and then we can access it from outside of function4.

Nested Function and Nonlocal Keyword

Python offers another keyword nonlocal that we can use in nested functions. As the rule of searching order for named variable states, the innermost scope will be searched first. Therefore, in a case with nested functions, the inner function cannot update the outer variable.

Python
def outer_function1():
    variable = 1

    def inner_function1():
        variable = 2
        print(f"inner_function: {variable}")

    inner_function1()
    print(f"outer_function: {variable}")

outer_function1()
# The output of the variable:
# inner_function: 2
# outer_function: 1

As we expected, the variable in inner_function1 is a different object than the variable in outer_function1.

Now, let’s use the nonlocal keyword. The keyword causes the variable to refer to the previously bound variable in the closest scope and prevent the variable from binding locally.

Python
 def outer_function2():
    variable = 1

    def inner_function2():
        nonlocal variable
        variable = 2
        print(f"inner_function: {variable}")

    inner_function2()
    print(f"outer_function: {variable}")

outer_function2()
# The output of the variable:
# inner_function: 2
# outer_function: 2

The variable in inner_function2 binds to the variable in outer_function2.

Global vs. Nonlocal

The main difference between global and nonlocal is that the nonlocal keyword enables access only to the next closest scope outside of the local scope, whereas the global keyword allows access to the global scope.

The following example has three-level nested functions, and we use the nonlocal keyword in the innermost level. The change of variable x in the innermost function only affects the variable x in the inner function, the next closest scope.

Python
x = "hello world"

def outer_nonlocal():

    x = 0

    def inner():

        x = 1

        def innermost():
            nonlocal x
            x = 2
            print(f"innermost: {x}")

        innermost()
        print(f"inner: {x}")

    inner()
    print(f"outer_nonlocal: {x}")

outer_nonlocal()
print(f"global: {x}")
# The output of x:
# innermost: 2
# inner: 2
# outer_nonlocal: 0
# global: hello world

Regarding the global keyword, the example using the global keyword in the innermost function enables access to the global variable y; the variables y in between (i.e., outer_global function and inner function) are not affected.

Python
y = "hello world"

def outer_global():

    y = 0

    def inner():

        y = 1

        def innermost():
            global y
            y = 2
            print(f"innermost: {y}")

        innermost()
        print(f"inner: {y}")

    inner()
    print(f"outer_global: {y}")

outer_global()
print(f"global: {y}")
# The output of y:
# innermost: 2
# inner: 1
# outer_global: 0
# global: 2

Conclusion

The scope is a fundamental concept of programming languages, and most of them work similarly. However, because of the way the assignment operator works in Python and the searching order for named variable rule, the Python scopes work very differently from C++ in some cases. Knowing this pitfall is critical to avoid writing bug code.

(All the example code is also available at variable_scope.)

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)