Namespaces, scopes, & side effects

Unit 09

Namespaces and scopes

This Section is adapted from Sebastian Raschka’s beginner’s guide to python namespaces and scopes.

In Python, a namespace is a container where names (e.g., variable names, function names, etc.) are mapped to objects. In the example

variable_name = 'object'

variable_name is mapped to the string object. This mapping is stored in a namespace (implemented as a dictionary, a_namespace = {'variable_name':'object', ...}). The same name can only be used once per namespace, but can exist in different namespaces without conflict. For example, there is the global namespace of your python script or notebook that contains the names of all loaded modules, assigned variables, and defined functions. Each time a new function is called a local namespace includes all local names inside the function. The local namespace only lasts until the function returns.

Scope refers to the region of the code where a particular variable is accessible. A variable’s scope is determined by where it is defined. The concept of scope is closely related to namespaces in Python. While the namespace defines the mapping from names to objects, the scope determines the visibility and accessibility of these names in various parts of your code.

Python will first try to find a variable name in the local scope. This can, for example, be within the current function defition or the current for-loop. If it doesn’t find the name in the local scope, it searches for the name in the enclosed scope, which could be a function enclosing another function or a loop. If there is no enclosing scope, or if the variable name is not defined in the enclosing scope either, it searches in the global scope. The global scope refers to the uppermost level of the current script or notebook or module. The last scope where python checks is the built-in scope, where for example the function len is defined.

For example,

# Global scope
a = 'global variable'

def a_func():
    # Local scope
    b = 'local variable'
    print(a, '[ a inside a_func() ]')  # can access global variable
    print(b, '[ a inside a_func() ]')  # can access local variable

a_func()
print(a, '[ a outside a_func() ]')  # can access global variable
## the following would cause a NameError, because `b` is not known in global scope
print(b, '[ b outside a_func() ]')  # can not access local variable from different scope

Brain twisters

Can you guess what the following code snippets will produce?

  1. Let’s define a local variable with the same name as a global variable
a = 'global value'

def a_func():
    a = 'local value'
    print(a, '[ a inside a_func() ]')

a_func()
print(a, '[ a outside a_func() ]')

Before you read the explanation, run the code block yourself and verify its output.

When we call a_func(), it will first look in its local scope for a. Since a is defined in the local scope of a_func, its assigned value local value is printed. Note that this doesn’t affect the global variable, which is in a different scope.

  1. Following the same logic, let’s briefly add an enclosing scope. What does the code snippet produce?
a = 'global value'

def outer():
    a = 'enclosed value'

    def inner():
        a = 'local value'
        print(a)

    inner()

outer()

Before you read the explanation, run the code block yourself and verify its output.

Let us quickly recapitulate what we just did: We called outer(), which defined the variable a locally (next to an existing a in the global scope). Next, the outer() function called inner(), which in turn defined a variable with the name a as well. The print() function inside inner() searched in the local scope first, and therefore it printed the value that was assigned in the local scope.

Side effects

We will stay with the same topic, but move from scalar variables to containers, here: lists and tuples. Can you still remember their difference?

a_list = ['global', 'list']
a_tuple = ('global', 'tuple')

def a_func(a_list, a_tuple):
    print(a_list, 'input to a_func')
    print(a_tuple, 'input to a_func')
    # a_list = a_list.copy()
    a_list[0] = 'local'
    a_tuple = ('local', 'tuple')
    print(a_list, 'modified in function scope')
    print(a_tuple, 'modified in function scope')
    return a_list, a_tuple

a_func(a_list, a_tuple)
print(a_list, 'the end')
print(a_tuple, 'the end')

Please take a few moments to fully understand what exactly is going on here: We define a_list and a_tuple in the global scope and use the two variables as inputs to a function. Within the function, the first element of a_list is modified. Since lists are mutable, this change affects a_list outside the function as well. Since tuples are immutable, the function cannot change a single element of a_tuple, but instead creates an entire new tuple with the same name. Since this assignment happens within the function, it only affects the local scope, but not the original a_tuple outside the function. After the function call, a_list outside the function reflects the changes made inside the function, while a_tuple remains unchanged.

Side effects

A function that modifies its input parameters is said to have side effects. Avoid writing functions with side effects. Try to make functions pure, meaning their outputs should only depend on their inputs, and they shouldn’t modify their inputs. Instead of modifying an input parameter, consider creating a new object within the function and returning it. This makes your functions more predictable and less prone to bugs.

Copy and deepcopy
  • Use the copy() method to create a new object which is a copy of the original object. For example, uncomment the line a_list = a_list.copy() in the code example above and verify the different outcome.
  • copy() only creates a shallow copy of your object. When you are dealing with complex data structures like lists of lists or dictionaries containing lists, you will have to create what’s called a deep copy. deepcopy() from the copy module recursively copies all nested objects.
from copy import deepcopy
a_nested_list = deepcopy(a_nested_list)

Learning checklist

  • I have a fundamental understanding of the concept of namespace and scope in python.
  • I will avoid writing functions with side effects, by creating copies (or deep copies) of input parameters that the function mutates.