Skip to content

Understanding Python's LEGB Scope Hierarchy

Updated: at 04:23 AM

In Python, the scope of a name defines its visibility throughout the code. Python implements scoping rules that determine whether a variable is available for use at a given point in a program. Proper scoping helps avoid bugs and unexpected behavior by controlling name resolution.

Python uses the LEGB rule to resolve a name reference based on the location where it is assigned or defined. LEGB stands for Local, Enclosed, Global, and Built-in scopes. This tutorial will provide an in-depth explanation of Python’s LEGB scoping hierarchy with code examples.

Table of Contents

Open Table of Contents

Overview of Python Scope Rules

Understanding this fundamental concept helps avoid common errors like undefined variable and name masking problems.

The LEGB Scoping Hierarchy

Python resolves names using the LEGB hierarchy or scope chain. It starts from the Local scope and continues searching progressively outer levels if not found.

Local (L) Scope

The innermost scope which contains names defined inside a function or a method. This includes:

The local scope has priority access to these names within the function body. However, local names are not visible from outside the function.

# Example local scope
def f():
  x = 'local variable'
  print(x)

f() # Prints 'local variable'
print(x) # Error, x not defined

Enclosed (E) Scope

The scope immediately outside the local scope that contains outer local scopes. This includes:

The enclosed scope bridges the gap between local and global scopes. It allows enclosing functions to access names defined in nested functions while preventing conflicts with global names.

# Example enclosed scope

name = 'global'

def outer():
  name = 'enclosed'

  def inner():
    print(name) # Accesses 'enclosed' from outer function

  inner()

outer() # Prints 'enclosed'

Global (G) Scope

The top-most scope in a Python program that contains names defined at the module level. This includes:

Global names are visible from everywhere in a code. But it is read-only by default in functions to avoid side-effects.

# Example global scope

count = 0 # global scope

def increment():
  global count
  count += 1

increment()
print(count) # Accesses global count, prints 1

Built-in (B) Scope

The scope containing built-in names such as functions and exceptions in Python standard library.

# Built-in scope example

min = 10 # Custom min in global scope

def func():
  print(min([1, 2, 3])) # Uses built-in min()

func() # Prints 1

So in summary, the LEGB rule gives priorities to names in this order:

Names are resolved starting from local scope, then enclosed, global, and lastly built-in scope.

Python provides some built-in functions and methods that can be used to access scope-related information programmatically:

locals()

The locals() function returns a dictionary containing all names defined in the local scope. This includes local variables and function parameters.

# Accessing local scope dictionary

def func(x):
  y = 3
  print(locals())

func(1)

# Prints {'x': 1, 'y': 3}

globals()

The globals() function returns a dictionary containing all global names in a module or script. This includes global variables, functions, classes, etc.

# Accessing global scope dictionary

count = 0

def increment():
  globals()['count'] += 1

increment()
print(globals())

# Prints {'count': 1, 'increment': <function increment> ...}

vars()

The vars([object]) method returns the __dict__ attribute of a module, class, or object. This displays the namespace containing all names defined in that scope.

# Using vars() on objects

class Person:
  name = 'John'

print(vars(Person))

# Prints {'__module__': '__main__', 'name': 'John', '__dict__': <dictproxy...> ...}

These functions provide introspection capabilities to inspect names and scopes at runtime.

Some common errors related to Python scope include:

NameError

This error occurs when trying to access a name that does not exist in any accessible scopes. This may be due to typos, failure to declare names, etc.

# Example NameError

print(var) # NameError, 'var' is not defined

UnboundLocalError

This error occurs when trying to access a local variable in a function or method before it is defined.

# Example UnboundLocalError

def increment():
  print(count) # Error, local count not defined
  count += 1

To fix this, declare the local name or parameterize it.

TypeError

This error can occur when attempting to modify a global name without declaring it in the function using global.

# Example TypeError

count = 0

def increment():
  count += 1 # TypeError without global declaration

Modifying Global and Local Names

Here is an example demonstrating this behavior:

# Modifying global vs local names

count = 0 # global scope

def modify_global():
  global count
  count = 100 # Modifies global count

def modify_local():
  count = 200 # Implicitly creates local count

def outer():
  count = 10 # enclosed scope

  def inner():
    nonlocal count
    count = 30 # Modifies outer count

  inner()
  print('Enclosed count:', count)

modify_global()
print('Global count:', count)

modify_local()
print('Local count:', count)

outer()
# Prints:
# Global count: 100
# Local count: 0
# Enclosed count: 30

So nonlocal and global keywords explicitly declare that a variable belongs to the enclosing or global scope respectively.

Scope of Class Members

The scope of class attributes and methods is defined by the location where they are defined:

Class Body Scope

Attributes and methods defined in the class body are in the local scope of the class body block. These become members shared by all instances of that class.

# Class body scope example

class Person:

  species = 'Human' # Class attribute

  def __init__(self, name):
    self.name = name # Instance attribute

  def print_info(self): # instance method
    print(f"{self.name} is a {Person.species}")

Local Scope in Methods

Parameters and variables defined inside methods are in the local scope, similar to functions.

# Method local scope

class Person:

  def increment_age(self, age):

    age += 1 # Defined only in increment_age()
    print(f"New age is {age}")

So in summary, the same LEGB rule applies to name resolution in object-oriented code. Class members follow the same scoping principles but belong to the class/instance namespace.

Modifying Class Members

See example:

# Modifying class members

class Person:
  species = 'Human'

  def __init__(self, name):
    self.name = name
    self.age = 20

  def celebrate_birthday(self):
    self.age += 1 # Modifies instance attribute

p1 = Person('John')
p2 = Person('Sarah')

p1.celebrate_birthday()

print(p1.age) # 21
print(p2.age) # 20

Person.species = 'Martian' # Modifies class attribute

So instance attributes should generally be modified via self to prevent side-effects.

Benefits of Python Scope

Properly understanding Python scope hierarchy leads to cleaner and more modular code. Some key benefits are:

In summary, properly leveraging Python scope enables sturdy programs with loosely coupled components, avoiding nasty bugs.

Typical Use Cases by Scope Type

The different scope types lend themselves to certain common use cases:

Local Scope

Enclosing Scope

Global Scope

Built-in Scope

Understanding these conventions helps write idiomatic Python code.

Typical Errors Caused by Scoping Issues

Some common bugs and issues related to scoping include:

Carefully designing scope architecture and being aware of the LEGB hierarchy helps avoid these problems.

Best Practices for Scope Management

Here are some tips for managing scope well in Python:

Adopting these best practices makes programs more robust and maintainable.

Conclusion

Understanding Python’s LEGB scoping rule is essential for writing clean, modular, and bug-free programs. The scoping hierarchy enables controlled access to names based on their declared scope. Leveraging scope properly provides isolation between components, avoids unintended state changes, and improves encapsulation in large programs. Mastering scope also unlocks advanced Python features for decorators, factories, coroutines, and more. With disciplined scope management following conventions, Python developers can build stable and scalable applications.