Maintenance

Site is under maintenance — quizzes are still available.

Go to quizzes

Python Scope and Namespaces

Master Python scope and namespaces: how variable lookup works in LEGB rule, practical examples, and common pitfalls. Hands-on lesson for Python developers.

Focus: python scope and namespaces

Sponsored

Have you ever written a function, only to be baffled when a variable inside it doesn't behave as you expected? Or tried to debug a frustrating "UnboundLocalError" or a simple name error that seems to appear out of nowhere? These are the classic symptoms of not understanding Python's scope and namespaces — the invisible rules that dictate where your variables live and how Python finds them. Mastering this concept is not just academic; it's the key to writing predictable, bug-free code and avoiding some of the most common and confusing errors in Python.

The problem this lesson solves

Without a clear mental model of variable scope, your Python code can behave in unpredictable ways. A function might unexpectedly overwrite a global variable, or you might get an error trying to access a variable you know exists. This lesson solves that confusion by giving you a concrete, step-by-step understanding of how Python manages variable names. Once you grasp the LEGB rule (Local, Enclosing, Global, Built-in), you'll be able to predict exactly how any name lookup will resolve. You'll stop writing code that works by accident and start writing code that works with intention.

Core concept / mental model

Think of Python's namespaces as a set of nested dictionaries, or like a system of filing cabinets. When your code tries to access a variable name, Python searches through these cabinets in a strict order. This search order is known as the LEGB rule:

  • Local — Inside the current function or class.
  • Enclosing — In functions that enclose the current function (e.g., outer function of a nested function).
  • Global — At the top level of the module/script.
  • Built-in — Pre-defined names in Python (like print, len, True).

Mental model: Imagine you are looking for a specific word in a book. You first check your own desk (Local). Then the bookshelf next to you (Enclosing). Then the entire library (Global). Finally, you check the dictionary on the librarian's desk (Built-in). You stop as soon as you find the word, even if it exists in multiple places.

A namespace is a mapping from names to objects. Every time a function is called, a new local namespace is created. The global namespace is created when the script starts. The built-in namespace is always available. The key rule: assignment (the = operator) creates or updates names within the current scope, unless you explicitly use global or nonlocal to indicate otherwise.

How it works step by step

Let's walk through how Python resolves a variable name when your code runs.

  1. Start: Python encounters a name (e.g., x).
  2. Search Local: Python first checks the innermost scope — the current function's local namespace. If x is found there, the search stops immediately.
  3. Search Enclosing: If not found locally, Python looks in the namespace of any enclosing functions (outer functions of nested definitions). It will look in the nearest enclosing scope first, then the next, and so on.
  4. Search Global: If still not found, Python searches the global (module-level) namespace.
  5. Search Built-in: If x is not found in any of the above, Python finally checks the built-in namespace. If it's not there either, you get a NameError.

This process happens at runtime, not at compile time. This is why a variable defined after a function call can still be accessed inside that function if it's looked up later (as long as it's not assigned inside the function).

Hands-on walkthrough

Let's solidify this with examples. All examples assume Python 3.10+.

Example 1: Simple Local vs Global

x = "global"

def my_func():
    x = "local"
    print(f"Inside function: {x}")

my_func()
print(f"Outside function: {x}")

Expected output:

Inside function: local
Outside function: global

The x = "local" inside my_func creates a new variable in the local namespace, shadowing the global x. The global x is unchanged. This is a critical point: assignment inside a function always targets the local scope by default.

Example 2: Modifying a Global Variable (using global)

y = 10

def add_to_global(value):
    global y  # Tell Python we intend to modify the global 'y'
    y += value

global_initial = y
add_to_global(5)
print(f"Global y after: {y}")

Expected output:

Global y after: 15

Without the global y declaration, Python would raise an UnboundLocalError because the assignment y += value would make Python think y is local, but it's being used before assignment on the right-hand side.

Example 3: Nested Functions and nonlocal

def outer():
    x = "outer value"

    def inner():
        nonlocal x  # Tell Python we want to modify 'x' from the enclosing scope
        x = "inner modified"
        print(f"Inner sees: {x}")

    inner()
    print(f"Outer sees after inner: {x}")

outer()

Expected output:

Inner sees: inner modified
Outer sees after inner: inner modified
  • Without nonlocal x, the inner() function would create a new local x, leaving outer()'s x unchanged.
  • nonlocal is used to rebind a variable in the nearest enclosing scope that is not global. It's specifically for nested functions.

Example 4: Looking up a variable defined after the function

def show_z():
    print(f"Value of z is: {z}")

z = 100
show_z()

Expected output:

Value of z is: 100

This works because the function only looks up z when it is called, not when it is defined. At the time of the call, z exists in the global namespace.

Compare options / when to choose what

Approach Use Case Risk
Local variables (default inside a function) Most common — data that should be private to a function None; it's the safest default
global keyword Rarely — sharing a configuration-like state across functions Makes code harder to reason about, test, and debug; can cause unintended side effects
nonlocal keyword Only inside nested functions when you need to modify an enclosing scope's variable Can still be confusing for readers; prefer passing data as arguments/return values if possible
Arguments/return values The preferred alternative to global and nonlocal — data flows explicitly Can be verbose in deeply nested code, but clarity wins

Pro tip: If you find yourself reaching for global, ask if you can instead pass the value as a parameter and return the result. It makes your functions pure and predictable.

Troubleshooting & edge cases

  • UnboundLocalError: You get this if you try to assign to a variable inside a function without declaring it global or nonlocal, but also try to read the variable's value before the assignment. Python sees the assignment and decides the variable is local, so the read fails. Fix: Use global or nonlocal, or rename the local variable.

  • Accidentally shadowing built-in functions: If you name a local variable list or print, you'll replace the built-in function within that scope. Fix: Avoid using built-in names for your variables.

  • Mutation vs. Rebinding: If you have a global list my_list = [], calling my_list.append(1) inside a function works without global, because you are mutating the object, not rebinding the name. But my_list = [1] inside the function would create a new local variable. Fix: Understand that assignment changes the name binding; method calls on objects do not.

  • Class scope oddity: In a class definition, variable assignments create a class-local namespace, but nested functions (methods) do not see class-level variables unless accessed via self or __class__. Fix: Access class variables via self.var (if you mean instance) or Classname.var.

What you learned & what's next

You now understand the core rules of Python's scope and namespaces. You can:

  • Explain the LEGB rule and trace how Python resolves any variable name.
  • Distinguish between local, global, enclosing, and built-in namespaces.
  • Correctly use global and nonlocal when you actually need to modify a variable from an outer scope.
  • Debug common errors like UnboundLocalError and accidental shadowing.

In the next lesson, you'll build on this knowledge by exploring closures and decorators — powerful patterns that rely heavily on understanding the enclosing scope and how functions retain references to their parent environments. You are now ready to write more robust, maintainable Python code.

Practice recap

Try this: create a nested function where the inner function modifies a variable from the outer function using nonlocal. Then, create a counter that increments every time you call an inner function. Finally, try to write a function that uses global to track how many times it has been called across the module. Run everything in a script to verify the behavior.

Common mistakes

  • Forgetting that assignment creates a local variable: using global or nonlocal is required to rebind a name in an outer scope.
  • Trying to use nonlocal when the variable is in the global scope — nonlocal only works for enclosing function scopes, not the module-level global scope. Use global instead.
  • Assuming global is needed for mutable objects (like list append or dict update). Mutation does not change the binding, so global is unnecessary; it's only required for reassignment.
  • Shadowing built-in names unintentionally by naming a local variable list, dict, print, etc., which can break other parts of your code until the scope exits.

Variations

  1. Use the inspect module (inspect.currentframe().f_locals) to programmatically inspect namespaces for debugging purposes.
  2. Leverage exec() or eval() with a custom namespace dictionary (e.g., exec(code, my_globals, my_locals)) to dynamically control variable scope at runtime.
  3. Use closures (functions that capture enclosing scope) for a design pattern that replaces global usage, especially for creating stateful factories.

Real-world use cases

  • Configuration management: loading settings from a config file into a global namespace, where functions can read but not accidentally overwrite them using read-only access via __getattr__ on a module.
  • Caching/memoization: a decorator storing results in an enclosing scope (closure) to cache expensive function calls without polluting the global namespace.
  • Implementing a simple state machine by using nonlocal in nested functions to track state transitions while keeping the state variable private to the outer function.

Key takeaways

  • Python's LEGB rule defines how variable names are resolved: Local, Enclosing, Global, Built-in — search stops at the first match.
  • Assignment inside a function always creates a name in the local scope by default; use global or nonlocal to rebind in an outer scope.
  • Mutation of an object (e.g., list.append) does not require global because the name binding is unchanged; only rebinding does.
  • nonlocal works only for nested functions and modifies the nearest enclosing scope that is not global; global works at the module level.
  • Shadowing built-in names (e.g., list) can cause subtle bugs — always avoid naming your variables after built-in Python keywords and functions.

Sponsored

Sponsored

Discussion

Questions, corrections, and tips help everyone reading this page.

0 comments

Add a comment

Shown publicly with your comment.

Be constructive · max 4,000 characters

No comments yet — start the thread.

Related tutorials, quizzes, and articles for this topic.