How to use 'nonlocal' in Python

Master Python's nonlocal keyword with our guide. We cover usage, tips, real-world applications, and debugging common errors.

How to use 'nonlocal' in Python
Published on: 
Mon
Apr 6, 2026
Updated on: 
Fri
Apr 10, 2026
The Replit Team

The nonlocal keyword in Python lets you modify variables in nested functions. It's a powerful tool to manage state within complex scopes without the need for global variables.

In this article, you'll learn techniques and tips to use nonlocal. We'll cover real-world applications and offer advice on how to debug common issues for cleaner, more efficient code.

Understanding the nonlocal keyword

def outer_function():
x = "outer value"
def inner_function():
nonlocal x
x = "changed in inner function"
print(f"Inner: {x}")
inner_function()
print(f"Outer: {x}")

outer_function()--OUTPUT--Inner: changed in inner function
Outer: changed in inner function

In this example, the nonlocal x statement inside inner_function is crucial. It signals that any changes to x should apply to the variable in the nearest enclosing scope—in this case, outer_function.

Without the nonlocal keyword, Python would create a new local variable x within inner_function, leaving the original x in outer_function unchanged. The output confirms that the change persists, as the value modified by the inner function is printed by the outer one.

Fundamental uses of nonlocal

This simple mechanism unlocks powerful patterns for creating counters, building stateful closures, and managing scope more effectively than using the global keyword.

Creating counters with nonlocal

def create_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment

counter = create_counter()
print(counter())
print(counter())
print(counter())--OUTPUT--1
2
3

The create_counter function returns the increment function, creating what's known as a closure. This pattern is perfect for maintaining state between function calls without using global variables.

  • The nonlocal count statement is what lets the increment function modify the count variable from its parent, create_counter.
  • Because the returned function "remembers" its enclosing environment, each call to counter updates the same count variable, creating a stateful counter.

Comparing global and nonlocal keywords

global_var = "global"

def outer():
outer_var = "outer"
def inner():
nonlocal outer_var
global global_var
outer_var = "modified outer"
global_var = "modified global"
inner()
print(f"Outer: {outer_var}")

outer()
print(f"Global: {global_var}")--OUTPUT--Outer: modified outer
Global: modified global

This example highlights the key difference between the global and nonlocal keywords. While both let you modify variables outside the current function, they target entirely different scopes.

  • The nonlocal keyword modifies a variable in the nearest enclosing scope that isn't global. Here, nonlocal outer_var allows the inner() function to change the outer_var belonging to outer().
  • In contrast, the global keyword reaches past any nested scopes to modify a variable at the top level of your script. The global global_var statement directly alters the global_var defined in the global scope.

Building closures with nonlocal for state

def make_adder(base):
total = base
def add(value):
nonlocal total
total += value
return total
return add

add_five = make_adder(5)
print(add_five(3))
print(add_five(7))--OUTPUT--8
15

The make_adder function is a factory that creates new, specialized functions. When you create add_five, it captures the initial base value of 5 and stores it in a private total variable. This creates a closure that "remembers" its starting point.

  • The nonlocal total statement is what allows the inner add function to modify the total variable from its parent scope.
  • Each time you call add_five, it updates its own unique running total, preserving the state between calls in a clean, contained way.

Advanced applications of nonlocal

Beyond these foundational uses, the nonlocal keyword unlocks more advanced patterns for stateful decorators, coroutines, and handling different object types.

Using nonlocal with mutable and immutable objects

def demonstrate_nonlocal_behavior():
immutable = "string"
mutable = [1, 2, 3]

def modify():
nonlocal immutable
immutable += " modified"

# Don't need nonlocal for mutable operations
mutable.append(4)

modify()
print(f"Immutable: {immutable}")
print(f"Mutable: {mutable}")

demonstrate_nonlocal_behavior()--OUTPUT--Immutable: string modified
Mutable: [1, 2, 3, 4]

How nonlocal behaves depends on whether you're working with mutable or immutable objects. You must use it to reassign an immutable variable, like a string. The operation immutable += " modified" creates a new string and reassigns the name, which is why nonlocal is required to update the variable in the outer scope.

  • Conversely, you don't need nonlocal to modify a mutable object in place, such as a list.
  • The method call mutable.append(4) changes the existing list object, so no reassignment is necessary.

Implementing decorators with nonlocal for state tracking

def counter_decorator(func):
call_count = 0
def wrapper(*args, **kwargs):
nonlocal call_count
call_count += 1
print(f"Call {call_count} to {func.__name__}")
return func(*args, **kwargs)
return wrapper

@counter_decorator
def greet(name):
return f"Hello, {name}!"

print(greet("Alice"))
print(greet("Bob"))--OUTPUT--Call 1 to greet
Hello, Alice!
Call 2 to greet
Hello, Bob!

This decorator adds a call counter to any function it wraps. The nonlocal call_count statement is the key to making this work, as it allows the inner wrapper function to modify a variable from its parent scope.

  • Without nonlocal, Python would treat call_count as a new local variable inside wrapper, and the counter would reset on every call.
  • This pattern creates a stateful decorator that remembers the call_count across multiple calls to the decorated greet function.

Using nonlocal in generator-based coroutines

def coroutine_example():
result = None

def coroutine():
nonlocal result
x = yield "Ready"
result = f"Processed: {x}"
yield result

coro = coroutine()
print(next(coro))
print(coro.send("Hello"))
print(f"Stored result: {result}")

coroutine_example()--OUTPUT--Ready
Processed: Hello
Stored result: Processed: Hello

The nonlocal keyword is also useful in generator-based coroutines for managing state. In this pattern, the inner coroutine function can update a variable in its parent scope, coroutine_example, allowing it to communicate results back out.

  • The nonlocal result statement is key. It ensures that when the coroutine assigns a value to result, it’s modifying the variable in the outer function, not creating a new local one.
  • After sending data into the coroutine with coro.send(), the updated state is reflected in the parent scope's result variable, effectively bridging the two contexts.

Move faster with Replit

Replit is an AI-powered development platform where Python dependencies come pre-installed, so you can skip setup and start coding instantly. It’s the perfect environment to go from learning individual techniques, like using nonlocal, to building complete applications.

Instead of just piecing together functions, you can describe the app you want to build and Agent 4 will take it from idea to a working product. Here are a few examples:

  • A simple analytics counter that uses closures to track clicks on different links, maintaining a separate count for each.
  • A configuration factory that generates specialized functions for different settings, with each function remembering its own state.
  • A stateful decorator that logs how many times a function is called, helping you identify performance bottlenecks.

Simply describe your app, and Replit will write the code, test it, and fix issues automatically, all within your browser.

Common errors and challenges

While powerful, the nonlocal keyword can introduce tricky bugs if you're not careful about a few common pitfalls.

Forgetting to declare a variable as nonlocal is the most frequent mistake. When you do this, Python creates a new local variable inside the nested function instead of modifying the one in the outer scope. This can lead to silent failures where your code runs without errors, but the outer variable never gets updated as you expect.

Another common issue is using nonlocal with undefined variables. The keyword requires the variable to already exist in an enclosing scope, and Python will raise a SyntaxError if it can't find one. This is a key difference from the global keyword, which can create a new variable if one doesn't exist.

Finally, confused scope references with nested nonlocal can cause headaches. The keyword always binds to the variable in the nearest enclosing function. In deeply nested code, it's easy to lose track and accidentally modify a variable in a closer scope than the one you intended.

Forgetting to declare a variable as nonlocal

When you try to modify a variable from an outer scope without declaring it nonlocal, Python assumes you're creating a new local variable. This leads to an UnboundLocalError because you're trying to change a variable that hasn't been assigned a value yet. The following code demonstrates this common pitfall.

def outer():
counter = 0

def increment():
counter += 1 # This will raise UnboundLocalError
return counter

return increment

count = outer()
print(count())

The += operator tries to modify counter, but without the nonlocal keyword, Python treats it as an unassigned local variable inside increment(). This conflict triggers the UnboundLocalError. See how a small change fixes this issue.

def outer():
counter = 0

def increment():
nonlocal counter # Fix: declare counter as nonlocal
counter += 1
return counter

return increment

count = outer()
print(count())

By adding nonlocal counter, you're telling the increment() function to modify the counter from its parent scope, outer(). This simple declaration is the key to making stateful closures work correctly.

  • Keep an eye out for this error whenever you use assignment operators like += inside a nested function. They try to modify a variable that, without nonlocal, Python sees as a new, uninitialized local variable.

Using nonlocal with undefined variables

Unlike the global keyword, nonlocal can't create new variables. It only modifies variables that already exist in an outer scope. If you try to declare a variable as nonlocal without defining it first, Python raises a SyntaxError. See what happens in the code below.

def outer_function():
def inner_function():
nonlocal value # SyntaxError: no binding for nonlocal 'value' found
value = 42
print(value)

inner_function()

outer_function()

The nonlocal value statement fails because the outer_function never defines a value variable for it to bind to, causing a SyntaxError. The corrected code below shows how to resolve this issue.

def outer_function():
value = None # Fix: define value in the outer scope first

def inner_function():
nonlocal value
value = 42
print(value)

inner_function()

outer_function()

By initializing value = None in the outer_function, you give the nonlocal statement an existing variable to bind to. The keyword can only modify variables from an outer scope—it can't create them. This pre-definition satisfies Python's requirement and prevents the SyntaxError.

  • Always define a variable in an enclosing scope before you try to modify it with nonlocal in a nested function.

Confused scope references with nested nonlocal

In deeply nested functions, the nonlocal keyword can cause confusion. It always binds to the variable in the nearest enclosing scope, which might not be the one you intend to modify, leading to subtle bugs. The following code demonstrates this behavior.

def outer():
x = "outer"

def middle():
x = "middle" # Creates a new local variable

def inner():
nonlocal x # Refers to middle's x, not outer's x
x = "modified"
print(f"Inner x: {x}")

inner()
print(f"Middle x: {x}")

middle()
print(f"Outer x: {x}") # Remains unchanged

outer()

The nonlocal x in inner() latches onto the x from middle(), not outer(). This happens because middle() defines its own x, shadowing the outermost variable and leaving it untouched. The corrected code below shows how to resolve this.

def outer():
x = "outer"

def middle():
nonlocal x # Now refers to outer's x
x = "middle"

def inner():
nonlocal x # This now modifies the same x
x = "modified"
print(f"Inner x: {x}")

inner()
print(f"Middle x: {x}")

middle()
print(f"Outer x: {x}") # Now shows the modified value

outer()

By adding nonlocal x to the middle() function, you create a chain that links all scopes. Now, middle() modifies the x from outer(), and inner() modifies that same variable. This ensures all three functions reference the same x, so the final change from inner() persists all the way out.

  • It's a good idea to watch for this when you have multiple nested functions. Reusing variable names can easily lead to unintended shadowing.

Real-world applications

Understanding how to avoid common errors with nonlocal opens the door to practical applications, such as creating simple caching mechanisms and configurable loggers.

Creating a simple caching mechanism with nonlocal

The nonlocal keyword lets you build a simple caching mechanism within a closure, which stores the results of expensive operations to avoid redundant calculations.

def create_caching_function():
cache = {}

def calculate(n):
nonlocal cache
if n not in cache:
cache[n] = n * n
print(f"Calculating square of {n}")
else:
print(f"Using cached value for {n}")
return cache[n]

return calculate

square = create_caching_function()
print(square(4))
print(square(4))
print(square(5))

The create_caching_function returns a calculate function that remembers its own private cache dictionary. This is possible because the nonlocal cache statement allows the inner function to modify the dictionary from its parent scope.

  • When you call square(4) the first time, it computes the result and saves it.
  • The second call retrieves the value directly from the cache, skipping the redundant calculation.

This pattern is a simple form of memoization, a technique that boosts performance by storing and reusing the results of function calls.

Building a configurable logger with nonlocal

The nonlocal keyword is perfect for creating a configurable logger, letting you adjust its output verbosity dynamically.

def create_logger(min_level=1):
current_level = min_level

def log(message, level=1):
nonlocal current_level
if level >= current_level:
print(f"[Level {level}] {message}")

def set_level(new_level):
nonlocal current_level
current_level = new_level
print(f"Log level changed to {new_level}")

log.set_level = set_level
return log

logger = create_logger(2)
logger("This won't show", 1)
logger("This will show", 2)
logger.set_level(1)
logger("Now this shows too", 1)

The create_logger function is a factory that returns a specialized log function. This returned function is a closure, meaning it remembers the current_level variable from its parent scope. Both the inner log and set_level functions use the nonlocal keyword to access and modify this shared state.

  • Attaching set_level as an attribute to the log function itself is a clever way to make the logger configurable after it has been created.
  • This allows you to call logger.set_level() to change its behavior dynamically, creating a stateful tool without needing a class.

Get started with Replit

Turn your understanding of nonlocal into a real tool. Describe what you want to build to Replit Agent, like “a currency converter that caches rates” or “a dashboard widget that tracks button clicks.”

The Agent writes the code, tests for errors, and deploys your app from a simple description. Start building with Replit.

Get started free

Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.

Get started free

Create and deploy websites, automations, internal tools, data pipelines and more in any programming language without setup, downloads or extra tools. All in a single cloud workspace with AI built in.