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.
#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.
# 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.
# 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.
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,
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.
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.
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.
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.
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.
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.
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)