Learnixo
Back to blog
AI Systemsintermediate

Scope and the LEGB Rule

Understand Python variable scope: Local, Enclosing, Global, and Built-in lookup order. Covers closures, nonlocal/global keywords, common bugs, and patterns used in LangChain callbacks and AI pipelines.

Asma Hafeez KhanMay 16, 20266 min read
PythonScopeLEGBClosuresFunctionsMachine Learning
Share:š•

What is Scope?

Scope determines where a variable name is visible. Python uses the LEGB rule to look up names: Local → Enclosing → Global → Built-in.

Python
x = "global"   # Global scope

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

    def inner():
        x = "local"   # Local scope
        print(x)      # "local" — LEGB stops at the first match

    inner()
    print(x)   # "enclosing"

outer()
print(x)   # "global"

L — Local Scope

Variables defined inside a function are local to that function.

Python
def compute_loss(y_true: float, y_pred: float) -> float:
    error = y_true - y_pred   # Local — not visible outside
    return error ** 2

compute_loss(1.0, 0.8)
# print(error)   # NameError: 'error' is not defined — it's local

E — Enclosing Scope

A nested function can read variables from its enclosing function's scope (this is a closure).

Python
def make_threshold_filter(threshold: float):
    """Returns a filter function that remembers the threshold."""

    def filter_scores(scores: list[float]) -> list[float]:
        # 'threshold' is in the enclosing scope — not passed as argument
        return [s for s in scores if s >= threshold]

    return filter_scores


high_confidence = make_threshold_filter(0.85)
print(high_confidence([0.9, 0.4, 0.87, 0.6]))   # [0.9, 0.87]

low_confidence = make_threshold_filter(0.5)
print(low_confidence([0.9, 0.4, 0.87, 0.6]))    # [0.9, 0.87, 0.6]

G — Global Scope

Variables defined at the module level are global. Functions can read globals without any keyword.

Python
MODEL_NAME = "gpt-4o"   # Global constant — readable anywhere
call_count = 0          # Global variable — needs 'global' to modify

def call_llm(prompt: str) -> str:
    global call_count   # Required to MODIFY a global
    call_count += 1
    return f"[{MODEL_NAME}] Response {call_count}"

call_llm("hello")
call_llm("world")
print(call_count)   # 2

Prefer constants over mutable globals. Mutable globals are hard to test and can cause subtle bugs. If you need shared mutable state, use a class instead.


B — Built-in Scope

Python's built-in names (len, print, range, type, int, list, etc.) are always available and are the last fallback in LEGB.

Python
print(len([1, 2, 3]))   # 3 — 'len' from built-in scope

# Shadowing a built-in (don't do this)
len = 42          # Overwrites the built-in 'len' in this scope
# len([1, 2, 3])  # TypeError: 'int' object is not callable

# Fix: delete the shadowing name to restore access
del len
print(len([1, 2, 3]))   # 3 — built-in restored

Avoid naming variables list, dict, set, type, id, input, min, max, sum, filter, map. These shadow built-ins and cause confusing bugs.


The global Keyword

Python
total_tokens_used = 0

def process_request(prompt: str) -> str:
    global total_tokens_used
    tokens = len(prompt.split()) * 2   # Rough estimate
    total_tokens_used += tokens
    return f"Processed: {prompt[:20]}..."

process_request("What is warfarin?")
process_request("Explain anticoagulation therapy.")
print(total_tokens_used)   # some number — shared across calls

For production code, prefer a class that encapsulates state:

Python
class TokenTracker:
    def __init__(self):
        self.total = 0

    def add(self, n: int) -> None:
        self.total += n

    def report(self) -> str:
        return f"Total tokens used: {self.total}"

tracker = TokenTracker()

The nonlocal Keyword

nonlocal lets an inner function modify a variable in its enclosing (not global) scope.

Python
def make_counter(start: int = 0):
    count = start   # Enclosing variable

    def increment(by: int = 1) -> int:
        nonlocal count   # Required to modify 'count' from enclosing scope
        count += by
        return count

    def reset() -> None:
        nonlocal count
        count = start

    return increment, reset


inc, rst = make_counter(0)
print(inc())    # 1
print(inc())    # 2
print(inc(5))   # 7
rst()
print(inc())    # 1 — reset worked

Closures in AI Pipelines

Closures are commonly used to create configurable functions:

Python
from langchain_core.runnables import RunnableLambda

def make_token_truncator(max_tokens: int):
    """Returns a function that truncates text to a token budget."""
    words_per_token = 0.75   # Captured in closure

    def truncate(text: str) -> str:
        max_words = int(max_tokens * words_per_token)
        words = text.split()
        if len(words) <= max_words:
            return text
        return " ".join(words[:max_words]) + "..."

    return truncate


truncate_4k = make_token_truncator(4096)
truncate_1k = make_token_truncator(1024)

# Wrap in LCEL
truncate_step = RunnableLambda(truncate_4k)

The Loop Variable Closure Bug

A common gotcha: closures capture the variable, not its value at definition time.

Python
# Bug: all functions reference the same 'i' variable
funcs = [lambda: i for i in range(3)]
print(funcs[0]())   # 2 — NOT 0!
print(funcs[1]())   # 2 — NOT 1!
print(funcs[2]())   # 2 — all see the final value of i

# Fix 1: use a default argument to capture the current value
funcs = [lambda i=i: i for i in range(3)]
print(funcs[0]())   # 0
print(funcs[1]())   # 1
print(funcs[2]())   # 2

# Fix 2: use functools.partial
from functools import partial

def make_retriever(index: int) -> int:
    return index

funcs = [partial(make_retriever, i) for i in range(3)]
print(funcs[0]())   # 0

Common Scope Bugs

Python
# Bug 1: assigning to a name makes it local — even reads before the assignment fail
x = 10

def buggy():
    print(x)   # UnboundLocalError: x referenced before assignment
    x = 20     # This assignment makes x local in the entire function

# Fix: either use 'global x' or don't assign to it locally


# Bug 2: mutable global modified without 'global' keyword still works
results = []   # Mutable global

def add_result(val):
    results.append(val)   # Mutates the global list — no 'global' needed
    # results = [val]     # This would create a local, NOT modify the global

add_result("drug_a")
print(results)   # ["drug_a"]


# Bug 3: shadowing a variable in a comprehension (Python 3 is safe here)
x = "outer"
squares = [x for x in range(5)]   # x inside comprehension is its own scope in Python 3
print(x)   # "outer" — comprehension doesn't leak in Python 3

Scope in Class Bodies

Python
class DrugClassifier:
    CLASSES = ["anticoagulant", "antidiabetic", "antihypertensive"]  # Class scope
    count = 0                                                         # Class scope

    def classify(self, drug: str) -> str:
        # 'CLASSES' is NOT directly visible here — must use self.CLASSES or DrugClassifier.CLASSES
        for cls in self.CLASSES:   # 'self' is local, refers to instance
            if cls in drug.lower():
                return cls
        return "unknown"

    @classmethod
    def increment(cls) -> None:
        cls.count += 1   # 'cls' is local (the class itself)

LEGB Quick Reference

| Scope | Where Defined | Keyword to Modify | Example | |---|---|---|---| | Local | Inside current function | (none needed) | x = 1 inside def f() | | Enclosing | Outer function (closures) | nonlocal | Inner function reads outer var | | Global | Module top-level | global | Constants, module state | | Built-in | Python interpreter | (can shadow, don't) | len, print, range |

Lookup order: Python searches L → E → G → B and stops at the first match. If a name is not found in any scope, you get a NameError.

Enjoyed this article?

Explore the AI Systems learning path for more.

Found this helpful?

Share:š•

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.