2025, Dec 19 05:00
Understanding Python globals across module namespaces: wildcard imports, __main__, and object mutation
Learn why Python globals behave differently after a wildcard import: module namespace vs __main__, assignment vs mutation, and how to avoid REPL surprises.
Globals in Python can be deceiving when a module is imported into an interactive session. A small script behaves one way when executed as a file, and another when you call its function by hand after a wildcard import. The root cause is not in the keyword itself, but in namespaces and what exactly the name “global” resolves to inside a module.
Reproducing the behavior in a minimal script
Consider a short file named modscope_demo.py. Running it as a script prints the expected before/after state because the function executes at import time in that context.
items = []
def bump():
global items
items = [20, 30, 40]
print("before ", items)
bump()
print("after ", items)
Executing it from the shell shows the empty list, then the reassigned one. Importing with a wildcard also triggers the top-level prints, so you see a similar outcome for that import step.
The surprise when you call the function by hand
Now import the names into the interactive session and call the function yourself. The list you look at in the REPL doesn’t change, even though the function ran.
>>> from modscope_demo import *
>>> items
[]
>>> bump()
>>> items
[]
That seems wrong at first glance, but it’s consistent with how Python binds names across module boundaries.
What is actually happening
Inside a module, global refers to the module’s own namespace. In other words, global items inside modscope_demo resolves to the name items stored in the modscope_demo module object. When you do from modscope_demo import *, Python copies those names into the current module’s namespace, which in an interactive session is __main__. The two names, __main__.items and modscope_demo.items, can refer to the same underlying object at a moment in time, but they remain different names in different namespaces.
Assignment breaks that shared reference. The function bump assigns a new list object to the module’s items. The name items in __main__ still points to the original empty list. You can make the object identity visible with id().
things = []
def bump():
global things
print(things, id(things))
things = [20, 30, 40]
print(things, id(things))
# helper to view the module-level binding again
def probe():
print(things, id(things))
If you import into the REPL and call bump, you will observe that the id printed before the assignment matches the id of the empty list you imported, and the id after the assignment is different. Inspecting things in the REPL still shows the original object, while calling probe from the module shows the new one. The names are separate; only the object reference was previously shared.
Mutation vs. assignment: why one propagates and the other doesn’t
If the function mutates the existing object rather than rebinds the name to a new object, both namespaces reflect the change because they still reference the same list object.
pool = []
def bump():
global pool
print(pool, id(pool))
pool.extend([20, 30, 40]) # mutate the existing list in place
print(pool, id(pool))
def probe():
print(pool, id(pool))
After importing into the REPL, the ids reported before and after remain the same, and the list observed in the interactive session changes as well. No new object was created; the original list was modified in place.
See the namespace boundary directly
The difference becomes clearer if you import the module itself and access its attributes explicitly instead of copying names into __main__.
# modscope_demo.py
box = []
def bump():
global box
box = [20, 30, 40]
>>> import modscope_demo
>>> modscope_demo.box
[]
>>> box = 5 # a name in __main__
>>> box
5
>>> modscope_demo.box # the module's name is separate
[]
>>> modscope_demo.bump()
>>> box # __main__ stays untouched
5
>>> modscope_demo.box # the module's binding changed
[20, 30, 40]
How to resolve the confusion
The behavior follows directly from Python’s scoping rules. A global inside a module targets that module’s own dictionary of names. A wildcard import creates separate names in the caller’s namespace; they are not aliases that track future rebindings inside the original module. If you need the caller to observe changes performed inside the module using assignment, refer to the object through the module namespace, for example modscope_demo.items or modscope_demo.box. If your intent is to update the contents of a pre-existing mutable object so that every reference sees the change, mutate it in place rather than assign a new object.
Why this matters
Understanding the distinction between names and objects, and between module namespaces and __main__, prevents subtle bugs in interactive work, in tests, and when wiring modules together. It also clarifies why a function that “uses global” appears to have no effect after a wildcard import, and why using in-place mutation produces the expected shared behavior.
Takeaways
Global names inside a module belong to that module. Wildcard imports copy those names into __main__, creating a second binding that can drift apart when assignment occurs in the module. If you want to observe the module’s current state, access it through the module object. If you want multiple references to reflect an update, mutate the shared object instead of rebinding the name.