Back to Blog
Python

Python Local Variable Scope: How It Works

python local variable: Understand how Python local variables work: scope resolution, lifetime, closures, and common errors like UnboundLocalError.

pythonvariable scopeLEGB ruleclosuresUnboundLocalError
Diagram showing local variable scope inside a function, with global scope outside.

When you assign a name inside a function body, Python treats it as a local variable. This is the most common way a python local variable comes into existence, but the rule has consequences that surprise developers who are new to the language. Consider this function:

def show(): print(value) # UnboundLocalError: local variable 'value' referenced before assignment value =10

Even though value is not assigned until after the print, Python's compiler sees the assignment and marks value as local to show. At runtime, the print tries to read a local variable that has not been bound yet, so it raises UnboundLocalError. This is not an error about a missing global; it is a direct consequence of how Python determines what is local.

How Python Decides a Name Is Local

Python does not require explicit declarations for local variables. Instead, the compiler scans the entire function body during compilation. Any name that appears on the left side of an assignment, as a loop variable, as an import target, or as a parameter is considered local to that function. This includes names assigned inside nested blocks such as if, for, or with. There is no block-level scope in Python; a variable assigned inside an if is still local to the entire function.

This behavior is defined by the language and is not configurable. The practical consequence is that you cannot read a variable before assigning it in the same function unless you explicitly declare it as global or nonlocal. The decision is made at compile time, not at runtime, which is why the error appears even if the assignment line is never executed.

The LEGB Rule for Name Resolution

When Python evaluates a name, it follows the LEGB order: Local, Enclosing, Global, Built-in. Local means the current function's scope. Enclosing refers to the scopes of any outer functions that contain the current one, which matters for nested functions. Global is the module-level scope, and Built-in is the last fallback for names like len or print.

x = "global" # module scope def outer(): x = "outer local" def inner(): x = "inner local" print(x) # inner local inner() outer()()

In this example, inner has its own local x, so it does not consult the enclosing or global scopes. If inner did not assign x, Python would look in outer's scope, then in the module scope. This resolution order is what makes closures and nested functions work predictably.

Local Variable Lifetime and Memory

A local variable is created when the function is called and destroyed when the function returns. The values are stored on the function's stack frame, which is deallocated when the call completes. For simple objects like integers and strings, this is efficient because the frame is a contiguous block of memory. However, if a local variable references a larger object, such as a list or a class instance, the object itself lives on the heap and is garbage-collected when no references remain.

There is an important exception: if a local variable is captured by a closure, its lifetime is extended. The closure keeps a reference to the variable, so the value persists even after the outer function has returned. This is not a leak; it is the intended behavior for factories, decorators, and callback functions.

UnboundLocalError and the Global Statement

UnboundLocalError is the most common error developers encounter with local variables. It occurs when you read a name that is local to the function but has not been assigned yet. The fix is usually to initialize the variable before reading it, or to declare it as global if you intend to modify a module-level name.

count = 0 def increment(): global count count += 1

Without the global declaration, count would be treated as a local variable, and count += 1 would try to read an unbound local. The global statement tells Python to use the module-level binding for both reads and writes. A similar statement, nonlocal, works for variables in an enclosing function scope.

Closures and Captured Local Variables

When a nested function references a variable from its enclosing function, that variable is captured by the closure. The nested function does not receive a copy; it receives a live reference. This means that if the outer function changes the variable after the inner function is defined, the inner function sees the updated value. This is often called late binding.

def multipliers(): funcs = [] for i in range(3): funcs.append(lambda: i) return funcs for f in multipliers(): print(f()) # prints 2, 2, 2

The lambda functions all capture the same i, and by the time they are called, the loop has finished and i is 2. To capture the current value at each iteration, you can use a default argument: lambda i=i: i. This creates a separate local binding for each function.

Performance: Local vs Global Lookup

Local variable access is faster than global access because of how Python stores names. Locals are stored in an array and accessed by index, while globals are stored in a dictionary and looked up by name. The difference is small for a single access but can matter in tight loops or recursive functions. For example, if you repeatedly call a function that reads a global constant, you can assign it to a local variable at the start of the function to avoid repeated dictionary lookups.

import math def circle_area(radius): pi = math.pi # local reference return pi * radius ** 2

This is not a micro-optimization to apply everywhere; it is a readability tradeoff. If the global name is unlikely to change and the function is called frequently, the local alias can reduce overhead. The Python interpreter does not inline globals, so the cost is real but usually negligible unless the loop is very hot.

Best Practices for Using Local Variables

Keep local variable scope as narrow as possible. A function that uses many temporary variables is often a sign that it should be split into smaller functions. Avoid shadowing built-in names like list or dict; this can confuse readers and cause subtle bugs if you later need the built-in. Use descriptive names that reflect the value's purpose, and initialize variables before use to avoid UnboundLocalError.

When a variable is only needed inside a loop, it is still local to the entire function, so be aware that its value persists after the loop ends. This is not a problem in most cases, but it can surprise you if you reuse the name later. For closures, be explicit about which variables you intend to capture, and consider using default arguments when you need to freeze a value at definition time.

python local variable: Practical Usage and Code Examples | RYUSLOG DEV