Why Is "Variable Not Defined" in Python? Understanding NameError From the Inside Out

If you have ever written Python code, you have almost certainly been greeted by this unwelcome message:

NameError: name 'my_variable' is not defined

It is one of the most frequently searched Python errors on the internet, and for good reason. Whether you are a first-week beginner or a developer with years of experience, NameError has a way of showing up when you least expect it. But this error is not random. It is a direct consequence of how Python was designed to resolve names, and once you understand the mechanics behind it, you will rarely be confused by it again.

This article goes deeper than the typical "check your spelling" advice. We are going to trace the error back to the language specification itself, walk through the scoping rules that govern every name lookup Python performs, examine the relevant Python Enhancement Proposals (PEPs) that shaped those rules, look at what the bytecode actually does when things go wrong, and build real understanding through code you can run and experiment with. We also cover several edge cases that go unmentioned in most treatments of this error.

What Exactly Is a NameError?

A NameError is raised when Python's interpreter encounters a name -- a variable, function, class, or module reference -- that it cannot find in any of the namespaces currently available during execution. Python does not require you to declare variables before using them the way languages like C or Java do. Instead, a variable comes into existence the moment you assign a value to it. If you reference a name before any assignment has happened, Python has no record of it, and it raises a NameError.

This is a deliberate design choice. Guido van Rossum, the creator of Python, described the philosophy behind the language as an experiment in how much freedom programmers need -- too much and nobody can read another's code; too little and expressiveness suffers. Python chose a middle path: no explicit declarations, but strict enforcement at runtime when a name cannot be found.

The Zen of Python (PEP 20), authored by Tim Peters and posted to the Python mailing list in 1999, captures the philosophical backbone of this design. Two of its aphorisms are particularly relevant here:

  • "Explicit is better than implicit."
  • "Namespaces are one honking great idea -- let's do more of those!"

These principles shaped every decision about how Python resolves variable names, and every NameError you encounter is the language enforcing those principles. The error is not Python failing you. It is Python being precise.

The Root Causes (With Real Code)

Let's walk through every common scenario that produces this error, with code you can run yourself.

1. Using a Variable Before Assigning It

This is the most fundamental cause. Python creates a variable at the point of assignment, not at the point of reference.

# This will raise NameError
print(greeting)

greeting = "Hello, world!"
NameError: name 'greeting' is not defined

Python executes code from top to bottom. When it reaches print(greeting), it searches for the name greeting in the current namespace, finds nothing, and raises the error. The fix is straightforward: move the assignment above the first usage.

greeting = "Hello, world!"
print(greeting)  # Works perfectly

2. Typos and Case Sensitivity

Python is case-sensitive. The names username, Username, and USERNAME are three entirely separate variables. A single mistyped character creates a reference to a name that does not exist.

user_name = "Ada Lovelace"
print(user_Name)  # Capital 'N' -- this is a different name entirely
NameError: name 'user_Name' is not defined

This is arguably the most common trigger in real-world code. Python treats user_Name as a completely different identifier from user_name. There is no fuzzy matching at assignment time. Either the name exists in the namespace exactly as written, or it does not.

3. Scope Violations: The LEGB Rule

This is where things get genuinely interesting, and where many intermediate developers get tripped up. Python resolves names using what is known as the LEGB rule, a lookup order that checks four namespace layers:

  • Local -- the innermost function scope
  • Enclosing -- any enclosing function scopes (for nested functions)
  • Global -- the module-level scope
  • Built-in -- Python's built-in names like print, len, range

If a name is not found in any of these four layers, Python raises a NameError.

def calculate_tax():
    tax_rate = 0.07
    total = price * tax_rate  # 'price' is not defined in any accessible scope
    return total

calculate_tax()
NameError: name 'price' is not defined

The variable price was never defined in the local scope, the enclosing scope, or the global scope. Python has no way to resolve it.

Here is a subtler example that catches many people off guard:

def outer():
    message = "hello from outer"

    def inner():
        print(message)  # This works -- 'message' is in the enclosing scope

    inner()

outer()  # Prints: hello from outer

This works because of PEP 227 -- Statically Nested Scopes, authored by Jeremy Hylton and introduced as an opt-in in Python 2.1 (via from __future__ import nested_scopes), then made the permanent default in Python 2.2. Before PEP 227, Python only checked three namespaces: local, global, and built-in. If function A was defined inside function B, names from B were invisible inside A. PEP 227 addressed this by introducing lexical scoping, meaning inner functions can now see variables defined in their enclosing functions. According to Python's own release notes, this was described as the largest change introduced in the Python 2.1 era.

But here is the trap -- variables defined inside a function are invisible outside of it:

def set_status():
    status = "active"

set_status()
print(status)  # NameError: name 'status' is not defined

The variable status lives only inside set_status(). Once the function returns, its local namespace is destroyed. The global scope never knew status existed.

4. The UnboundLocalError Cousin

Closely related to NameError is UnboundLocalError, which occurs when Python's compiler detects that a name is assigned somewhere in a function and therefore treats it as local throughout the entire function, even before the assignment executes.

counter = 10

def increment():
    counter = counter + 1  # UnboundLocalError!
    return counter

increment()
UnboundLocalError: local variable 'counter' referenced before assignment

Because counter appears on the left side of an assignment inside the function, Python's compiler marks it as a local variable for the entire function body. When execution reaches counter + 1, the local counter has not been assigned yet, and the error is raised.

This is where PEP 3104 -- Access to Names in Outer Scopes becomes essential. Authored for Python 3.0, PEP 3104 introduced the nonlocal keyword, which lets you explicitly tell Python that a variable belongs to an enclosing scope rather than the local one. The PEP documents that Guido van Rossum preferred this approach and specifically favored nonlocal as the keyword name for this declaration.

counter = 10

def increment():
    global counter  # Tells Python: use the global 'counter'
    counter = counter + 1
    return counter

print(increment())  # 11

For enclosing (non-global) scopes, use nonlocal:

def make_counter():
    count = 0

    def increment():
        nonlocal count  # Refers to 'count' in make_counter's scope
        count += 1
        return count

    return increment

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

The augmented assignment operators +=, -=, *=, and similar forms are equivalent to full assignments for the purpose of scope classification. Writing x += 1 inside a function tells the compiler that x is local to that function, even if a global x exists. This is a frequent source of surprising UnboundLocalError messages.

5. Forgetting to Import a Module

Every module you use must be imported before you reference it. This might seem obvious, but it is remarkably easy to forget, especially when jumping between files or prototyping quickly.

result = math.sqrt(144)  # NameError: name 'math' is not defined

The fix:

import math
result = math.sqrt(144)  # 12.0

A related trap is star imports creating invisible namespaces. When you write from module import *, names are pulled into the current namespace without being listed explicitly. If the module later changes its exports, names that were previously available can disappear. This is one of several reasons why PEP 8 advises against wildcard imports outside of interactive sessions.

6. Forgetting Quotes Around Strings

This one catches beginners constantly. If you write a word without quotes, Python interprets it as a variable name, not a string.

name = Alice  # NameError: name 'Alice' is not defined

Python looks for a variable called Alice. Since none exists, it raises the error. The fix is simply to wrap the value in quotes:

name = "Alice"

7. Conditional or Exception-Dependent Definitions

A subtler category: if a variable is only assigned inside a conditional branch or a try block, it may never get assigned if that branch does not execute.

def load_config(path):
    try:
        result = open(path).read()
    except FileNotFoundError:
        print("Config not found")

    return result  # UnboundLocalError if the file was not found

If the exception fires, result is never assigned, but the return statement still references it. The defensive fix is to initialize the variable before the try block:

def load_config(path):
    result = None
    try:
        result = open(path).read()
    except FileNotFoundError:
        print("Config not found")
    return result

The PEP 526 Gotcha: Type Annotations and Local Scope

PEP 526 -- Syntax for Variable Annotations, finalized for Python 3.6, introduced a subtle behavior that can produce confusing errors. As the PEP specifies, annotating a local variable causes the interpreter to treat it as local, even if it was never assigned a value.

This means the following code behaves differently depending on whether the annotation is present:

def f():
    a: int
    print(a)  # UnboundLocalError: local variable 'a' referenced before assignment

Without the annotation a: int, this same code would raise a NameError instead, because a would not be recognized as a local variable at all. The annotation tells the compiler that a is local, so it becomes an UnboundLocalError -- the variable exists in the local namespace conceptually, but was never assigned a value.

Pro Tip

Type annotations do not assign values. They only declare intent to the compiler and to type-checking tools. Always pair an annotation with an assignment if you need the variable to hold a value. Use a: int = 0 rather than a bare a: int when the variable needs to be readable immediately.

The except Clause Variable Deletion Trap

This is one of the least-documented causes of NameError and it surprises developers of every experience level. When you name an exception in an except clause, Python automatically deletes that name after the clause exits. This is specified in the Python language reference and is intentional, designed to break reference cycles since exception objects hold references to the traceback, which holds references to the local scope.

try:
    result = 1 / 0
except ZeroDivisionError as e:
    print(f"Caught: {e}")

print(e)  # NameError: name 'e' is not defined

The name e is deleted the moment the except block exits, even though it was perfectly accessible inside it. If you need to preserve the exception information after the block, capture it explicitly before the block ends:

saved_error = None
try:
    result = 1 / 0
except ZeroDivisionError as e:
    saved_error = e  # Save it before 'e' gets deleted

print(saved_error)  # ZeroDivisionError: division by zero

This behavior is governed by the as binding in except clauses and is documented in the Python language reference under compound statements. The automatic deletion was introduced to prevent common memory leaks in code that catches exceptions inside loops or long-running functions.

Walrus Operator Scope: A Modern Wrinkle

Python 3.8 introduced the walrus operator (:=), formally called the assignment expression, via PEP 572. It assigns a value to a name as part of a larger expression. But its scoping rules are deliberately different from regular assignment -- and that difference can produce NameError in unexpected places.

In a list comprehension, regular assignment variables are scoped to the comprehension itself and are invisible outside it:

squares = [x * x for x in range(5)]
print(x)  # NameError: name 'x' is not defined  (Python 3 behavior)

The walrus operator, by contrast, assigns to the enclosing scope -- the scope containing the comprehension, not the comprehension's own scope:

results = [y := x * x for x in range(5)]
print(y)  # 16 -- the last value assigned by the walrus operator

This means y is visible after the comprehension exits, while x is not. The asymmetry is intentional: PEP 572 designed walrus assignments to "leak" into the enclosing scope precisely so they can be used to communicate a value out of a comprehension. However, if the comprehension never executes (for example, if the iterable is empty), the walrus-assigned variable will not exist at all, and referencing it afterward raises a NameError:

results = [y := x for x in []]  # Empty iterable
print(y)  # NameError: name 'y' is not defined

This is a particularly tricky case because the code looks syntactically complete. There is no obvious error on the line with the assignment. The problem only surfaces at runtime when the iterable happens to be empty.

Note

The walrus operator is also not permitted at the top level of a comprehension's iteration expression. It can only appear inside the filter or transform parts. Attempting to use it in the wrong position produces a SyntaxError at parse time, not a NameError at runtime.

How Python Got Better at Telling You What Went Wrong

For years, the NameError message was bare-bones: NameError: name 'foo' is not defined. Period. Starting with Python 3.10, CPython (the standard interpreter) began offering "Did you mean..." suggestions when it detected a close match. This work was driven primarily by Pablo Galindo Salgado, who also served as release manager for Python 3.10 and 3.11.

In Python 3.10, you started seeing messages like:

>>> pint
NameError: name 'pint' is not defined. Did you mean: 'print'?

Python 3.12 expanded this in two important ways. First, if you use a standard library module name without importing it, the interpreter now suggests the import. Second, if you reference a name inside a class method that matches an instance attribute, the error message suggests using self:

class Player:
    def __init__(self):
        self.score = 0

    def show(self):
        print(score)  # NameError

In Python 3.12+, this produces:

NameError: name 'score' is not defined. Did you mean: 'self.score'?

And for forgotten imports:

>>> sys.version_info
NameError: name 'sys' is not defined. Did you forget to import 'sys'?

Python 3.13, released in October 2024, continued this trend by adding color to tracebacks by default (controllable via the PYTHON_COLORS and NO_COLOR environment variables). This makes it significantly easier to visually distinguish the error type, the affected line, and the suggestion from one another in a terminal. Python 3.13 also improved suggestions for incorrect keyword arguments passed to functions, extending the "did you mean" concept beyond names to parameter spelling.

These improvements are built on top of the foundation laid by PEP 657 -- Include Fine Grained Error Locations in Tracebacks (Python 3.11), which added column-level caret indicators to tracebacks, making it far easier to pinpoint exactly where an error occurred on a given line. The earlier PEP 678 -- Enriching Exceptions with Notes (also Python 3.11) provided the mechanism for attaching additional hint information to exception objects themselves.

How Python Resolves Names Under the Hood

When you write print(x), Python does not just "look for x." It follows a precise, deterministic process defined by the language specification.

At compile time (yes, Python does have a compilation step -- it compiles source code to bytecode), the compiler analyzes every function body and classifies each name into one of three categories:

  1. Local -- the name is assigned somewhere in this function (and no global or nonlocal declaration overrides it)
  2. Global -- the name is declared with the global keyword, or it appears at module level
  3. Free -- the name is used but not assigned in this function, so it must come from an enclosing scope

This classification happens before any code runs. That is why the UnboundLocalError trap works the way it does: the compiler has already decided a variable is local based on the presence of an assignment statement, even if that assignment is on the last line of the function.

At runtime, Python uses different bytecode instructions depending on the classification. LOAD_FAST is used for local variables that are known to be initialized. Starting in Python 3.12, the compiler also emits a distinct LOAD_FAST_CHECK instruction in cases where a local variable might not be initialized -- this instruction performs an explicit null check and raises UnboundLocalError with a clear message if the slot is empty, rather than failing silently or with a less informative error. LOAD_GLOBAL handles global and built-in lookups, and LOAD_DEREF handles free variables accessed through closures. If any of these instructions fails to find a value, the corresponding error is raised.

Note

PEP 227 describes the name resolution rules as typical for statically scoped languages, with three notable exceptions. First, names in class scope are not accessible to nested functions -- the resolution process skips class definitions, which is why methods must use self or the class name to access class-level names. Second, the global statement short-circuits the normal rules entirely, bypassing enclosing function scopes. Third, variables are not declared: a name springs into existence through assignment, not through a declaration statement. That third exception is responsible for the majority of NameError and UnboundLocalError occurrences.

Using the dis Module to See It Yourself

You do not have to take the bytecode explanation on faith. Python's standard library includes the dis module, which disassembles bytecode into human-readable form. You can use it to directly observe the compiler's scope decisions.

import dis

counter = 10

def increment():
    counter = counter + 1
    return counter

dis.dis(increment)

The output will show something like this:

  2           LOAD_FAST_CHECK          0 (counter)
              LOAD_CONST               1 (1)
              BINARY_OP                0 (+)
              STORE_FAST               0 (counter)
  3           LOAD_FAST                0 (counter)
              RETURN_VALUE

Notice that counter is accessed with LOAD_FAST_CHECK (or LOAD_FAST in older Python versions), not LOAD_GLOBAL. The compiler has classified it as a local variable because of the counter = ... assignment later in the function. When the function runs, that first load fails because the local slot for counter has not been filled yet, and the error is raised. The disassembly proves that Python made this decision at compile time, before a single line of the function body executed.

Now compare this to a version that uses global:

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

dis.dis(increment_global)
  2           LOAD_GLOBAL              0 (counter)
              LOAD_CONST               1 (1)
              BINARY_OP                0 (+)
              STORE_GLOBAL             0 (counter)
  3           LOAD_GLOBAL              0 (counter)
              RETURN_VALUE

Now the lookup is LOAD_GLOBAL, which finds the module-level counter correctly. The dis module is one of the most underused tools in the Python standard library. When an error confuses you, disassembling the function that raised it will often reveal the compiler's reasoning immediately.

The Class Scope Exception

There is one scoping rule that surprises nearly everyone the first time they encounter it. Class-level scope does not participate in the LEGB lookup for nested functions.

class Config:
    timeout = 30

    def show_timeout(self):
        print(timeout)  # NameError: name 'timeout' is not defined

Config().show_timeout()

Even though timeout is defined at the class level and show_timeout is a method of that class, the method cannot see timeout as a bare name. You must access it through self.timeout or Config.timeout.

PEP 227 explains this design decision: class-level names create attributes on the class object, and accessing them inside a method requires an explicit attribute reference through either self or the class name. This rule prevents ambiguous situations where it might be unclear whether a bare name refers to a local variable, an inherited attribute, or a class-level assignment. The explicitness requirement aligns directly with the Zen of Python principle that explicit is better than implicit.

This same rule applies to class methods, static methods, and nested class definitions. There is no way around it -- class scope simply does not participate in the E of LEGB for the functions nested inside it.

A Diagnostic Checklist

When you encounter NameError: name 'x' is not defined, work through this sequence:

  1. Verify spelling and case. Python treats myVar, myvar, and MyVar as three separate names. Read the traceback line carefully and compare it character by character against the definition.
  2. Check definition order. Is the variable assigned above the line where the error occurs? Python executes top to bottom.
  3. Examine scope boundaries. Is the variable defined inside a function, loop, or conditional block that does not share scope with the line that references it?
  4. Check your imports. If the name refers to a module or something from a module, make sure the import statement is present and correct.
  5. Look for conditional assignment. Is the variable only assigned inside an if, try, or loop body that might not execute?
  6. Check for accidental shadowing. Is there an assignment to the same name later in the function that causes Python to treat it as local throughout?
  7. Inside a class method? Remember that class-level attributes require self. or the class name as a prefix.
  8. Check except clause variables. Did you reference the exception name (as e) after the except block ended? Python deletes it automatically.
  9. Using a walrus operator in a comprehension over a potentially empty iterable? If the iterable is empty, the walrus-assigned variable will not exist after the comprehension.
  10. Disassemble if still stuck. Run import dis; dis.dis(your_function) and look at which bytecode instruction Python uses to load the problematic name. LOAD_FAST or LOAD_FAST_CHECK means the compiler classified it as local; LOAD_GLOBAL means global. That alone will tell you whether you need global, nonlocal, or a plain assignment above the reference.

Preventing NameError Proactively

Beyond the diagnostic checklist, several tools and practices can catch these errors before your code runs.

Static analysis tools like mypy, pyflakes, and Pylint can detect undefined name references at analysis time, before you ever run the program. These tools read your source code, build a model of which names are defined where, and flag references to names that do not exist. If you use an IDE like PyCharm or VS Code with the Pylance extension, many of these checks happen in real time as you type. Pyflakes in particular is fast and focused purely on logical errors like undefined names, unused imports, and shadowed variables -- it makes an excellent addition to a pre-commit hook.

PEP 526's variable annotation syntax, while it does not prevent errors on its own, makes your intent explicit enough that type checkers can catch mismatches. Combining annotations with a type checker like mypy means that an undefined variable reference will surface as a warning or error during analysis, not at runtime. The key is to pair every annotation with an assignment when the variable needs an initial value.

Writing unit tests also helps considerably. A function that references an undefined variable will raise NameError the moment that code path executes. If your test suite covers the function and exercises the relevant branches, the error will surface during testing rather than in production. This is especially important for the conditional-assignment trap: tests that exercise the exception path in a try/except block are the most reliable way to catch UnboundLocalError issues that only appear when the try succeeds on developer machines.

For teams, a linter enforced in CI -- whether through GitHub Actions, GitLab CI, or any other pipeline -- means no undefined-name reference survives a code review. The ruff linter, which has largely replaced standalone flake8 and pyflakes in many projects due to its speed, includes the F821 rule for undefined names and runs in milliseconds even on large codebases.

Conclusion

The NameError is not a bug in Python. It is the language doing exactly what it was designed to do: enforcing explicit name resolution across well-defined namespace boundaries. Every one of the scoping rules that can produce this error exists for a reason, and those reasons are documented in the PEPs that shaped the language over more than two decades.

PEP 227 gave us nested scopes so that inner functions could finally access their enclosing context. PEP 3104 gave us the nonlocal keyword so that we could write to those enclosing scopes, not just read from them. PEP 526 gave us variable annotations, but with the honest caveat that annotations are not assignments. PEP 572 gave us the walrus operator's useful but asymmetric scoping semantics. And PEP 20, the Zen of Python, continues to remind us that namespaces, explicit behavior, and readability are not accidents of the language. They are the point.

The improvements from Python 3.10 through 3.13 reflect a matured understanding of how developers actually read errors: better suggestions, colored output, column-level precision, and context-aware hints like "did you forget to import?" and "did you mean self.x?" have collectively made Python's error messages some of the most instructive of any language in active use today.

The next time Python tells you a variable is not defined, do not just fix the immediate error. Use dis to look at the bytecode if the cause is not obvious. Identify which namespace rule was enforced. That understanding is the difference between a developer who patches symptoms and one who writes code that does not produce them in the first place.

back to articles