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.
#include <iostream>
int global_variable = 0;
int myFunction(int parameter=0)
{
int local_variable = 0;
if (parameter > 0)
{
local_variable += parameter;
}
else
{
local_variable += global_variable;
}
global_variable = local_variable;
return local_variable;
}
double myFunction2()
{
double global_variable = 1.23;
return global_variable;
}
void main()
{
std::cout << global_variable << std::endl;
std::cout << myFunction(10) << std::endl;
std::cout << global_variable << std::endl;
std::cout << myFunction2() << std::endl;
std::cout << global_variable << std::endl;
}
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.
condition = True
if condition:
result = 1
else:
result = 2
print(result)
Likewise, for
-loop (and while
-loop), with
-statement, and try
-except
do not define a scope either.
for i in range(10):
x = 1 + i
print(x)
with open("example.txt") as file:
data = file.read()
print(data)
try:
raise ValueError("Test exception")
except ValueError:
message = "Catch an exception"
print(message)
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]
def function1():
print(global_variable)
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]
def function2():
global_variable = [2, 3, 4]
print(global_variable)
print(hex(id(global_variable)))
function2()
print(global_variable)
print(hex(id(global_variable)))
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]
def function3():
global global_variable
global_variable = [3, 4, 5]
print(global_variable)
print(hex(id(global_variable)))
function3()
print(global_variable)
print(hex(id(global_variable)))
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)
print(hex(id(new_global_variable)))
function4()
print(new_global_variable)
print(hex(id(new_global_variable)))
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()
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 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}")
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}")
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.)