Skip to main content
Python

Understanding the LEGB Rule for Variable Scope in Python

5 mins

local labeled circles within a large circle representing variable scope

What is Scope? #

In Python, scope refers to the visibility and lifetime of variables. It determines where a variable can be accessed or modified within the code. The various scope levels in Python can be summarized using the LEGB acronym.

  • Local scope: Variables defined within a function are local to that function and cannot be accessed from outside.
  • Enclosing scope: Variables defined in the enclosing function (if any) can be accessed by nested functions.
  • Global scope: Variables defined at the top level of a module or script are global and can be accessed from anywhere within that module.
  • Built-in scope: Python has a set of built-in functions and variables that are always available.

Understanding scope is crucial for writing clean and maintainable code, as it helps prevent naming conflicts and unintended side effects.

graph TD A[B: Built-in Scope] --> B[G: Global Scope] B --> C[E: Enclosing Scope] C --> D[L: Local Scope] A:::scope B:::scope C:::scope D:::scope classDef scope fill:#f9d6a5,stroke:#333,stroke-width:2px,color:#222

Local Scope #

When you define a variable inside a function, it is considered to be in the local scope of that function. This means that the variable can only be accessed and modified within that function.

def my_function():
    local_var = "I'm a local variable"
    print(local_var)  # This works  

my_function()
print(local_var)  # This will raise a NameError

Enclosing Scope #

Nested functions can access variables from their enclosing function’s scope.

def outer_function():
    outer_var = "I'm in the outer function"
    def inner_function():
        print(outer_var)  # This works
    inner_function()
outer_function()

Global Scope #

Variables defined at the top level of a module or script are in the global scope. They can be accessed from anywhere within that module.

global_var = "I'm a global variable"

def my_function():
    print(global_var)  # This works
my_function()
print(global_var)  # This also works

Built-in Scope #

Common built-in functions are always available such as print(), range(), etc.

print("Hello, World!")  # This works

Using globals() and locals() #

The globals() function returns a dictionary representing the current global variable table, while the locals() function returns a dictionary representing the current local variable table.

Whenever you are unsure about the scope of a variable, you can use these functions to help you inspect the current state.

global_var = "I'm a global variable"

def my_function():
    local_var = "I'm a local variable"
    print("Local variables:", locals())
    print("Global variables:", globals())

my_function()

Output:

Local variables: {'local_var': "I'm a local variable"}
Global variables: {'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <_frozen_importlib_external.SourceFileLoader object at 0x7f9c8c0c1d90>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'global_var': "I'm a global variable", 'my_function': <function my_function at 0x7f9c8c0c1ee0>}

Scopes with Nested Functions #

When you have nested functions, the inner function can access variables from the outer function’s scope. But also it has access to the global scope.

var_g = "I'm a global variable"

def outer_function():
    var_o = "I'm in the outer function"
    def inner_function():
        var_i = "I'm in the inner function"
        print(var_i)  # This will print the inner function's variable
        print(var_o)  # This will print the outer function's variable
        print(var_g)  # This will print the global variable
    inner_function()
    print(var_o)  # This will print the outer function's variable

outer_function()

print(var_g)  # This will print the global variable

Shadowing Variables #

When a variable defined in a local scope has the same name as a variable in an enclosing or global scope, the local variable “shadows” the outer variable. This means that within the local scope, the local variable takes precedence.

def my_function():
    var = "I'm a local variable"
    print(var)  # This will print the local variable

var = "I'm a global variable"
my_function()
print(var)  # outputs "I'm a global variable"

Typically, shadowing a global variable is usually accidental, and can lead to odd behavior in your code.

If you are using lots of global variables, a convention of prefixing them with g_ can help avoid confusion.

g_var = "I'm a global variable" 

The global Keyword #

If you need to modify a global variable inside a function, you can use the global keyword.

count = 0  # Global variable

def my_function():
    global count
    count += 1
    print("Inside function:", count)

my_function()
print("Outside function:", count)

Output:

Inside function: 1
Outside function: 1

The nonlocal Keyword #

If you want to modify a variable in the nearest enclosing scope (but not global), you can use the nonlocal keyword. This is useful in nested functions.

def outer_function():
    count = 0  # Enclosing variable
    def inner_function():
        nonlocal count
        count += 1
        print("Inside inner function:", count)
    inner_function()
    print("Inside outer function:", count)
outer_function()

Output:

Inside inner function: 1
Inside outer function: 1

Note: If you specify a variable as nonlocal, it must already exist in the nearest enclosing scope; otherwise, you’ll get a SyntaxError.

def outer_function():
    def inner_function():
        nonlocal count
        count += 1
        print("Inside inner function:", count)

SyntaxError: no binding for nonlocal 'count' found

This will this be an error even if there is a global variable with the same name.

Nested functions with nonlocal and global #

When using nested functions, you can use both nonlocal and global keywords to specify the required scope for variables.

def outer_function():
    var = "x"  # Enclosing variable

    def inner_function():
        nonlocal var
        def inner_function2():
            nonlocal var
            print("Inside inner function2:", var)

        def inner_function3():
            global var
            print("Inside inner function3:", var)

        inner_function2()
        inner_function3()

    inner_function()

outer_function()

Outputs:

Inside inner function2: x
Inside inner function3: y

Avoiding Global Variables #

Although we covered how to use the global keyword to help modify variables in the global scope, it’s generally best to avoid using global variables when possible.

Global variables can make code harder to track changes to the global state, leading to a maintenance nightmare when there are many global variables.

Here are some tips to avoid using global variables:

  • Use function parameters and return values to pass data between functions. Makes it easier to track data flow.
  • Encapsulate related data and behavior in classes. Reduces name clashes and puts related data together.
  • Use modules to group related functions and variables together. The module namespace helps avoid name clashes.
  • Limit the use of global variables to constants that do not change. E.g. Settings and defaults.