Python Scope

Python Scope

The scope of a name defines the area of a program in which you can unambiguously access that name, such as variables, functions, objects, and so on. An identifier will only be visible to and accessible in its scope and it is the scope that rules how identifiers are looked up in the code.

When you can access the value of a given name from someplace in your code, you’ll say that the name is in scope. If you can’t access the name, then you’ll say that the name is out of scope. Python will raise NameError if you try to access names which are out of scope.

Python scopes are implemented as dictionaries that map names to objects. These dictionaries are commonly called namespaces. The terms may differ in other languages but the concept will be similar. For example, JavaScript too implements the scope as dictionaries but calls it the "Execution Context ".

Several programming languages, including Python, resolves names using the LEGB rule, where LEGB stands for Local, Enclosing, Global, and Built-in scopes. Let’s have a quick overview of what these terms mean. We will also look at an internal representation of the scopes as we go through each.

1. Local Scope

This scope contains the names that you define inside the function. These names will only be visible from the code of the function. It’s created at the function call, not at the function definition. If you are using a recursive function, then each call will result in a new local scope being created. There is a built-in function locals() to get the local dictionary. But we will be using the dir() function, which lists all the names in the current scope. I find it more flexible, so we will be using it for all the examples 😉.

def square(num):
    result = num * num
    print(dir()) # ['num', 'result']
    print(num, result)

square(2)        # 2 4

2. Enclosing Scope

It is a special scope that only exists for nested functions. Let's say you have an inner function defined within a function, then the enclosing scope contains the names that you define in the outer function. So, basically, the outer function's local scope encloses the inner function and hence it will become the enclosed scope for the inner function.

def outer():
    outer_value = 5
    def inner():
        inner_value = 10
        print(inner_value, outer_value) # 10 5
        print(dir()) # ['inner_value', 'outer_value']
    inner()

outer()

From the example, we can see that the names defined in the outer function are also available in the inner function's dictionary, in addition to its local names. This forms what is called closure.

3. Global Scope

The global scope also called module scope, contains all of the names that you define at the top level of a program, including imports. Names in this scope are visible everywhere in your code. There is a built-in function globals() to get the current global dictionary, but as I mentioned earlier, we will stick with our dir().

import math

print(math.sqrt(16)) # 4.0

# ..... code from above examples

print(dir())
# ['__builtins__', '__name__', ... , 'math', 'outer', 'square']

We can see our top-level functions: square and outer and the imported module: math in the global dictionary. There are many more names available in the global dictionary. __name__ for example, the name of the module; these are added automatically by Python when we run the program.

4. Built-in Scope

This is a special scope that contains names such as keywords, functions, exceptions, and other attributes that are built into Python. Names in this Python scope are also available from everywhere in your code and are loaded by Python when you run a program.

If we look at the names list from the previous example, we see it has a name called __builtins__ . So, global dictionary has a key called __builtins__; let's have a look at its value:

print(globals()['__builtins__']) # <module 'builtins' (built-in)>

Ohh! it's a module called builtins. But wait, we imported the math module alone, so who imported this "builtins" module? and why 🤔?! Anyways, let's have a look at its namespace:

import builtins

print(dir(builtins))    # ['print', 'len', 'filter', ...]

builtins.print("Hello") # Hello

Ok. So the built-in functions like print, len etc. are actually defined in this builtins module. So, what happened was that, while we ran the program, Python under the hood, imported this module into __main__, which added it to our global dictionary under the key __builtins__ so that we could use all these built-in functions from anywhere in the code. Cool 😌.

If you look into any of our function objects, we can see there exists a property on them called __globals__ which links back to the global dictionary. This enables us to access global names from inside the function. (We will explore this further in the last section of this article)

print("__globals__" in dir(square))        # True

print(square.__globals__['math'].sqrt(25)) # 5.0

But, when we imported the math module, we used the square root function as math.sqrt() , so how come we could use the built-in functions directly, when len, print and all were not keys in the global dictionary 🤔?!

Name Resolution from Scopes

Whenever we use a name, such as a variable or a function name, Python searches through different scope levels (or namespaces) to determine whether the name exists or not. As mentioned earlier in the article, Python uses the LEGB rule for name resolution. The LEGB rule is a kind of name-lookup procedure, which determines the order in which Python looks up names. For example, if you reference a given name, then Python will look that name up sequentially in the local, enclosing, global, and built-in scope. If the name exists, then you’ll get the first occurrence of it. Otherwise, you’ll get an error.

From our examples, we know that the inner function can access the outer function's local scope through the enclosing/nonlocal scope. And every function is linked to the global scope through __globals__, which in turn has access to the built-in scope through __builtins__. This link forms a chain, which is called the scope chain. Name resolution happens with the help of this scope chain, starting from local till built-in.

import math

PI = math.pi

showArea  = lambda area: print(f"Area: ", area)

def area(radius):
    print = showArea
    def calculate():
        PI = 6
        print(PI * radius * radius)
    calculate()

area(1)

Now that you know the scope chain and resolution order, can you explain how the identifiers: PI, radius and print got resolved in the calculate function? Also, can you predict the output of this program?

Modifying the Behaviour of Python Scope

Consider the below program:

global_val = 1

def outer():
    outer_val = 2
    def inner():
        # I intent to update the outer_val from enclosing scope 
        outer_val = 3
        print(outer_val)  # 3
    inner()
    print(outer_val)      # 2
    #  I intent to update global_val from global scope   
    global_val = 4
    print(global_val)     # 4

outer()
print(global_val)         # 1

Noticed anything weird 🧐?

  • We expect the outer_val to be 3 after inner() is called, but we see that even though other_val was giving value 3 inside inner(), that change was not reflected outside inner().

  • Similarly, we saw the change in global_val inside outer(), when it was called. But afterwards, global_val was still giving its initial value in global.

So what exactly is happening here 🤯?

If we assign some value to a variable inside any scope:

  • If that name is already present in that scope, the value will get updated

  • Otherwise, it would declare that variable in that scope

So, when we did outer_val = 3 inside inner(), it actually declared a variable called outer_val with value 3 in the local scope of inner(). It was not updating the other_val from outer()'s scope. In order to alter this normal behaviour: before using that name, we should inform Python that we intend to use the name not from its local scope, but from the enclosed scope. We can do that with the nonlocal keyword. Similarly, for global names, we can make use of the global keyword. With this information, let's try to fix our code and see if we get the desired results:

global_val = 1

def outer():
    outer_val = 2
    def inner():
        nonlocal outer_val
        outer_val = 3
        print(outer_val)  # 3
    inner()
    print(outer_val)      # 3  
    global global_val
    global_val = 4
    print(global_val)     # 4

outer()
print(global_val)         # 4

Voilà.. 🪄 we modified the normal behaviour of Python scope !! 🤩


Well, that’s all for this article. Thanks for reading! 🙏

I hope you liked this article and if you have any questions or suggestions, please drop them in the comments. Happy Coding.. 👋