Skip to content

LEGB Resolution

Four Scopes

1. Local (L)

def function():
    x = 10  # Local

2. Enclosing (E)

def outer():
    x = 10  # Enclosing for inner
    def inner():
        print(x)

3. Global (G)

x = 10  # Global

def function():
    print(x)

4. Built-in (B)

# Always available
print(len([1, 2, 3]))

Lookup Order

Searches: L → E → G → B

Accessing Namespaces

1. globals()

Returns the global namespace dictionary:

x = 42
print(globals()['x'])  # 42

2. locals()

Returns the local namespace dictionary:

def f():
    a = 10
    b = 20
    print(locals())  # {'a': 10, 'b': 20}

3. __builtins__

Contains built-in names:

import builtins
print(dir(builtins))  # ['abs', 'all', 'any', ...]

4. No enclosings() Function

There is no built-in to access enclosing scope directly. Enclosing variables are captured in closures via __closure__:

def outer():
    x = 10
    def inner():
        return x
    return inner

f = outer()
print(f.__closure__[0].cell_contents)  # 10

Namespace Summary

Namespace Access Method Modifiable?
Local locals() No (snapshot)
Enclosing __closure__ Via nonlocal
Global globals() Yes
Built-in builtins module Not recommended

Summary

  • Four scope levels
  • Specific lookup order
  • First match wins

Runnable Example: legb_examples.py

#!/usr/bin/env python3
"""
LEGB Rule - Practical Examples
==============================

This file contains 15 practical examples demonstrating Python's LEGB rule.
Run this file to see all examples in action.

Usage: python 02_legb_examples.py
"""

if __name__ == "__main__":

    print("=" * 70)
    print("PYTHON LEGB RULE - 15 PRACTICAL EXAMPLES")
    print("=" * 70)
    print()

    # ============================================================================
    # EXAMPLE 1: Basic LEGB Demonstration
    # ============================================================================

    print("Example 1: Basic LEGB Demonstration")
    print("-" * 70)

    x = "Global X"  # Global scope

    def outer():
        x = "Enclosing X"  # Enclosing scope

        def inner():
            x = "Local X"  # Local scope
            print(f"Inside inner(): {x}")

        inner()
        print(f"Inside outer(): {x}")

    outer()
    print(f"In global scope: {x}")

    print("\nExplanation:")
    print("• Each scope has its own 'x' variable")
    print("• inner() uses Local x")
    print("• outer() uses Enclosing x")
    print("• Global scope uses Global x")
    print()

    # ============================================================================
    # EXAMPLE 2: Searching Up the Chain
    # ============================================================================

    print("=" * 70)
    print("Example 2: Searching Up the Chain")
    print("-" * 70)

    message = "Global message"

    def outer():
        # No 'message' in enclosing scope

        def inner():
            # No 'message' in local scope
            # Python searches: Local → Enclosing → Global (found!)
            print(f"Message: {message}")

        inner()

    outer()

    print("\nExplanation:")
    print("• inner() has no local 'message'")
    print("• outer() has no enclosing 'message'")
    print("• Python finds 'message' in global scope")
    print()

    # ============================================================================
    # EXAMPLE 3: Built-in Scope
    # ============================================================================

    print("=" * 70)
    print("Example 3: Built-in Scope")
    print("-" * 70)

    def use_builtins():
        # No local/enclosing/global definitions of these
        # Python uses built-in scope
        data = [1, 2, 3, 4, 5]
        print(f"Length: {len(data)}")  # Built-in len()
        print(f"Max: {max(data)}")     # Built-in max()
        print(f"Sum: {sum(data)}")     # Built-in sum()

    use_builtins()

    print("\nExplanation:")
    print("• len, max, sum are built-in functions")
    print("• Available everywhere without import")
    print("• Last resort in LEGB chain")
    print()

    # ============================================================================
    # EXAMPLE 4: Shadowing Variables
    # ============================================================================

    print("=" * 70)
    print("Example 4: Shadowing Variables (Not Recommended)")
    print("-" * 70)

    x = 100  # Global

    def func1():
        x = 200  # Shadows global (creates local)
        print(f"func1 - Local x: {x}")

    def func2():
        print(f"func2 - Global x: {x}")

    func1()
    func2()
    print(f"Global x unchanged: {x}")

    print("\nExplanation:")
    print("• func1 creates a local 'x' (shadows global)")
    print("• func2 reads the global 'x'")
    print("• Global x is never modified")
    print()

    # ============================================================================
    # EXAMPLE 5: Reading Global (No Keyword Needed)
    # ============================================================================

    print("=" * 70)
    print("Example 5: Reading Global Variables")
    print("-" * 70)

    PI = 3.14159
    APP_NAME = "MyApp"
    VERSION = "1.0"

    def show_constants():
        # Can read globals without 'global' keyword
        print(f"π = {PI}")
        print(f"App: {APP_NAME} v{VERSION}")

    show_constants()

    print("\nExplanation:")
    print("• Reading global variables requires no keyword")
    print("• Only assignment needs 'global' keyword")
    print()

    # ============================================================================
    # EXAMPLE 6: The global Keyword
    # ============================================================================

    print("=" * 70)
    print("Example 6: Using global Keyword")
    print("-" * 70)

    counter = 0  # Global

    def increment():
        global counter  # Tell Python to use global counter
        counter += 1
        print(f"Counter incremented to: {counter}")

    print(f"Initial counter: {counter}")
    increment()
    increment()
    increment()
    print(f"Final counter: {counter}")

    print("\nExplanation:")
    print("• 'global' keyword tells Python to use global variable")
    print("• Without 'global', would create local variable")
    print("• Allows function to modify global state")
    print()

    # ============================================================================
    # EXAMPLE 7: UnboundLocalError Example
    # ============================================================================

    print("=" * 70)
    print("Example 7: Common Mistake - UnboundLocalError")
    print("-" * 70)

    x = 10

    def problematic_function():
        try:
            print(x)  # Trying to read x
            x = 20    # But assignment makes x local!
        except UnboundLocalError as e:
            print(f"Error: {e}")
            print("Problem: x is treated as local because of assignment below")

    problematic_function()

    print("\nExplanation:")
    print("• Python sees 'x = 20' and makes x local")
    print("• Trying to print x before assignment fails")
    print("• Solution: Use 'global x' or rename variable")
    print()

    # ============================================================================
    # EXAMPLE 8: Multiple Enclosing Scopes
    # ============================================================================

    print("=" * 70)
    print("Example 8: Multiple Enclosing Scopes")
    print("-" * 70)

    def level1():
        x = "Level 1"
        print(f"Level 1: x = '{x}'")

        def level2():
            y = "Level 2"
            print(f"Level 2: y = '{y}', x = '{x}' (from level1)")

            def level3():
                z = "Level 3"
                print(f"Level 3: z = '{z}', y = '{y}' (from level2), x = '{x}' (from level1)")

            level3()

        level2()

    level1()

    print("\nExplanation:")
    print("• Inner functions can access all outer scopes")
    print("• Python searches each enclosing scope in order")
    print("• Can have multiple levels of nesting")
    print()

    # ============================================================================
    # EXAMPLE 9: The nonlocal Keyword
    # ============================================================================

    print("=" * 70)
    print("Example 9: Using nonlocal Keyword")
    print("-" * 70)

    def outer():
        count = 0  # Enclosing variable

        def increment():
            nonlocal count  # Access enclosing count
            count += 1
            return count

        print(f"Initial count: {count}")
        print(f"After 1st increment: {increment()}")
        print(f"After 2nd increment: {increment()}")
        print(f"After 3rd increment: {increment()}")
        print(f"Final count in outer: {count}")

    outer()

    print("\nExplanation:")
    print("• 'nonlocal' allows modifying enclosing scope")
    print("• Without 'nonlocal', would create local variable")
    print("• Only works with enclosing scope, not global")
    print()

    # ============================================================================
    # EXAMPLE 10: Closure Example
    # ============================================================================

    print("=" * 70)
    print("Example 10: Closures - Functions that Remember")
    print("-" * 70)

    def make_multiplier(n):
        def multiply(x):
            return x * n  # Captures 'n' from enclosing scope
        return multiply

    times_2 = make_multiplier(2)
    times_3 = make_multiplier(3)
    times_10 = make_multiplier(10)

    print(f"times_2(5) = {times_2(5)}")
    print(f"times_3(5) = {times_3(5)}")
    print(f"times_10(5) = {times_10(5)}")

    print("\nExplanation:")
    print("• Inner function 'multiply' captures 'n' from outer scope")
    print("• Each returned function remembers its own 'n'")
    print("• This is called a 'closure'")
    print()

    # ============================================================================
    # EXAMPLE 11: Namespace Inspection
    # ============================================================================

    print("=" * 70)
    print("Example 11: Inspecting Namespaces")
    print("-" * 70)

    global_var = "I'm global"

    def outer():
        enclosing_var = "I'm enclosing"

        def inner():
            local_var = "I'm local"
            print("Local namespace:", locals())
            print("\nGlobal namespace (first 5 items):")
            for key in list(globals().keys())[:5]:
                print(f"  {key}")

        inner()

    outer()

    print("\nExplanation:")
    print("• locals() returns current scope's variables")
    print("• globals() returns module-level variables")
    print("• Useful for debugging scope issues")
    print()

    # ============================================================================
    # EXAMPLE 12: Shadowing Built-ins (Bad Practice!)
    # ============================================================================

    print("=" * 70)
    print("Example 12: Shadowing Built-ins (Don't Do This!)")
    print("-" * 70)

    def bad_practice():
        # Shadowing built-in 'list'
        list = [1, 2, 3]  # Now 'list' is a variable, not the class
        print(f"My list: {list}")

        # Can't use list() constructor anymore!
        try:
            new_list = list("abc")  # Trying to convert string to list
        except TypeError as e:
            print(f"Error: {e}")
            print("Problem: 'list' now refers to [1,2,3], not list class")

    bad_practice()

    # After function, built-in 'list' works again
    print(f"\nOutside function, list('xyz') = {list('xyz')}")

    print("\nExplanation:")
    print("• Variable name 'list' shadows built-in 'list' class")
    print("• Always use different names to avoid this")
    print("• Good alternatives: items, values, data, etc.")
    print()

    # ============================================================================
    # EXAMPLE 13: global vs. nonlocal
    # ============================================================================

    print("=" * 70)
    print("Example 13: global vs. nonlocal")
    print("-" * 70)

    x = "I'm global"

    def outer():
        x = "I'm enclosing"

        def use_nonlocal():
            nonlocal x
            print(f"nonlocal sees: '{x}' (enclosing)")

        def use_global():
            global x
            print(f"global sees: '{x}' (global)")

        use_nonlocal()
        use_global()

    outer()

    print("\nExplanation:")
    print("• 'nonlocal' accesses enclosing scope")
    print("• 'global' accesses global scope")
    print("• Both skip local scope")
    print()

    # ============================================================================
    # EXAMPLE 14: Practical Closure - Counter Factory
    # ============================================================================

    print("=" * 70)
    print("Example 14: Practical Closure - Counter Factory")
    print("-" * 70)

    def make_counter(start=0, step=1):
        count = start

        def increment():
            nonlocal count
            count += step
            return count

        def decrement():
            nonlocal count
            count -= step
            return count

        def reset():
            nonlocal count
            count = start

        def get_value():
            return count

        return increment, decrement, reset, get_value

    # Create two independent counters
    inc1, dec1, reset1, get1 = make_counter(0, 1)
    inc2, dec2, reset2, get2 = make_counter(100, 10)

    print("Counter 1:")
    print(f"  Start: {get1()}")
    print(f"  +1: {inc1()}")
    print(f"  +1: {inc1()}")
    print(f"  -1: {dec1()}")

    print("\nCounter 2:")
    print(f"  Start: {get2()}")
    print(f"  +10: {inc2()}")
    print(f"  +10: {inc2()}")
    print(f"  -10: {dec2()}")

    print("\nExplanation:")
    print("• Each counter maintains its own state")
    print("• State is preserved across function calls")
    print("• Closures provide encapsulation")
    print()

    # ============================================================================
    # EXAMPLE 15: LEGB in Classes
    # ============================================================================

    print("=" * 70)
    print("Example 15: LEGB with Classes")
    print("-" * 70)

    class_var = "I'm a module variable"

    class MyClass:
        class_attr = "I'm a class attribute"

        def method(self):
            local_var = "I'm local to method"

            # Can access all scopes
            print(f"Local: {local_var}")
            print(f"Class attribute: {self.class_attr}")
            print(f"Module variable: {class_var}")
            print(f"Built-in: {len([1, 2, 3])}")

    obj = MyClass()
    obj.method()

    print("\nExplanation:")
    print("• Class methods have access to all scopes")
    print("• self.attr accesses instance/class attributes")
    print("• LEGB still applies for regular variables")
    print()

    # ============================================================================
    # SUMMARY
    # ============================================================================

    print("=" * 70)
    print("SUMMARY - LEGB RULE")
    print("=" * 70)
    print("""
    Key Takeaways:

    1. Search Order: Local → Enclosing → Global → Built-in

    2. Reading is easy: Can read outer scopes automatically

    3. Writing requires keywords:
       • global: For modifying global variables
       • nonlocal: For modifying enclosing variables

    4. Shadowing: Inner scope can hide outer scope names

    5. Closures: Inner functions can capture outer variables

    6. Best Practice: Minimize global/nonlocal usage, prefer parameters

    Remember: When Python can't find a name in any scope, you get NameError!
    """)

    print("=" * 70)
    print("Next: Practice with exercises in 03_legb_exercises.py")
    print("=" * 70)