Scopes#
If we have a variable x
, that variable may
exist in more than one namespace. Some of these namespaces are
in stack frames, created when a function is called. Python
searches namespaces in a
particular order, which determines which x
it finds.
Python always searches in this order:
The local scope. This is the namespace in the top stack frame, which generally corresponds to the function that is currently executing. If a function is not executing (e.g., you are executing code within a source file that is not part of any function), then the local scope will be the namespace of the module (see below).
The enclosing scope. Later you may find it useful to define functions within other functions. For now the enclosing scope will be the same as the global scope.
The global scope. This is a namespace of the module (source file) of the code that is currently executing. It is where our functions and global variables live.
The builtin scope. This is a namespace for the builtin functions (and some other things) that are always available in Python. For example, when you type
len("abc")
, Python usually findslen
in the builtin scope.
This is called LEGB resolution order. To remember it, imagine that a football player’s dominant leg is “leg A”, and their other leg is “leg B”. A skilled football (soccer) player must practice kicking with leg B.
When a name exists in more than one of these scopes, Python uses the name it finds first. We say the other occurrences of the name are shadowed, i.e., covered up and hidden. For example:
# Shadow the built-in len function
def len(s: str) -> int:
"""Don't do this. Avoid shadowing built-ins!"""
return 21
def f(s: str) -> int:
return 2 * len(s)
print(f("Short string"))
42
Python looks first in the local namespace, but does not find len
.
There is no enclosing namespace, so it next looks in the global
namespace, and finds function len
defined in this module. It does
not look farther, so it uses the len
function defined in this
module instead of the built-in len
function.
It can be hard to remember and avoid all the built-in names. Try as I may to avoid it, I often accidentally shadow a built-in function with a variable. This can lead to confusing error messages.
len = 0 # Oops
def total_length(l: list[str]) -> int:
"""Total length of all the strings in l"""
tot = 0
for s in l:
tot += len(s)
return tot
print(total_length(["this", "that", "the other"]))
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
Input In [2], in <cell line: 0>()
7 tot += len(s)
8 return tot
---> 10 print(total_length(["this", "that", "the other"]))
Input In [2], in total_length(l)
5 tot = 0
6 for s in l:
----> 7 tot += len(s)
8 return tot
TypeError: 'int' object is not callable
When Python complains that some type is “not callable”, it is a strong clue that we have accidentally shadowed a function with a variable.
Which variables are local?#
Since a function can use both global variables and local variables, how can we tell which is which? If we assign a value to a variable (bind it) anywhere in a function, Python will create a variable with that name in the local scope of the function. If we use the variable but do not bind it within the function, Python will not create a local variable, so it will find a global variable instead. In other words, the local variables of a function are the variables that are bound within the function.
x = "<<global x>>"
y = "<<global y>>"
def f():
"""Access both local and global variables"""
y = "Something new" # y is bound here!
print(f"The value of x is '{x}' while executing f")
print(f"The value of y is '{y}' while executing f")
print(f"x is '{x}', y is '{y}' before calling f()")
print("======== Calling f ========")
f()
print("======== After call to f ====")
print(f"x is '{x}', y is '{y}' after calling f")
x is '<<global x>>', y is '<<global y>>' before calling f()
======== Calling f ========
The value of x is '<<global x>>' while executing f
The value of y is 'Something new' while executing f
======== After call to f ====
x is '<<global x>>', y is '<<global y>>' after calling f
Binding y
within f
makes y
a local variable
throughout f, even before the assignment that binds it. Notice
what happens if we move the assignment after the print statements:
x = "<<global x>>"
y = "<<global y>>"
def f():
"""Access both local and global variables"""
# assignment to y has not been made yet
print(f"The value of x is '{x}' while executing f")
print(f"The value of y is '{y}' while executing f")
y = "Something new" # y is bound here!
print(f"x is '{x}', y is '{y}' before calling f()")
print("======== Calling f ========")
f()
print("======== After call to f ====")
print(f"x is '{x}', y is '{y}' after calling f")
x is '<<global x>>', y is '<<global y>>' before calling f()
======== Calling f ========
The value of x is '<<global x>>' while executing f
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
Input In [4], in <cell line: 0>()
11 print(f"x is '{x}', y is '{y}' before calling f()")
12 print("======== Calling f ========")
---> 13 f()
14 print("======== After call to f ====")
15 print(f"x is '{x}', y is '{y}' after calling f")
Input In [4], in f()
6 # assignment to y has not been made yet
7 print(f"The value of x is '{x}' while executing f")
----> 8 print(f"The value of y is '{y}' while executing f")
9 y = "Something new"
UnboundLocalError: cannot access local variable 'y' where it is not associated with a value
Formal arguments are local variables#
When we call a function f
, we bind the values of actual
arguments to the formal arguments of f. These bindings create
local variables, just as if we had written an assignment statement
for each formal argument.
x = "<<global x>>"
y = "<<global y>>"
def g(x: str):
"""x will be bound by a call, and y by assignment,
so both x and y will be local variables.
"""
y = x
print(f"The value of x is '{x}' while g is executing")
print(f"The value of y is '{y}' while g is executing")
print(f"x is {x}, y is {y} before calling g(y)")
print("======== Calling g ========")
g(y)
print("======== After call to g ====")
print(f"x is '{x}', y is '{y}' after calling g(y)")
x is <<global x>>, y is <<global y>> before calling g(y)
======== Calling g ========
The value of x is '<<global y>>' while g is executing
The value of y is '<<global y>>' while g is executing
======== After call to g ====
x is '<<global x>>', y is '<<global y>>' after calling g(y)
Here x
is made local by binding of the actual argument to a local
argument, and y
is made local by binding in an assignment.
Although both x
and y
are local, they are both aliases of the
global y
.
Modifying an element does not bind the collection#
It may seem that setting an element of a list or other collection is an “assignment to” a variable, but it does not make a new binding to the variable itself.
m = [1, 2, 3, 4]
def g():
"""This function does NOT bind m;
it only modifies an element of m.
"""
m[2] = 37
print(f"Within g the value of m is now {m}")
print(f"Before g() the value of m is {m}")
g()
print(f"After g() the value of m is {m}")
Before g() the value of m is [1, 2, 3, 4]
Within g the value of m is now [1, 2, 37, 4]
After g() the value of m is [1, 2, 37, 4]
This can be confusing, but it makes sense if you think about the way
Python executes m[2] = 37
. First it finds m
, then it finds
element 2
of m
and changes that. m
is still associated with
the same reference to the same list, unchanged. The value of m
has been modified, but the binding of m
has not changed.
If we did not want to modify the global m
within g
, we could
avoid it by making a copy. Most collection types (list
, dict
,
and some others) have a copy
method.
m = [1, 2, 3, 4]
def g():
"""This function binds v
to a copy of the global m.
"""
v = m.copy()
v[2] = 37
print(f"Within g the value of v is now {v}")
print(f"Within g the value of m is still {m}")
print(f"Before g() the value of m is {m}")
g()
print(f"After g() the value of m is {m}")
Before g() the value of m is [1, 2, 3, 4]
Within g the value of v is now [1, 2, 37, 4]
Within g the value of m is still [1, 2, 3, 4]
After g() the value of m is [1, 2, 3, 4]
What would happen if we tried to make a copy with m = m.copy()
instead of introducing a new variable v
? It will not work!
Recall that binding a variable anywhere in a function makes it a
local variable everywhere in the function.
m = [1, 2, 3, 4]
def g():
"""This function binds attempts to bind
local m to a copy of global m, but it fails
because every reference to m is to the local m.
"""
m = m.copy()
m[2] = 37
print(f"Within g the value of m is now {m}")
g()
---------------------------------------------------------------------------
UnboundLocalError Traceback (most recent call last)
Input In [8], in <cell line: 0>()
9 m[2] = 37
10 print(f"Within g the value of m is now {m}")
---> 12 g()
Input In [8], in g()
3 def g():
4 """This function binds attempts to bind
5 local m to a copy of global m, but it fails
6 because every reference to m is to the local m.
7 """
----> 8 m = m.copy()
9 m[2] = 37
10 print(f"Within g the value of m is now {m}")
UnboundLocalError: cannot access local variable 'm' where it is not associated with a value
The local
m
shadows the global m
. Thus m = m.copy()
tries to make a copy of the local variable, which will fail because
the local variable hasn’t been given a value yet.
Default Arguments: A Binding Gotcha#
Occasionally we have to think carefully about scope and about the time of binding to avoid unintended effects. Some infamous Python “gotchas” result from unintended effects through shared bindings.
A Python function can have keyword arguments as well as positional arguments, and those keyword arguments can have default values. The keyword argument is bound to its default value just once, when the function is defined. This seems fine …
def stars(n: int, initial=[]) -> list[str]:
"""Should return list of n stars. Oops."""
if n == 0:
return initial
initial.append("*")
return stars(n-1, initial)
print(stars(3))
['*', '*', '*']
Handy! But watch out …
print(stars(2))
['*', '*', '*', '*', '*']
Although the formal argument initial
is local to function stars
,
the default value is bound to a mutable value whose scope is the
module in which the function appears. It is just as if we had written:
sneaky = []
def stars(n: int, initial=sneaky) -> list[str]:
"""Should return list of n stars. Oops."""
if n == 0:
return initial
initial.append("*")
return stars(n-1, initial)
print(stars(3))
print(stars(2))
['*', '*', '*']
['*', '*', '*', '*', '*']
Variable sneaky
is bound to an empty list just once, and then
that binding is copied to initial
each
time we call stars
without providing an explicit value for
initial
. Many an hour of
debugging has been spent diagnosing unexpected effects of such
shared bindings.
Recap#
The scope rules in Python are simple, but their consequences can be confusing. There are just two rules to remember:
Python searches for names in LEGB order, Local, Enclosing, Global, Built-in. (So far we have seen just LGM, but “LEG-B” is easier to pronounce and thus easier to remember.)
The Local variables of a function are those that have been bound through an explicit assignment or through argument passing.
Everything else follows from these two rules.