Article

Python Scopes: Global and Nonlocal Variables Explained


What Are Scopes in Python? Understanding Local, Global, and Built-in Variables

In Python, when we write x = "devcuriosity" we create a variable x assigned to the string object - devcuriosity.

We can say that a variable is bound to an object. That variable only exists in parts of our code.

The region of the code where a variable is defined and accessible is known as its lexical scope. Lexical scopes are managed through namespaces, which are mappings between variable names and the objects they reference.


The LEGB Rule: How Python Resolves Variable Names in Different Scopes

Python follows the LEGB rule to resolve variable names:

  • Local Scope: Names defined within a function.

  • Enclosing Scope: Names defined in any enclosing functions.

  • Global Scope: Names defined at the top-level of a module or script.

  • Built-in Scope: Names preassigned in Python, such as len or print.

Built-in Scope
├── Global Scope (Module 1)
│   └── Local Scope (Function 1 in Module 1)
├── Global Scope (Module 2)
│   └── Enclosing Scope (Function 1 in Module 2)
│       └── Local Scope (Inner Function 1 in Function 1 in Module 2)

If you reference a variable name inside scope and Python does not find it in that scope's namespace it will look for it in an enclosing scope's namespace.

That is why IDEs are showing a warning if you use by accident a variable name that belongs to Python built-ins or key-words. For example:

len = "that is a mistake"  # Overriding the built-in len() function

my_string = "test"

len(my_string)  # Attempting to call the overridden len

Output:

(...) TypeError: 'str' object is not callable

Using len as a variable name, we have overridden the original Python len() function.

If you try to access a variable not in the current namespace, Python will try to look for it in an outer namespace. If it doesn't find it anywhere, you will receive NameError.


Local Scope in Python: Accessing and Using Local Variables

When we create a variable inside a function, it resides in the function local scope. This means the variable is only accessible within that function and cannot be accessed from outside.

If we call built-in Python scope "2" and our function local scope as a "1". Then we can say that "1" has access to "2". But "2" cannot access "1".

If we consider the built-in Python scope as the outer scope and our function's local scope as the inner scope, we can say that the inner scope has access to the outer scope. However, the outer scope cannot access variables from the inner scope.

Take a look at this:

def my_function(name: str) -> str:
    greetings = "Hello"  # greetings is local to my_function

    return f"{greetings} {name.capitalize()}!"

print(my_function("nathan"))  # Hello Nathan!

print(greetings)  # Raises NameError: name 'greetings' is not defined

Output:

Hello Nathan!

(...) NameError: name 'greetings' is not defined

From the local scope of my_function we could access Python's capitalize() function and greetings variable that we created in my_function function. But we cannot access greetings outside of my_function.

That is because it exists only in the local scope of my_function.

But remember that Python will "stop" at the first variable that it will find. Consider this example:

greetings = "Welcome"

def my_function(name: str) -> str:
    greetings = "Hello"
    print(greetings)  # Hello
    return f"{greetings} {name.capitalize()}!"

print(my_function("nathan"))  # Hello Nathan!

print(greetings)  # Welcome

Output:

Hello
Hello Nathan!
Welcome

We can say that we have 3 scopes here: - Python built-in scope (the biggest) - Our module scope (that exist in our .py file) - Local scope of our function my_function

The greetings variable on module scope is the Welcome string. And in the local scope of function my_function the greetings is Hello.

Please notice that Hello didn't override Welcome. We were accessing the same name in two places, but each time Python "found" different variable bound to that name in a given namespace.


Global Variables in Python: Using the global Keyword

We can change behavior from before with a global keyword. The global keyword allows a function to modify a variable defined in the global scope. By declaring a variable as global within a function, you inform Python that you intend to use the globally scoped variable, not create a new local one.

greetings = "Welcome"  # Global scope

def my_function(name: str) -> str:
    global greetings
    print(greetings)  # Outputs: Welcome (global variable)
    greetings = "Hello"
    print(greetings)  # Outputs: Hello (modified global variable)

    return f"{greetings} {name.capitalize()}!"

print(my_function("nathan"))  # Hello Nathan!

print(greetings)  # Outputs: Hello (global variable modified)

Output:

Welcome
Hello
Hello Nathan!
Hello

We modified our previous example, and added the line global greetings. With that, we let Python know that we want to access greetings from global scope, instead of creating a new variable in my_function local scope.


What Is the nonlocal Keyword in Python? Accessing Enclosing Scopes

By using nonlocal, we tell Python to use or modify a variable from the nearest enclosing scope that is not the global scope. This allows a nested function to modify a variable defined in its enclosing function.

Note that nonlocal cannot be used to access global variables.

Take a look at the example below. I added more prints and comments next to them to make everything more readable.

greetings = "Welcome"

def my_function(name: str) -> str:
    greetings = "Hello"
    print(greetings)  # Hello

    def inner_change_greeting():
        nonlocal greetings
        greetings = "Hi"

    inner_change_greeting()
    print(greetings)  # Hi

    return f"{greetings} {name.capitalize()}!"

print(my_function("nathan"))  # Hi Nathan!

print(greetings)  # Welcome

Output:

Hello
Hi
Hi Nathan!
Welcome

Best Practices for Using Global and Nonlocal Variables in Python

  • Avoid Overusing global: Modifying global variables can lead to code that is difficult to read, debug and maintain. Instead, consider returning values from functions and passing them as parameters.

  • Use nonlocal Sparingly: While nonlocal might be useful in certain scenarios, excessive use can complicate the scope chain, making the code significantly harder to follow.

Be cautious with mutable types. When dealing with mutable types (like lists or dictionaries), modifications within a function can affect the variable outside the function even without global or nonlocal.

I hope this article was helpful to you! If you have any feedback or questions, please feel free to reach out to me through the "Contact" page.

Thank you for reading!

I hope you found this helpful! If you have any feedback, questions or requests, please reach out to me through Contact page!

Thank You Image


You might also be interested in: