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
- The LEGB Scoping Hierarchy
- Scope Related Built-in Functions
- Scope Related Errors
- Modifying Global and Local Names
- Scope of Class Members
- Modifying Class Members
- Benefits of Python Scope
- Typical Use Cases by Scope Type
- Typical Errors Caused by Scoping Issues
- Best Practices for Scope Management
- Conclusion
Overview of Python Scope Rules
-
Scope refers to the visibility of variables and other names in different parts of a Python program.
-
The LEGB rule defines how Python looks up names during execution.
-
Python resolves names based on the location where they are assigned or defined.
-
Names have the smallest scope possible. The scopes are searched in the given LEGB order.
-
Code in the inner scope does not see names assigned in the outer scopes.
-
However, outer scopes have access to names in the inner scopes following the LEGB rule.
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:
- Function parameters
- Local variables declared inside a function
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:
- Enclosing functions for nested functions
- Names that are not local but referenced in inner functions
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:
- Names imported from other modules
- Global names defined in a file or interactive interpreter
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 names are automatically available for all Python code without needing to import.
-
Python searches this last if a name is not found in the other narrower scopes.
# 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:
- Local (Innermost)
- Enclosed (Outer local)
- Global (Module)
- Built-in (Python predefined)
Names are resolved starting from local scope, then enclosed, global, and lastly built-in scope.
Scope Related Built-in Functions
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.
Scope Related Errors
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
-
By default, functions cannot modify global names. Trying to assign to a global implicitly creates a new local name instead.
-
To modify global names, use the
global
keyword inside functions. -
The
nonlocal
keyword is used to modify variables in the enclosing scope instead of global or local.
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
-
Class attributes are shared by all instances. Modifying a class attribute changes it globally.
-
Instance attributes are independent for each instance. One instance can modify its attributes without affecting others.
-
Use
self
keyword to modify instance attributes and avoid modifying class attributes.
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:
-
Avoid unintended closure side-effects by using appropriate scope
-
Prevent accidental modifications using global and nonlocal
-
Improve encapsulation and reduce coupling between parts
-
Reuse names safely without collisions
-
Write reentrant code that holds no state within inner scopes
-
Hide information using closures for abstraction and data hiding
-
Improve readability, debugging, and maintenance for large programs
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
- Temporary names like loop variables and temporary results
- Local constant values with reuse in a function
- Private helper functions within code blocks
Enclosing Scope
- Factory functions creating closures
- Decorators modifying subroutine behavior
- Coroutines with state preserved between calls
Global Scope
- Constants, configurations or options
- Shared utility functions or classes
- Globally accessible program state
Built-in Scope
- Standard library modules like collections/math
- Built-in functions open()/print()
- Common exceptions like TypeError/ValueError
- Special constants like file/name
Understanding these conventions helps write idiomatic Python code.
Typical Errors Caused by Scoping Issues
Some common bugs and issues related to scoping include:
- Accessing uninitialized local variables
- Unintended closure behaviors
- Name collisions between scopes
- Modifying global state in functions
- Overwriting built-ins with globals
- Modifying class attributes instead of instance
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:
- Use local scope for temporary names and avoid global mutable state
- Declare globals explicitly when modifying from functions
- Use
nonlocal
for mutable enclosing state instead of globals - Avoid global and built-in name collisions/shadowing
- Access enclosing scope sparingly with closures when needed
- Use class/instance attributes appropriately
- Limit scope leakage from closures using techniques like closures over classes
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.