8 minutes read

A scope is a part of the program where a certain variable can be reached by its name. The scope is a very important concept in programming because it defines the visibility of a name within the code block.

Global vs. Local

When you define a variable, it becomes either global or local. A variable is considered global if it's defined at the top level of a module. This means you can refer to this variable from any part of your program. Global variables can be useful when you need to share state information or configuration between different functions. For example, you can store the name of the current user in a global variable and then use it where needed. This approach makes your code easier to modify: to set a new username, you only need to change a single variable.

Local variables are created when you define them within the body of a function. Their names can only be resolved inside the current function's scope. This localization helps you avoid issues with side effects that may occur when using global variables.

Consider the following example to see the difference between global and local variables:

phrase = "Let it be"

def global_printer():
    print(phrase)  # we can use phrase because it's a global variable

global_printer()  # Let it be is printed
print(phrase)  # we can also print it directly

phrase = "Hey Jude"

global_printer()  # Hey Jude is now printed because we changed the value of phrase

def printer():
    local_phrase = "Yesterday"
    print(local_phrase)  # local_phrase is a local variable

printer()  # Yesterday is printed as expected

print(local_phrase)  # NameError is raised

Thus, a global variable can be accessed both from the top level of the module and from within function bodies. On the other hand, a local variable is only visible inside its nearest scope and cannot be accessed from outside that scope.

LEGB rule

Variable resolution in Python follows the LEGB rule. This means that the interpreter looks for a name in the following order:

  1. Locals: Variables defined within the function body and not declared global.

  2. Enclosing: Names in the local scope of all enclosing functions, from inner to outer.

  3. Globals: Names defined at the top level of a module or declared global with the global keyword.

  4. Built-in: Any built-in name in Python.

Let's consider an example to illustrate the LEGB rule:

x = "global"
def outer():
    x = "outer local"
    def inner():
        x = "inner local"
        def func():
            x = "func local"
            print(x)
        func()
    inner()

outer()  # "func local"

When the print() function inside func() is called, the interpreter needs to resolve the name x. It will first look at the innermost scope and search for a local definition of x in the func() function. In the case of the code above, the interpreter will successfully find the local x in func() and print its value, 'func local'. You can visualize this process to see how it works almost in real-time.

But what if there isn't a definition of x in func()? Then, the interpreter will move outward and look in the inner() function. Consider the following example:

x = "global"
def outer():
    x = "outer local"
    def inner():
        x = "inner local"
        def func():
            print(x)
        func()
    inner()

outer()  # "inner local"

As you can see, the name x was resolved in the inner() function, since the value "inner local" was printed.

If we remove the definition of x from the inner() function as well and run the code again, the interpreter will continue the search among the outer() function's local variables in the same manner. If we keep deleting the lines of code defining x, the interpreter will move on to outer() locals, then globals, and finally built-in names. If there is no matching built-in name, an error will be raised.

Let's look at an example where the interpreter reaches the global definition of x:

x = "global"
def outer():
    def inner():
        def func():
            print(x)
        func()
    inner()

outer()  # "global"

Don't forget about LEGB rule if you plan on using enclosing functions.

Keywords "nonlocal" and "global"

We have already mentioned one way to assign a global variable: define it at the top level of a module. However, there is also a special keyword global that allows us to declare a variable as global inside a function's body.

You can't change the value of a global variable inside a function without using the global keyword:

x = 1
def print_global():
    print(x)

print_global()  # 1

def modify_global():
    print(x)
    x = x + 1

modify_global()  # UnboundLocalError: local variable 'x' referenced before assignment, line 8

The error is raised because, to execute print(x) on the 8th line, the interpreter tries to resolve x and finds it in the local scope – the local x is declared on the next line (9th), i.e., after its use on line 8, so the interpreter raises the error. However, even if we removed line 8, the same error would occur. In that case, to execute x = x + 1, the interpreter would need to compute the expression x + 1 and resolve the variable x in it. However, x is declared in the same line (remember, the interpreter checks the local scope first). Since its value would be needed before it had actually been computed, the interpreter would raise the same error:

UnboundLocalError: local variable 'x' referenced before assignment

To fix this error, we need to declare x as global:

x = 1
def global_func():
    global x
    print(x)
    x = x + 1

global_func()  # 1
global_func()  # 2
global_func()  # 3

When x is global you can increment its value inside the function.

nonlocal keyword lets us assign to variables in the outer (but not global) scope:

def func():
    x = 1
    def inner():
        x = 2
        print("inner:", x)
    inner()
    print("outer:", x)

def nonlocal_func():
    x = 1
    def inner():
        nonlocal x
        x = 2
        print("inner:", x)
    inner()
    print("outer:", x)

func()  # inner: 2
        # outer: 1

nonlocal_func()  # inner: 2
                 # outer: 2

Though global and nonlocal are present in the language, they are not often used in practice, because these keywords make programs less predictable and harder to understand.

Why do we need scopes?

Python distinguishes between global and local scopes to enhance code organisation. Global scope allows retaining information between function calls, aiding data transfer and communication in complex processes like multithreading. However, if all the declarations were stored in a global scope, the namespace would be extremely clogged up and hard to navigate, which may lead to confusion and bugs. Therefore Python saves you the trouble by allowing you to "isolate" some variables from the rest of the code when you split it into functions.

Summary

In this topic, we've found out the difference between global and local variables and "nonlocal" and "global" keywords, learned the LEGB rule, and how scopes can be helpful in practice. Hope you'll find this new knowledge useful!

1608 learners liked this piece of theory. 123 didn't like it. What about you?
Report a typo