Course
Seeing “name is not defined” or “local variable referenced before assignment” right after writing a few lines is frustrating. The code looks fine, yet Python disagrees. The root cause is almost always scope: where a name is visible and which object it refers to at that moment.
Once you understand Python’s name lookup—often summarized as the LEGB rule—these errors become predictable and easy to fix. In this tutorial, I will varibles and scopes in Python step by step.
What Is a Variable?
A variable is a name bound to an object. In Python, you create a binding with the assignment operator =. Names are not typed; they can point to any object (string, int, list, function, and so on).
customer_name = "Ava Torres"
order_count = 1
total_cents = 1 + 2 * 300 # evaluated first, then bound
Follow these rules and conventions when naming variables to avoid syntax errors and subtle bugs.
- Use letters, digits, and underscores only; names cannot start with a digit.
- Do not use keywords such as
True,for, orclassas names.
- Avoid shadowing built-ins like
list,dict,sum, ormax.
- Prefer
snake_casefor readability (PEP 8).
These examples demonstrate what not to do and the errors Python raises:
first string value = "First string" # spaces not allowed
# SyntaxError: invalid syntax
1st_value = 10 # cannot start with a digit
# SyntaxError: invalid decimal literal
True = "yes" # cannot assign to a keyword
# SyntaxError: cannot assign to True
How Python Scope Works (LEGB)
Scope is the region of code where a name is visible. When you use a name, Python looks it up in this order (LEGB):
- Local: the current function’s scope.
- Enclosing: any outer function scopes (for nested functions).
- Global: the module’s top level (module namespace).
- Built-in: names defined by Python in
builtins(for example,len,print).
Names live in namespaces (think dictionaries mapping names to objects). Scope is about which namespaces Python consults to resolve a name at a given point in code.
Local scope
Names assigned inside a function are local to that function unless declared otherwise. They are not visible outside the function.
def show_order_id():
order_id = 42
print("inside function:", order_id)
show_order_id()
print("outside function:", order_id) # NameError
Python decides scope at compile time. If a function assigns to a name anywhere in its body, all uses of that name in the function are treated as local—leading to a common UnboundLocalError when you read before assigning:
discount_rate = 0.10 # module-level (global)
def price_with_discount(amount_cents):
print("configured discount:", discount_rate) # looks local because of the assignment below
discount_rate = 0.20 # assignment makes 'discount_rate' local in this function
return int(amount_cents * (1 - discount_rate))
# UnboundLocalError: cannot access local variable 'discount_rate' where it is not associated with a value
To use the module-level name in the function, either avoid assigning to it or mark it global (covered below).
Enclosing scope (closures)
Nested functions can see names from their immediately enclosing function. To rebind such a name (not just read it), declare it nonlocal.
def make_step_counter():
count = 0 # enclosing scope for 'increment'
def increment():
nonlocal count # rebind the 'count' in the nearest enclosing function
count += 1
return count
return increment
step = make_step_counter()
print(step()) # 1
print(step()) # 2
Without nonlocal, assigning to count inside increment() would create a new local name and leave the outer count unchanged.
Global scope
Names assigned at the top level of a module live in the module’s global namespace. Any function can read them. To assign to a module-level name from inside a function, declare it global.
greeting = "Hello"
def greet_city(city_name):
print(greeting, city_name) # reads global
def set_greeting(new_greeting):
global greeting
greeting = new_greeting # rebinds global
greet_city("Nairobi") # Hello Nairobi
set_greeting("Hi")
greet_city("Nairobi") # Hi Nairobi
Use global sparingly. Prefer passing values as parameters and returning results to keep code testable and predictable.
Built-in scope (and a note on keywords)
The built-in scope contains names like len, print, and Exception. Avoid shadowing them, or you’ll lose access to the built-in for that scope.
list = [1, 2, 3] # shadows the built-in 'list' constructor
list("abc") # TypeError: 'list' object is not callable
del list # fix by deleting the shadowing name
Keywords (such as if, for, def) are part of Python’s syntax, not the built-in namespace, and can never be used as identifiers.
Blocks, Loops, and Comprehensions
Python’s block statements and comprehensions have specific scoping behaviors that often surprise developers coming from other languages.
There is no block scope for if/for/while/with
Assignments inside these blocks affect the containing scope (function or module). Loop variables also remain defined after the loop ends.
if True:
status = "ready"
print(status) # "ready"
for i in range(3):
pass
print(i) # 2 (the last value from the loop)
Comprehensions isolate their iteration variable
List, dict, and set comprehensions have their own local scope for loop variables. The iteration variable does not leak into the surrounding scope.
numbers = [1, 2, 3]
[x for x in numbers]
print("x" in globals() or "x" in locals()) # False
Python 3.12 (PEP 709) inlined comprehensions for speed while preserving this isolation; you still get clear, non-leaking loop variables with faster execution.
Using the global Keyword
Declare a name as global inside a function when you need to rebind a module-level variable. Place the declaration near the top of the function so it’s obvious which variable you are modifying.
tax_rate = 0.08
def configure_tax(rate):
global tax_rate
tax_rate = float(rate)
def total_with_tax(cents):
return int(cents * (1 + tax_rate))
configure_tax(0.10)
print(total_with_tax(1000)) # 1100
Using the nonlocal Keyword
Use nonlocal to rebind a name from the nearest enclosing function scope. This is common in closures that maintain state.
def make_accumulator(start=0):
total = start
def add(amount):
nonlocal total
total += amount
return total
return add
acc = make_accumulator()
print(acc(5)) # 5
print(acc(10)) # 15
Using nonlocal for a name not defined in an enclosing function is a SyntaxError. If the name is global, use global instead.
locals() and globals() in 2025
These functions are useful for inspection and debugging, not for updating variables. Starting in Python 3.13 (PEP 667), each call to locals() in a function returns an independent snapshot. Editing that snapshot does not change real locals. I confirmed this behavior by running the following snippet on Python 3.13:
def probe():
project = "alpha"
snap1 = locals()
snap1["project"] = "beta" # edits the snapshot only
observed = project # still "alpha"
snap2 = locals() # new snapshot
return snap1["project"], observed, snap2["project"]
print(probe()) # ('beta', 'alpha', 'alpha')
Use globals() similarly for the module namespace. Prefer explicit parameters and return values over dynamic lookups when writing application code.
Common Pitfalls and How to Fix Them
These mistakes account for most scope-related errors I see in practice.
UnboundLocalErrorfrom assigning in a function: Python treats a name as local if it’s assigned anywhere in the function. Either move the read after the assignment, rename, or add global/nonlocal as appropriate.
- Expecting block scope: Names assigned in if/for/while/with persist in the containing scope. Use tighter helper functions to limit scope.
- Shadowing built-ins: Avoid names such as list, dict, sum, id, or min. Choose descriptive names like
customer_listormin_allowed.
- Forgetting
nonlocalin closures: If you intend to update an outer function variable, declare it nonlocal. Otherwise you create a new local and the outer value doesn’t change.
- Confusing keywords with built-ins: Keywords (syntax) cannot be used as names, ever. Built-ins can be shadowed but shouldn’t be.
Best Practices That Keep Scope Simple
These habits make code easier to read, test, and debug.
- Favor small, focused functions. Define variables in the narrowest scope that works.
- Pass data in and out via parameters and return values. Minimize module-level state.
- Use closures intentionally. Document which outer names a nested function captures, and use nonlocal when you truly need to rebind.
- Choose descriptive, non-conflicting names. A leading underscore (for example,
_cache) signals internal use.
- Treat locals()/globals() as read-only diagnostics, not a configuration mechanism.
Conclusion
Python resolves names by searching Local → Enclosing → Global → Built-in scopes. Understanding that order—and when to use global or nonlocal—eliminates common errors like NameError and UnboundLocalError. Keep variables in the smallest practical scope, avoid shadowing built-ins, and use closures deliberately. With that mental model, your functions behave predictably, and scope stops being a source of surprises.

I have worked in various industries and have worn multiple hats: software developer, machine learning researcher, data scientist, product manager. But at the core of it all, I am a programmer who loves to learn and share knowledge!