Learn what variable scopes are all about and get familiar with the 'LEGB' rule. You will also deal with scenarios where you'll get to see the global and nonlocal keywords in action.
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 boundPowered by
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, or class as names.
Avoid shadowing built-ins like list, dict, sum, or max.
Prefer snake_case for 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 literalTrue="yes"# cannot assign to a keyword# SyntaxError: cannot assign to TruePowered by
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.
defshow_order_id():
order_id =42print("inside function:", order_id)
show_order_id()print("outside function:", order_id)# NameErrorPowered by
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)defprice_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 functionreturnint(amount_cents *(1- discount_rate))# UnboundLocalError: cannot access local variable 'discount_rate' where it is not associated with a valuePowered by
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.
defmake_step_counter():
count =0# enclosing scope for 'increment'defincrement():nonlocal count # rebind the 'count' in the nearest enclosing function
count +=1return count
return increment
step = make_step_counter()print(step())# 1print(step())# 2Powered by
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"defgreet_city(city_name):print(greeting, city_name)# reads globaldefset_greeting(new_greeting):global greeting
greeting = new_greeting # rebinds global
greet_city("Nairobi")# Hello Nairobi
set_greeting("Hi")
greet_city("Nairobi")# Hi NairobiPowered by
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' constructorlist("abc")# TypeError: 'list' object is not callabledellist# fix by deleting the shadowing namePowered by
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.
ifTrue:
status ="ready"print(status)# "ready"for i inrange(3):passprint(i)# 2 (the last value from the loop)Powered by
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"inglobals()or"x"inlocals())# FalsePowered by
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.08defconfigure_tax(rate):global tax_rate
tax_rate =float(rate)deftotal_with_tax(cents):returnint(cents *(1+ tax_rate))
configure_tax(0.10)print(total_with_tax(1000))# 1100Powered by
Using the nonlocalKeyword
Use nonlocal to rebind a name from the nearest enclosing function scope. This is common in closures that maintain state.
defmake_accumulator(start=0):
total = start
defadd(amount):nonlocal total
total += amount
return total
return add
acc = make_accumulator()print(acc(5))# 5print(acc(10))# 15Powered by
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:
defprobe():
project ="alpha"
snap1 =locals()
snap1["project"]="beta"# edits the snapshot only
observed = project # still "alpha"
snap2 =locals()# new snapshotreturn snap1["project"], observed, snap2["project"]print(probe())# ('beta', 'alpha', 'alpha')Powered by
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.
UnboundLocalError from 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_list or min_allowed.
Forgetting nonlocal in 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!
Learn about Scala's variables, rules for defining variables, different types of variables with multiple declaration and assignments along with the scope of a variable.
A comprehensive introductory tutorial to Python loops. Learn and practice while and for loops, nested loops, the break and continue keywords, the range function and more!
With this beginner tutorial, you'll start to explore PEP-8, Python's style guide, so that you can start formatting your code correctly to maximize its readability!
Master the basics of data analysis with Python in just four hours. This online course will introduce the Python interface and explore popular packages.
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).Follow these rules and conventions when naming variables to avoid syntax errors and subtle bugs.
True,for, orclassas names.list,dict,sum, ormax.snake_casefor readability (PEP 8).These examples demonstrate what not to do and the errors Python raises:
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):
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.
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
UnboundLocalErrorwhen you read before assigning: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.Without
nonlocal, assigning tocountinsideincrement()would create a new local name and leave the outercountunchanged.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.Use
globalsparingly. 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, andException. Avoid shadowing them, or you’ll lose access to the built-in for that scope.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/withAssignments inside these blocks affect the containing scope (function or module). Loop variables also remain defined after the loop ends.
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.
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
globalKeywordDeclare a name as
globalinside 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.Using the
nonlocalKeywordUse
nonlocalto rebind a name from the nearest enclosing function scope. This is common in closures that maintain state.Using
nonlocalfor a name not defined in an enclosing function is aSyntaxError. If the name is global, useglobalinstead.locals()andglobals()in 2025These 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:
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.customer_listormin_allowed.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.Best Practices That Keep Scope Simple
These habits make code easier to read, test, and debug.
_cache) signals internal use.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
NameErrorandUnboundLocalError. 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.