Back to Blog
Python

Python Function Scope: Local, Global, and Nonlocal

python function scope: Understand how Python resolves variable names inside functions using the LEGB rule, and learn when to use global, nonlocal, and closures.

pythonscopeclosuresLEGBnonlocalglobal
Diagram illustrating Python function scope with nested functions and variable lookup order.

python function scope requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.

Python's function scope determines which variable a name refers to when you read or assign it inside a function. The rule is simple: assignment binds a name to the current scope, while reading a name triggers a lookup that follows the LEGB order. Getting this wrong leads to confusing UnboundLocalError or silent use of the wrong variable.

How Python Decides Which Variable You Mean

When you write x = 10 inside a function, Python treats that as a local variable for the entire function, even if the assignment appears after a read. This is why the following code fails:

def example(): print(x) x is not defined yet x = 10 example()

Running this raises UnboundLocalError: local variable 'x' referenced before assignment. The print tries to read x, but because the function contains an assignment to x, Python marks it as local from the start. The variable exists in the local scope but has no value yet.

This behavior is not surprising once you understand that Python decides scope at compile time, not at runtime. The entire function body is scanned, and any name that is assigned anywhere becomes local to that function unless explicitly declared otherwise.

The LEGB Rule: Local, Enclosing, Global, Built-in

When you read a variable inside a function, Python searches for it in this order:

  1. Local – names defined inside the current function
  2. Enclosing – names defined in any enclosing function (for nested functions)
  3. Global – names defined at module level
  4. Built-in – names pre-defined in the builtins module like len or print

The first match wins. If no match is found, you get a NameError. This order is often called the LEGB rule.

Consider this example:

value = "global" def outer(): value = "enclosing" def inner(): value = "local" print(value) inner() outer()

The output is local because the innermost function has its own value. If you remove the assignment inside inner, it will print enclosing, then global, and so on.

Local Scope: Variables That Live Only Inside a Function

Every time a function is called, a new local scope is created. Variables assigned inside the function are local to that call and are destroyed when the function returns. This means two calls to the same function do not share local variables.

def counter(): count = 0 count += 1 return count print(counter()) # 1 print(counter()) # 1

Each call starts with count = 0, so the result is always 1. To persist state across calls, you need a global variable, an attribute on an object,, or a closure.

Local variables are fast because the interpreter stores them in a compact array rather than a dictionary. This is one reason why local access is cheaper than global access, though the difference is rarely significant unless you are in a tight loop.

Global Scope: Reading vs Assigning

Reading a global variable inside a function works without any special keyword:

name = "Ada" def greet(): print("Hello, " + name) greet()

This prints Hello, Ada because name is found in the global scope. However, if you try to assign to name inside the function, you create a new local variable instead of modifying the global one:

name = "Ada" def change(): name = "Grace" change() print(name) # "Ada"

The assignment name = "Grace" creates a local variable that shadows the global. To actually modify the global, you must declare it with the global keyword:

name = "Ada" def change(): global name name = "Grace" change() print(name) # "Grace"

The global statement tells Python that the name refers to the module-level variable for the entire function, not just the line where it appears. Use it sparingly because it couples your function to external state and makes the code harder to test and reason about.

The nonlocal Keyword and Closures

Nested functions can access variables from their enclosing function without any keyword. But if the inner function assigns to a name, it becomes local to the inner function. To modify a variable in the enclosing scope, use nonlocal:

def outer(): count = 0 def increment(): nonlocal count count += 1 return count return increment counter = outer() print(counter()) # 1 print(counter()) # 2

Here, increment is a closure: it captures the count variable from outer even after outer has returned. The nonlocal keyword is the only way to rebind that captured variable. Without it, the += would create a local count and raise UnboundLocalError.

Closures are useful for creating function factories, decorators, and stateful callbacks. They keep state private without polluting the global namespace. The tradeoff is that each closure retains a reference to the enclosing scope, which can keep objects alive longer than expected. If you create many closures in a loop, be aware that each one holds its own copy of the captured variables.

Common Scope Mistakes That Lead to Bugs

One frequent mistake is assuming that a variable used in a comprehension or a lambda follows a different scope rule. In Python, list comprehensions have their own local scope in Python 3, but lambdas do not. For example:

funcs = [lambda x: x + i for i in range(3)] print([f(0) for f in funcs]) # [2, 2, 2]

The lambdas all capture the same i from the enclosing scope, which ends up as 2 after the loop finishes. To fix this, you can give the lambda a default argument:

funcs = [lambda x, i=i: x + i for i in range(3)] print([f(0) for f in funcs]) # [0, 1, 2]

Another mistake is using global when you only need to read a variable. Reading does not require global; only assignment does. Adding unnecessary global statements makes the code more brittle and can hide the fact that you are accidentally depending on external state.

Scope Choices and Their Effect on Maintainability and Performance

The way you handle scope directly affects how easy your code is to test and maintain. Functions that rely on global variables are harder to isolate because you must set up the global state before each test. Prefer passing values as arguments and returning results. This makes the function's dependencies explicit.

From a performance standpoint, local variable access is faster than global access because the interpreter uses index-based lookup for locals versus dictionary lookup for globals. In tight loops, this difference can add up. If you need to access a global repeatedly inside a loop, assign it to a local variable first:

import math def circle_area(radius): pi = math.pi # local reference return pi * radius ** 2 ```n This also improves readability by giving a short name to a frequently used value. Closures add a small overhead because the captured variables live in a cell object rather than a plain local. This overhead is usually negligible, but it matters in high-frequency callbacks. If you need maximum speed, consider using a class with attributes instead of a closure, or restructure the code to avoid capturing variables in hot paths. Finally, be aware that Python's scope rules are lexical, meaning they are determined by the source code structure, not by runtime call order. This is what makes closures work predictably. Understanding this distinction helps you reason about variable lifetime and avoid surprises when you pass functions around as objects.
python function scope: Practical Usage and Code Examples | RYUSLOG DEV