python
error-handling
global-variable
UnboundLocalError
programming-debugging

UnboundLocalError trying to use a variable supposed to be global that is reassigned even after first use

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

UnboundLocalError appears when Python decides a name is local inside a function, but that local value is read before assignment. This often surprises developers who intended to read a global variable and then update it. Understanding Python scope rules is the fastest way to fix the error correctly.

Core Topic Sections

Why this error happens

Inside a function, any assignment to a variable name makes that name local by default. Python determines this at compile time for the function body.

Example that fails:

python
1counter = 10
2
3def increment():
4    print(counter)      # error: Python treats counter as local in this function
5    counter = counter + 1
6
7increment()

Even though the print statement appears before assignment, Python already marked counter as local because assignment exists later in the same function.

Fix 1: use global intentionally

If you really want to mutate a module-level variable, declare it with global.

python
1counter = 10
2
3def increment():
4    global counter
5    print(counter)
6    counter += 1
7
8increment()
9print(counter)

This works, but global mutation should be limited because it increases coupling and test complexity.

Fix 2: avoid global mutation with return values

A cleaner design is to pass value in and return updated value.

python
1def increment(value: int) -> int:
2    return value + 1
3
4counter = 10
5counter = increment(counter)
6print(counter)

This style is easier to test and reason about in larger codebases.

nonlocal for nested function scopes

When dealing with nested functions, nonlocal targets the nearest enclosing function scope, not module scope.

python
1def make_counter():
2    value = 0
3
4    def inc():
5        nonlocal value
6        value += 1
7        return value
8
9    return inc
10
11c = make_counter()
12print(c())
13print(c())

Use nonlocal only when nested closure state is intentional.

Scope inspection and debugging steps

When debugging scope-related errors:

  1. Search function body for assignments to the same name.
  2. Check if the name should be local, nonlocal, or global.
  3. Rename variables to avoid accidental shadowing.
  4. Prefer explicit inputs and outputs over hidden shared state.

These steps solve most scope bugs quickly.

Common real-world scenarios

This error appears frequently in:

  1. Accumulator counters in scripts.
  2. Caching variables shared across helper functions.
  3. Flags updated inside callback handlers.
  4. Notebook cells where variable reuse is inconsistent.

Consistent naming and clear data flow reduce these failures.

Refactor pattern for maintainability

If many functions depend on one mutable value, consider encapsulating state in a class or dataclass.

python
1from dataclasses import dataclass
2
3@dataclass
4class Counter:
5    value: int = 0
6
7    def increment(self) -> int:
8        self.value += 1
9        return self.value
10
11counter = Counter(10)
12print(counter.increment())

Object-based state management is explicit and easier to extend than scattered globals.

Testing strategy for scope-sensitive code

Add focused tests that call functions multiple times and validate state transitions. Bugs often appear only after second or third call.

Recommended checks:

  1. Initial value behavior.
  2. Repeated update behavior.
  3. Reset behavior if supported.
  4. Independence between separate instances.

Scope bugs become predictable when state transitions are tested explicitly.

Common Pitfalls

  • Assuming read-before-write of a global name is safe when the function also assigns it.
  • Using global everywhere instead of improving function design.
  • Confusing global and nonlocal in nested function code.
  • Reusing variable names that shadow outer scope unintentionally.
  • Debugging only first call and missing state-related failures on later calls.

Summary

  • UnboundLocalError is a scope resolution issue, not a runtime random failure.
  • Assignment inside a function makes the name local unless declared otherwise.
  • Use global only when module-level mutation is genuinely required.
  • Prefer return-value or object-based state flow for maintainable code.
  • Test state transitions explicitly to catch scope bugs early.

Course illustration
Course illustration

All Rights Reserved.