Python online course

Python Variable Scope: Local, Global, Enclosing, and Built-in Names

Learn Python variable scope, LEGB name lookup, local and global assignments, enclosing scopes, nonlocal, mutation, shadowing, and common scope errors.

Python scope is the part of a program where a name is available for lookup. Understanding scope explains why a variable can be used in one function but not another, why an assignment can cause UnboundLocalError, and how nested functions retain state.

A variable name and the object it refers to are different things. In count = 3, count is a name, and the integer object 3 is the value currently bound to that name. Name binding is the association between an identifier and an object. Scope describes where Python can resolve that identifier.

How Python Scope Works

A name becomes available through an operation such as assignment, a function parameter, an import, a function definition, or an explicit global or nonlocal declaration. Python determines the relevant scope from the location and context where the name is created, assigned, or declared.

Scope applies to names, not permanently to objects. Several names in different scopes can refer to the same object, and one name can later be rebound to a different object.

message = "first object"
message = "second object"

The second line rebinds message; it does not change the first string object.

Module-Level (Global) Scope

A Python file is a module. A name assigned at the top level of that file normally belongs to that module's global scope. Functions defined in the same module can read that name when no nearer local or enclosing name shadows it.

app_name = "Inventory"


def show_app_name():
    print(app_name)


def show_again():
    print(app_name)


show_app_name()
show_again()

Both functions read the module-level name. “Global” means global to this module, not automatically universal across every imported module. Another module must access it through an import and the exporting module's namespace, and its own local name with the same spelling can still be independent.

Function-Local Scope

Names assigned inside a function belong to that function's local scope by default. Parameters are local names too. Local names normally exist for the duration of a particular function call.

def make_message():
    message = "created here"
    print(message)


def use_message():
    print(message)


make_message()
use_message()  # NameError: name 'message' is not defined

message is local to make_message. A sibling function cannot directly access it. When Python cannot find a requested name in any accessible scope, it raises NameError.

Return a value or pass it as an argument when another function needs it:

def make_message():
    return "created here"


def use_message(message):
    print(message)


text = make_message()
use_message(text)

Local Shadowing of a Global Name

Shadowing occurs when a nearer scope uses the same name as an outer scope. An assignment inside a function creates a separate local binding unless a scope declaration says otherwise.

status = "global"


def show_status():
    status = "local"
    print(status)


show_status()       # local
print(status)       # global

The local name takes precedence inside show_status. Changing that local binding does not replace the module-level binding.

The LEGB Name-Resolution Rule

When Python evaluates a name, it normally searches in this order: Local, Enclosing, Global, Built-in. Python stops at the first matching binding.

Lookup levelWhere Python looksTypical exampleWhen it applies
LocalThe current function or other current local contextA parameter or name assigned in the functionFirst, for code running inside a function
EnclosingOuter function scopes around a nested functionA name in a factory functionAfter Local, when functions are nested
GlobalThe current module's top-level namespaceA constant assigned outside functionsAfter Local and Enclosing
Built-inNames supplied by Pythonlen, print, and rangeLast, if no program-defined binding was found
label = "global"


def outer():
    label = "enclosing"

    def inner():
        local_value = "local"
        print(local_value)  # Local
        print(label)        # Enclosing
        print(len("abc"))  # len is Built-in

    inner()


outer()

If inner had no local or enclosing label, Python would try the module-level label. If no matching name existed at any level, the result would be NameError.

Reading Versus Assigning Names

Reading a global name inside a function is allowed:

tax_rate = 0.2


def add_tax(price):
    return price * (1 + tax_rate)


print(add_tax(100))

However, an assignment anywhere in a function causes Python to classify that name as local throughout that function, unless global or nonlocal is declared. This classification happens even if the assignment appears after a read.

value = 10


def show_value():
    print(value)  # UnboundLocalError
    value = 20


show_value()

Python sees the assignment to value and treats every use of value in that function as a local use. The read occurs before the local binding has received a value, so Python raises UnboundLocalError, a specialized form of NameError.

The global Statement

global tells Python that assignments to a name inside a function should target the current module's top-level binding rather than create a local binding.

counter = 0


def next_counter():
    global counter
    counter += 1
    return counter


print(next_counter())  # 1
print(next_counter())  # 2
print(counter)         # 2

Without global counter, the augmented assignment would be treated as a local assignment and would fail because the local counter had not been initialized.

Unrestricted global mutation makes code harder to test and reason about: any function may change shared state, and call order can matter. Prefer arguments and return values in ordinary application code:

def next_counter(counter):
    return counter + 1


counter = 0
counter = next_counter(counter)

Use global only when deliberate, limited module-level state is the clearest design.

The nonlocal Statement, Enclosing Scope, and Closures

An enclosing scope belongs to an outer function surrounding a nested function. nonlocal makes assignment in the nested function target a name in the nearest enclosing function scope.

def make_counter():
    count = 0

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

    return increment


counter = make_counter()
print(counter())  # 1
print(counter())  # 2

The returned nested function is a closure: it retains access to count after make_counter has returned. nonlocal cannot target a module-level global name. It also requires an existing binding in an enclosing function scope; otherwise Python raises a syntax error.

Mutation Versus Rebinding

Rebinding makes a name refer to a different object. Mutation changes the contents of an existing mutable object while preserving the name's binding.

items = ["book"]


def append_item():
    items.append("pen")  # mutation; no global needed


def replace_items():
    items = ["new list"]  # new local binding


append_item()
print(items)  # ['book', 'pen']
replace_items()
print(items)  # ['book', 'pen']

The function can mutate the list because it does not assign a new object to the name items. If it must replace the outer list, it needs global items for a module-level list or nonlocal items for an enclosing-function list.

Operation inside a functionExample targetScope declaration neededEffect on outer value
Read global nameprint(total)NoReads the module-level binding
Reassign global nametotal = 0global totalReplaces the module-level binding
Mutate global list or dictionaryitems.append(x)No, if the name is not reassignedChanges the shared object
Reassign enclosing namecount = count + 1nonlocal countReplaces the outer-function binding
Mutate an enclosing mutable objectstate["ready"] = TrueNo, if the name is not reassignedChanges the shared object

An augmented assignment such as items += [x] is commonly treated as assignment for scope analysis. In a function, it can therefore require global items or nonlocal items, even though the operation may mutate a list in place at runtime.

Scope Boundaries and Modern Python Details

  • Functions and lambdas: each function call has local names. A lambda follows the same general lexical scope rules as a function.
  • Modules: a module has its own top-level namespace. Its global names are not automatically shared with unrelated modules.
  • if, for, and while: these blocks do not create a separate local scope. A name assigned in one of these blocks remains available in the surrounding function or module scope, subject to normal execution rules.
  • Comprehensions: in Python 3, a comprehension has its own iteration-variable scope. The loop variable does not leak into the surrounding scope.
  • Classes: a class body creates a class namespace, but class name lookup has special behavior. A class body should not be treated exactly like a nested function's enclosing scope. Methods do not automatically use class-body names as ordinary enclosing locals; use an instance, class, or module reference as appropriate.
numbers = [1, 2, 3]

squares = [number * number for number in numbers]

# Python 3: this raises NameError because number is
# local to the comprehension.
print(number)

Good Scope Design Practices

  • Use function parameters for inputs and return values for outputs.
  • Keep state explicit where possible instead of relying on hidden global or enclosing mutation.
  • Use descriptive names to reduce accidental shadowing.
  • Do not overwrite built-in names such as list, str, id, or max.
  • Reserve global and nonlocal for clear, limited cases where shared state is intentional.
  • Copy a mutable object before modifying it when a function should not affect its caller's object.
def add_tag(tags, tag):
    new_tags = tags.copy()
    new_tags.append(tag)
    return new_tags


original = ["python"]
updated = add_tag(original, "scope")
print(original)  # ['python']
print(updated)  # ['python', 'scope']

Common Scope Errors and Fixes

SymptomLikely causeTypical exceptionCorrection
Accessing another function's local variableThe name belongs only to the first function's callNameErrorPass it as an argument, return it, or deliberately place state in an outer scope
Using a name absent from every lookup scopeNo Local, Enclosing, Global, or Built-in binding existsNameErrorDefine the name, correct its spelling, or pass the required value
Reading a local name before assignmentAn assignment anywhere classified the name as localUnboundLocalErrorRename the local, initialize it first, pass and return values, or use a suitable declaration
Unexpectedly changing shared mutable stateA list or dictionary was mutated through a shared referenceUsually no exceptionCopy the object for isolation, or document and intentionally manage the mutation

Diagnosing Typical Problems

  • If a second function receives NameError for a value created by the first, the value is local to the first function. Pass it, return it, or choose an intentionally shared scope.
  • If a global value appears unchanged after a function assigns the same spelling, the assignment probably created a local shadow. Return the new value and assign it at the caller, or use global only when appropriate.
  • If a nested function cannot update an outer value, assignment created a new local name. Declare nonlocal for an existing enclosing binding, or return the updated value instead.
  • If assigning to list or str causes confusing failures, a built-in was shadowed. Rename the variable and restart the interactive session if the bad binding remains active.

Exam- and Debugging-Relevant Rules

  1. LEGB means Local, Enclosing, Global, Built-in.
  2. Reading an accessible global does not require global; rebinding it does.
  3. An assignment anywhere in a function makes that name local throughout the function unless declared global or nonlocal.
  4. global targets the current module; nonlocal targets the nearest enclosing function scope.
  5. Mutation of a shared list or dictionary is different from rebinding the name that refers to it.
  6. NameError means lookup found no accessible binding. UnboundLocalError means Python expected a local binding, but it was read before assignment.

For related practice, review assignment operators, importing modules, Python lists, Python dictionaries, and types of Python errors.