Python Scoping

Chapter 10 14 mins

Learning outcomes:

  1. What is scoping
  2. The global and local scope
  3. Nested local scopes
  4. Variable resolution and the LEGB rule

Introduction

Working with variables is fundamental to every programming language, including Python. However, understanding where in a script what variables are accessible is fundamental to being able to work with variables.

The concept of where variables are accessible and where they're not is referred to as variable scoping, and it's what we'll be learning in this chapter.

Let's begin!

What is a scope?

We'll start by understanding the meaning of the term 'scope'. In simple words:

A scope is a region of code where a given set of name bindings is accessible.

A scope can be thought of as an area in a script where given variables are available.

Let's take a quick example. Consider the code below where we define a variable x at the start of the script:

x = 10
print(x) # 10

The variable x here is accessible everywhere in the script. This area, where x is defined, is known as the global scope.

Collectively all places in a Python script that are not part of a function, are referred to as the global scope.

We'll learn a lot more about the global scope below.

A variable defined in the global scope is available everywhere in a Python script — inside functions, inside statement blocks; simply everywhere.

When we talk about the scope of a variable, we refer to all the places where it's accessible.

In the code above, we would say that x is a globally-scoped variable, or simply that x is a global variable. This tells us that x is available in every single location throughout the script.

At the core level, Python has two types of variable scopes:

  1. The global scope
  2. The local scope

In the sections below we explore the bits and pieces to both these types, along with considering a handful of examples for each one.

The global scope

Every segment of a Python script, that's not a function's body, is part of the global scope.

A variable defined in the global scope, is formally known as a globally-scoped variable, or simply as a global variable.

As the name might suggest, a global variable is accessible everywhere in a program.

If we look up the meaning of the word 'global', we see that it means relating to the whole of something. In this case, it means exactly the same thing — that a variable is accessible throughout the whole script.

The opposite of 'global' is 'local'.

Most of the variable we've been creating so far were all global variables.

Let's see an example once again.

Here we define a variable x at the start of the script above. This variable is global, and likewise available everywhere throughout the script.

Let's confirm whether x is really available everywhere.

x = 10

# accessible here
print(x)

if True:
    # accessible inside a block
    print('Inside an if block', x)

def print_num():
    # accessible inside a function
    print('Inside a function:', x)

print_num()
10
Inside an if block: 10
Inside a function: 10

As is evident over here, the global variable x is available everywhere — directly in the script in line 4; inside the body of if in line 8; inside the function print_num() in line 12; and so on.

Moving on, although a very trivial detail but still worth discussion, a global variable is available everywhere in a script only after the point it is defined. It's not available before its definition.

Consider the code below:

if True:
    a = 5
    print(a) # 5

print(a) # 5

Here the variable a, defined inside the if statement, is indeed global — it's accessible everywhere in the script — but not before its definition. If we try to access it before line 2, we'll get an error.

This is illustrated below:

print(a)

if True:
    a = 5
    print(a)

print(a)
Traceback (most recent call last): File "stdin", line 1, in <module> print(a) NameError: name 'a' is not defined

The error message is pretty much self-explanatory — it says that the name a is not defined.

Keep this trivial, but useful, fact in mind when working with global variables. Just because it's said that global variables are accessible everywhere doesn't mean that they'll do their magic even before they're actually defined.

The local scope

The area of a function's body defines what's known as a local scope. Specifically, it's referred to as the local scope of the respective function.

A variable defined in a local scope is formally known as a locally-scoped variable, or simply as a local variable.

Contrary to a global variable, a locally-scoped variable is accessible only within the respective function it's defined in.

When we create a variable inside a function in Python, it becomes local to that function. Everywhere inside the function, the variable is accessible, however when we come out of the function, the variable no longer exists.

Consider the following code:

def say_hello():
    msg = "Hello programmer!"
    print(msg)

say_hello()

print(msg)
Hello programmer!
Traceback (most recent call last): File "stdin", line 7, in <module> print(msg) NameError: name 'msg' is not defined

The variable msg is defined inside the function say_hello() and is therefore local to it. The print() statement in line 3 works fine since msg is available inside the function.

However, the print() statement outside the function, in line 7, throws an error. This is because msg is local to say_hello() — it's not available anywhere outside the function.

All the local variables of a function are automatically deleted when the function completes. This is the reason why local variables aren't accessible after the function within which they're defined completes.

Nested local scopes

We just read above that local variables are accessible everywhere inside their container function.

This means that if we define a variable inside a function f(), then that variable would be accessible even inside a function h() defined within the function f().

Below we demonstrate an example:

def func1():
    x = 10
    print('Inside func1():', x)

    def func2():
        print('Inside func2():', x)
    
    func2()

func1()
Inside func1(): 10
Inside func2(): 10

A variable x is defined inside func1() and so it's local to func1() i.e available everywhere within it. Since func2() is also part of the function func1(), the variable x is available within func2() as well.

This is confirmed by the print() statement inside func2() in line 6. When we call func2(), it prints out the value of x without any sort of errors.

One thing to note here is that a variable defined inside func2() would be local to func2() only and thereby not accessible in the outer function func1().

This can be seen as follows:

def func1():
    def func2():
        y = 20
        print('Inside func2():', y)
    
    func2()
    print('Inside func1():', y)

func1()

We create a variable y inside func2() and then try to access to it from func1().

As expected, we get an error, since y is available only within func2() — outside, in func1(), no such variable y exists.

Inside func2(): 20
Traceback (most recent call last): File "stdin", line 9, in <module> func1() File "stdin", line 7, in func1 print('Inside func1():', y) NameError: name 'y' is not defined

Variable resolution via LEGB

When the Python interpreter encounters a variable in an expression, it has to resolve it with a given value or throw an error, if the variable doesn't exist. This whole process is known as variable resolution.

The term 'resolution' here comes from the word 'resolve'.

Given that there can be potentially a handful of scopes to look into,

Python follows the LEGB rule to determine what value to resolve a variable with when it's encountered, or whether to throw an error.

LEGB stands for Local, Enclosing, Global and Built-in.

This rule simply states the order of scopes in which Python searches for the existence of a given variable.

The LEGB rule is much simpler to understand with the aid of an example.

Consider the following code:

def func1():
    def func2():
        x = 'local'
        print(x)
    func2()

func1()
local

We're concerned with line 4, where we print x because this statement gets Python to resolve the name x with a value.

When print(x) is encountered in line 4 in the function func2(), search begins in the local scope of this function for x. Since a match is found in this scope, the variable x is resolved with the value of this match i.e 'local'.

Let's consider another example.

Everything here is the same as in the previous code except for the definition of x. Previously it was defined inside the function func2(), but this time it's defined in the outer function func1().

def func1():
    x = 'enclosing'

    def func2():
        print(x)
    func2()

func1()
enclosing

Once again, let's focus on how is x resolved for the statement print(x).

Search begins in the local scope of func2() based on the LEGB rule. No variable x is found here, likewise search shifts to the enclosing scope i.e inside func1(). Here a variable x is indeed found and so it's used to resolve the name x in line 5.

Let's see an example of resolution from the global scope:

x = 'global'

def func1():
    def func2():
        print(x)
    func2()

func1()
global

The print() statement is encountered in line 7, and consequently search begings to resolve the name x. The local scope of func2() fails to give a match for the name x, causing the search to shift to the enclosing scope i.e inside func1(). Even here nothing is found, and so search shifts to the global scope.

It's in the global scope that a variable x is found, and hence used to resolve the name x (in line 7).

If the global scope also fails to produce a match for a given name reference, Python finally looks into the built-in list of functions as the last resort to resolution. If the name exists here, it's resolved, otherwise a NameError is thrown.

Consider the following code:

def func1():
    def func2():
        print(str)
    func2()

func1()
<class 'str'>

As before, search begins in the local scope of func1() as soon as the name str is encountered in line 3. Nothing as such exists here, likewise search moves one step upwards — into the enclosing scope of func1(). Once more, nothing is found here causing the search to move another step upwards — out of func1(), into the global scope.

Even the global scope has no match for the name str, leaving the interpreter to look finally into the built-in list of names. Here a match for str is found and ultimately used to resolve the name str referred to in line 3.

As can be seen from all this long discussion, a variable's resolution occurs in a bottom-to-top approach. We start at the lowest level and then move sequentially upwards until we find a match or reach the end, at which point we throw a NameError.

This is Python's usual routine when it needs to resolve a given name with a value, or throw an error if it doesn't exist anywhere. Yes, that's right — this happens for every name that our program refers to!

Python performs the LEGB run in the blink of an eye and so we never notice the grunt work it goes through while resolving each of the names we use in our programs!