Scope of variables¶

This notebook covers roughly the same ground as this video.

Every variable in Python has a scope, which determines where the variable can be used and retains its meaning. Most of the variables that we have seen so far have global scope: they can be used and retain their value in most places. However, if we introduce a variable as part of a function definition, then it scope will be restricted to that function.

In the example below, x has global scope: we can use it both inside and outside the function f(a). However, the scope of b is local to the function f(a); we cannot use it outside the function, so the last line below produces an error message. Specifically, it produces a NameError, which is the standard thing when we try to refer to a variable that does not exist. Although the error here is easy to understand, you should read the error message carefully to start learning about how to interpret such messages.

In [ ]:
x = 111 # This has global scope
print(f'Outside the function, x={x}')

def f(a):
    b = 2 * a # Here we introduce a new variable with local scope
    print(f'Inside the function, b={b}')
    print(f'Inside the function, x={x}')
    return 3 * b

f(100)

print(f'Outside the function, we still have x={x}')
print(f'Outside the function, b={b}') # This will fail

Here is a more complicated example where we have a global variable called x and a local variable of the same name. Inside the function, x refers to the local variable; outside the function, x refers to the global variable.

In [ ]:
x = 111111 # This has global scope
print(f'Outside the function, x={x}')

def f(a):
    # Here we introduce a new variable with local scope, which shadows the global x
    # The assignment x = 2 * a changes the value of the local variable x, but not the global one
    x = 2 * a 
    print(f'Inside the function, x={x}')
    return 3 * x

f(100)

print(f'Outside the function, we still have x={x}')

You might want to change the value of a global variable from within a function. This can be done by declaring the variable to be global in the function body. However, this is rarely a good idea. Especially in large projects with many interacting pieces, this practice usually leads to code that is hard to understand and test and debug.

In [ ]:
x = 1234 # This has global scope

def multiply_x_by(a):
    global x
    print(f'The global value of x is currently {x}')
    # Because x has been declared global, the next line will change the 
    # global value of x instead of introducing a new local variable x
    x = a * x 
    print(f'The global value of x has now changed to {x}\n')

multiply_x_by(2)
multiply_x_by(1000)

print(f'Outside the function, we still have x={x}')

There is a further wrinkle involving mutable objects. Suppose that l is a global variable whose value is a list. Inside a function we could do something like l.append(42), to add 42 on the end of the list. This does not count as introducing a new local variable; it just mutates the list referred to by the global variable l (even without a global declaration). This is similar to the last example and is discouraged for the same reasons.

In [ ]:
l = [1,2,3,4]

def add_the_answer_to_l():
    l.append(42)

print(f'Originally             : {l=}')
add_the_answer_to_l()
print(f'After the function call: {l=}')

As well as local and global variables, we also have function arguments. These mostly behave like local variables. However, if a function argument is a mutable object like a list, then the function can mutate it (and this is not discouraged). An example is given below.

In [ ]:
def add_aardvark(x):
    x.append('aardvark')

u = ['elephant', 'zebra']

print(f'Originally             : {u=}')
add_aardvark(u)
print(f'After the function call: {u=}')