Python Namespaces: How Names Are Resolved Across Modules
Key Highlights
- A Python namespace is a mapping that connects names to objects. Variables, functions, modules, classes, and many object attributes participate in this model.
- A namespace and a scope are related but different: a namespace is where name-to-object bindings exist, while scope describes where a name can be looked up directly.
- Every module has its own namespace, so the same name can exist independently in different modules without automatically colliding.
- Python resolves ordinary names through the LEGB lookup order: Local, Enclosing, Global, and Built-in.
- Imports create name bindings. Understanding those bindings explains why import module, from module import name, and module.name behave differently.
Why Can Two Python Files Both Define value Without Breaking Each Other?
Suppose you have two modules:
project/
├── pricing.py
└── inventory.py
Both contain:
value = 100
There is no conflict.
You can write:
import pricing
import inventory
print(pricing.value)
print(inventory.value)
and get:
100
100
So where does Python keep these two value names? The answer is namespaces.
If you have ever wondered what is Python namespace, why math.sqrt works while sqrt sometimes does not, or how Python decides which name a function refers to, namespaces provide the underlying model.
The key is to stop thinking of a variable name as the object itself. Python maintains mappings between names and objects, and different parts of a program have different namespaces. Scope then determines which of those names Python considers during lookup.
What is a Namespace in Python?
A namespace is a mapping from names to objects.
For example:
language = "Python"
version = 3
Conceptually, the namespace contains:
"language" → "Python"
"version" → 3
The names are not the objects themselves. They are references or bindings associated with objects.
If you later write:
language = "Rust"
the binding changes:
"language" → "Rust"
The original "Python" object is not necessarily destroyed; it can continue to exist if something else refers to it. This is the basic namespace meaning that appears throughout Python: names are mapped to objects.
Namespace vs Scope: They Are Not the Same
When learning Python, this distinction might be quite confusing. Where names are mapped or kept is described by a namespace. Where a name is immediately accessible for search is indicated by a scope.
For example:
message = "Hello"
def greet():
print(message)
greet()
The module's namespace has the name message. Because the function's name lookup criteria incorporate the global namespace of the surrounding module, Python may still resolve messages within greet().
So a useful distinction is:
Namespace → name-to-object mapping
Scope → region/context in which a name can be looked up
This is why namespace and scope in Python should be learned together without treating them as synonyms.
What is Scope in Python?
If you are asking what is scope in Python, think about name visibility during lookup.
Consider:
x = "global"
def show():
x = "local"
print(x)
show()
print(x)
Output:
local
global
The function has its own local binding for x. The module has its own global binding for x. The two names have the same spelling but belong to different namespaces associated with different scopes. This is the foundation of variable scope in Python.
Four Main Levels of Name Lookup
Python's ordinary name lookup is commonly summarized using LEGB:
L → Local
E → Enclosing
G → Global
B → Built-in
Consider:
x = "global"
def outer():
x = "enclosing"
def inner():
x = "local"
print(x)
inner()
outer()
The output is:
local
Why?
Python finds x in the local namespace of inner() first. If it were not there, Python could continue outward to the enclosing function, then the module's global namespace, and finally the built-in namespace.
The simplified lookup order is:
Local
↓
Enclosing
↓
Global
↓
Built-in
This is one of the most useful connections between Python scope and namespaces.
Local Namespaces Belong to Function Calls
Every function call gets its own local execution context.
Consider:
def calculate(number):
result = number * 2
return result
When you call:
a = calculate(10)
b = calculate(20)
the two calls do not share the same local number and result bindings.
Conceptually:
First call:
number → 10
result → 20
Second call:
number → 20
result → 40
This is why the same function can safely use local variable names on repeated calls. The local bindings belong to the individual function execution, not to one permanent global namespace.
Module Namespaces: The Key to Understanding Imports
Every Python module has its own namespace. Suppose settings.py contains:
APP_NAME = "Task Tracker"
DEBUG = True
def show():
print(APP_NAME, DEBUG)
The module namespace contains bindings such as:
APP_NAME → "Task Tracker"
DEBUG → True
show → function object
Now another module does:
import settings
The name settings is bound in the current module's namespace to the settings module object.
Then:
settings.APP_NAME
accesses APP_NAME through the namespace associated with the settings module. This is why modules provide a natural boundary for names.
How Names are Resolved Across Modules
This is the heart of the topic.
Imagine:
pricing.py
value = 100
def calculate():
return value * 2
inventory.py
value = 50
main.py
import pricing
import inventory
print(pricing.value)
print(inventory.value)
print(pricing.calculate())
Output:
100
50
200
There are three relevant namespaces here:
main namespace
pricing → pricing module
inventory → inventory module
pricing namespace
value → 100
calculate → function
inventory namespace
value → 50
When Python evaluates:
pricing.value
it first resolves pricing in the current namespace. That gives it the module object. Then .value accesses the attribute associated with that module.
For:
inventory.value
the same process occurs with a different module. The identical name value is safe because the bindings live in different namespaces.
Why import module and from module import name Are Different
Consider:
import math
This binds:
math → math module object
in the current namespace.
So you write:
math.sqrt(25)
The current namespace provides math, and then attribute access finds sqrt on the module.
Now compare:
from math import sqrt
This binds:
sqrt → sqrt function
in the current namespace.
So you write:
sqrt(25)
But this does not necessarily create a current-namespace name called math.
For example:
from math import sqrt
print(sqrt(25))
works.
But:
print(math.sqrt(25))
does not work merely because sqrt was imported from math. The module may be loaded internally, but the important point for namespace reasoning is that the current namespace receives the name specified by the import form.
Imports are Name Bindings
Consider these examples:
import math
creates:
math → math module
import math as m
creates:
m → math module
from math import sqrt
creates:
sqrt → sqrt function
from math import sqrt as square_root
creates:
square_root → sqrt function
This is an important way to understand imports.
An import does not simply mean:
"Make this file available."
It also determines which names are introduced into the current namespace. That distinction becomes especially useful when working with modules and debugging unexpected NameError exceptions.
Why math.sqrt Works but sqrt May Not
Suppose:
import math
Then:
math.sqrt(16)
works. But:
sqrt(16)
raises:
NameError
unless sqrt was independently defined or imported.
Why?
Because your namespace contains:
math → math module
It does not contain:
sqrt → function
The expression:
math.sqrt
performs two steps conceptually:
1. Find math in the current namespace
2. Find sqrt through the math object's attributes
This is why dot notation is so important when understanding namespaces across modules.
Module Attributes Are Namespace Entries
Suppose config.py contains:
DEBUG = True
TIMEOUT = 30
Then:
import config
allows:
print(config.DEBUG)
print(config.TIMEOUT)
The module exposes those names as attributes. You can inspect the module namespace:
print(vars(config))
You will see many entries in addition to your explicitly defined names because modules also have standard attributes.
The useful mental model is:
module object
↓
module namespace
↓
names such as DEBUG, TIMEOUT, functions, classes...
So when you see:
config.DEBUG
think of it as accessing a name associated with the module object.
Packages Also Have Namespaces
Packages follow the same basic namespace model.
Consider:
project/
└── shop/
├── __init__.py
├── products.py
└── orders.py
Suppose shop/__init__.py contains:
APP_NAME = "Shop"
Then:
import shop
print(shop.APP_NAME)
works because shop is a package module object with its own namespace.
If you import:
import shop.products
the package can expose the relevant submodule through its attributes.
The important idea is:
A package is not merely a folder at runtime. It is represented by a module object with namespace behavior. This is one reason namespace thinking makes module and package imports much easier to understand.
Dotted Names Are a Form of Namespace Navigation
Consider:
app.users.models.User
At a high level, each dot moves you through an attribute relationship:
app
↓
users
↓
models
↓
User
For example:
import app.users.models
user = app.users.models.User()
Conceptually:
current namespace
app → app package
app namespace
users → app.users package
app.users namespace
models → app.users.models module
app.users.models namespace
User → class
This model explains why fully qualified names can be so useful: the path itself communicates where a name belongs.
Why Namespaces Prevent Name Collisions
Imagine two modules:
reports.py
name = "Annual Report"
users.py
name = "Ada"
Both can coexist:
import reports
import users
print(reports.name)
print(users.name)
There is no collision because the names are separated by module namespaces. The same principle appears with objects:
customer.name
product.name
company.name
All three can use name. The name is meaningful within its particular namespace. This is one of the major reasons namespaces are essential for large Python programs: names do not have to be globally unique across the entire application.
What Happens When a Name Is Shadowed?
Consider:
name = "Python"
def show():
name = "Java"
print(name)
show()
print(name)
Output:
Java
Python
The local name shadows the module-level name during lookup inside show(). The global binding still exists. Python simply finds the local binding first. The same thing can happen with built-in names.
For example:
list = [1, 2, 3]
Now:
list("abc")
fails because the name list resolves to your variable before Python reaches the built-in list type. The problem is not that Python's built-in disappeared. Your name is being found earlier in the lookup chain.
The Built-In Namespace
Python provides a built-in namespace containing names such as:
print
len
range
str
int
list
dict
Exception
That is why this works without an import:
print(len([1, 2, 3]))
When a plain name cannot be found in the local, enclosing, or global namespaces, Python can look in the built-in namespace.
So the simplified lookup chain is:
Local
↓
Enclosing
↓
Global
↓
Built-in
This is the LEGB model. Understanding it as a chain of namespaces makes what is scope in Python much easier to answer than simply memorizing four letters.
Enclosing Namespaces and Nested Functions
Consider:
def outer():
message = "Hello"
def inner():
print(message)
inner()
outer()
message is not local to inner().
It belongs to the enclosing function's namespace. When inner() looks for message, Python can find it in the enclosing scope.
The lookup conceptually moves through:
inner local
↓
outer enclosing
↓
module global
↓
built-in
This is also the foundation for closures.
For example:
def make_greeter():
message = "Hello"
def greet():
print(message)
return greet
hello = make_greeter()
hello()
Through its closure, the returning function can maintain access to the surrounding binding even after make_greeter() has returned. This shows that while namespace lifetime and object lifetime are connected, they are not always the same.
global Changes Where an Assignment Goes
Consider:
count = 0
def increment():
global count
count += 1
increment()
print(count)
Output:
1
The statement:
global count
tells Python that assignments to count inside increment() should target the module-level binding.
Without it:
count = 0
def increment():
count += 1
Python treats count as a local variable because of the assignment, resulting in an UnboundLocalError when it tries to read that local binding before it has been assigned.
The important idea is:
local assignment → current function's local namespace
global assignment → module namespace
This is a practical example of how scope determines which namespace an assignment targets.
nonlocal Targets an Enclosing Namespace
Nested functions can use nonlocal to modify a name belonging to an enclosing function.
def make_counter():
count = 0
def increment():
nonlocal count
count += 1
return count
return increment
Now:
counter = make_counter()
print(counter())
print(counter())
print(counter())
Output:
1
2
3
Here:
nonlocal count
does not target the module namespace. It targets the appropriate enclosing function namespace.
So a useful summary is:
local → current function
nonlocal → enclosing function
global → module
These keywords make the intended namespace for assignment explicit.
Namespace and Attribute Lookup Are Different
Compare:
name
with:
user.name
They may look similar, but Python does not resolve them using the same process.
Plain name
name
uses scope-based name lookup:
Local → Enclosing → Global → Built-in
Attribute access
user.name
starts with the user object and performs attribute lookup.
That process can involve:
- Instance attributes
- Class attributes
- Base classes
- Descriptors
- Attribute hooks
So do not treat:
name
and:
user.name
as two versions of the same lookup algorithm. This distinction is particularly useful when diagnosing NameError versus AttributeError.
NameError vs AttributeError
Consider:
print(username)
If Python cannot resolve username as a plain name, you may get:
NameError
Now consider:
user.username
If the user object does not provide the requested attribute through its attribute lookup rules, you may get:
AttributeError
So:
Missing plain name → NameError
Missing attribute → AttributeError
This distinction becomes much easier to understand once you separate name lookup from attribute lookup.
Inspecting Namespaces with globals()
Python provides tools that let you inspect namespace information.
For example:
x = 10
print(globals()["x"])
Output:
10
globals() returns the current module's global namespace mapping.
You can also check:
print("x" in globals())
This can be useful while learning or debugging. It is generally not a good idea to use globals() as your normal application state-management mechanism. Think of it primarily as an inspection tool.
Inspecting Local Names with locals()
Inside a function:
def show(name):
message = f"Hello, {name}"
print(locals())
show("Ada")
You may see:
{
'name': 'Ada',
'message': 'Hello, Ada'
}
This provides a view of the local names available at that point. However, do not rely on modifying locals() to create or change ordinary local variables:
def example():
locals()["x"] = 10
print(x)
That is not a reliable way to manipulate local variables. Use locals() primarily for inspection and debugging.
vars() Is Useful for Namespace Inspection
vars() can show the namespace dictionary of many objects.
For example:
class User:
pass
user = User()
user.name = "Ada"
print(vars(user))
Output:
{'name': 'Ada'}
You can also inspect a module:
import math
print(vars(math))
Or a class:
print(vars(User))
This makes vars() a useful learning tool when you want to see which names are stored directly on an object, module, or class.
Not every object has a normal __dict__, so vars() is not universally applicable.
dir() vs vars()
These two functions are related but different.
vars(user)
helps show names stored directly in a namespace dictionary when one exists.
dir(user)
attempts to list names available through the object, including inherited and special names.
For example:
print(vars(user))
print(dir(user))
The results can therefore be very different.
A useful mental shortcut is:
vars() → what is stored directly here?
dir() → what names appear available?
This distinction becomes useful when exploring unfamiliar modules, classes, and objects.
A Common Import Trap: The Module Is Loaded, but the Name Is Different
Consider:
from math import sqrt
You can inspect:
import sys
print("math" in sys.modules)
print("math" in globals())
print("sqrt" in globals())
You may find:
True
False
True
Why? The module can be present in Python's module cache while the current namespace contains only the name sqrt.
This is an important distinction:
Module loading ≠ current namespace binding
Understanding this prevents many misconceptions about how imports work.
Namespaces and Shared Module State
Suppose config.py contains:
DEBUG = True
Two modules can import it:
import config
Each importing module gets its own current-namespace binding:
module_a namespace:
config → config module
module_b namespace:
config → same config module object
The module object itself has its own namespace:
config namespace:
DEBUG → True
This is why module-level state can be shared by different parts of an application.
The important distinction is between:
- The importing module's namespace.
- The imported module's namespace.
- The module object being referenced by both.
This is another reason namespaces are central to understanding imports.
Why from module import * Can Cause Problems
Consider:
from math import *
from statistics import *
This introduces many names into the current namespace. That can make it harder to determine where a particular name came from.
Names can also collide. For example, if two imported modules expose the same name, one binding can replace the other in the current namespace.
Explicit imports are generally easier to understand:
import math
import statistics
result = math.sqrt(25)
or:
from math import sqrt
when a direct binding genuinely improves readability. A namespace prefix can add useful information rather than unnecessary verbosity.
Namespaces and Mutability Are Different Concepts
Namespaces store bindings.
Objects have their own behavior, including mutability.
Consider:
items = []
other = items
Conceptually:
items → same list object
other → same list object
Now:
items.append("Python")
changes the list object.
The namespace bindings themselves did not need to change.
But:
items = []
rebinds the name items to a different object.
So:
Mutation → change the object
Rebinding → change what a name refers to
This distinction is useful because beginners often interpret assignment and mutation as the same operation. They are not.
How Namespaces Help You Design Better Modules
A well-designed module gives related names a clear home.
For example:
payments.py
calculate_tax
process_payment
refund
users.py
create_user
deactivate_user
find_user
Then:
import payments
import users
payments.process_payment()
users.create_user()
The module prefixes communicate ownership. Compare that with importing dozens of names directly into one namespace:
from payments import *
from users import *
The second version makes the current namespace harder to reason about. Namespaces are therefore not merely an internal Python concept. They influence readability, organization, and how easily developers can understand where a name comes from.
The Complete Picture
A useful high-level model is:
Python Program
|
+---------------+---------------+
| | |
Module A Module B Module C
namespace namespace namespace
| | |
names names names
| | |
objects objects objects
Inside a function:
Module namespace
|
↓
Function call
|
↓
Local namespace
For nested functions:
Local
↓
Enclosing
↓
Global
↓
Built-in
For modules:
current namespace
|
↓
module name
|
↓
module namespace
|
↓
module attribute
This is why:
app.users.models.User
can be understood as a sequence of namespace and attribute relationships rather than a mysterious special syntax.
Final Takeaway
If you want to define namespace in one sentence:
A namespace is a mapping that associates names with objects.
But that definition becomes much more useful when connected to scope.
Scope determines where Python looks for a plain name.
Namespace provides the name-to-object bindings Python can search.
For ordinary name lookup, Python follows the familiar:
Local → Enclosing → Global → Built-in
model.
Modules introduce another crucial layer. Each module has its own namespace, which is why these can safely coexist:
pricing.value
inventory.value
Imports then create bindings in the current namespace:
import math
binds math, while:
from math import sqrt
binds sqrt.
Dot notation such as:
math.sqrt
then moves from the module object to an attribute associated with its namespace.
Once this model is clear, many Python concepts become easier to connect:
Functions → local namespaces
Nested funcs → enclosing namespaces
Modules → module namespaces
Packages → package/module namespaces
Classes → class namespaces
Instances → instance attributes
Imports → name bindings
LEGB → ordinary name lookup
globals() → inspect module globals
locals() → inspect local bindings
vars() → inspect many object namespaces
The most useful mental model is simple:
It is not necessary for names to be distinct throughout your Python application. They must be properly resolved inside the namespaces in which they reside.
This enables functions to reuse local variables across calls, objects to have their own attributes, imports to expose just the bindings you really require, and various modules to specify the same names.
Understanding namespaces therefore gives you a much clearer picture of how Python resolves names across modules, and why so many seemingly unrelated Python behaviors follow the same underlying rules.
Frequently Asked Questions
1. What is Python namespace?▾
A Python namespace is a mapping between names and the objects those names refer to. Module, local, class, and many object attribute namespaces are examples.
2. What is the difference between namespace and scope in Python?▾
A namespace describes the name-to-object mappings. Scope describes where a name can be resolved directly during lookup. They work together but are not interchangeable terms.
3. What is scope in Python?▾
The namespaces that are taken into account while resolving a plain name are determined by Python scope. LEGB stands for Local, Enclosing, Global, and Built-in.
4. How does Python resolve a name across modules?▾
When you write something like pricing.value, Python first resolves pricing in the current namespace. It then performs attribute lookup for value on the resulting module object. Each module has its own namespace, which keeps names such as value separate.
5. Why can two Python modules use the same variable name?▾
Each module has its own namespace. Therefore, pricing.value and inventory.value can refer to different objects without creating a naming conflict.
6. What is the difference between import module and from module import name?▾
import module binds the module name in the current namespace: import math Then you use: math.sqrt(25) from module import name binds the selected name directly: from math import sqrt Then you use: sqrt(25)
7. How can I inspect a Python namespace?▾
For module globals, use: globals() For local bindings, use: locals() For many objects, modules, and classes, use: vars(object) These tools are particularly useful for learning and debugging namespace behavior.


