On this page
Description
Every Python program is a set of modules - files that import each other. Most of the time imports just work, so it's easy to never think about them. But a few interview questions, and a surprising number of production bugs, come down to what an import actually does: when a module's code runs, what from x import y really gives you, why two modules that import each other can fail, and why the simplest singleton in Python is just a module.
Here's the kind of bug we mean. An online store keeps its feature flags in a module, and an admin page reloads them:
# flags.py
settings = load_flags()
# checkout.py
from flags import settings
def use_new_checkout() -> bool:
return settings["new_checkout"]
When an admin changes a flag, the admin page runs flags.settings = load_flags(). Code that reads flags.settings sees the new value at once. checkout.py never does - it keeps reading the old dictionary until the server restarts. By the end of the second section you'll know why.
We'll go from simple to more technical:
- What happens when you import a module - and why its code runs only once.
- What
import xandfrom x import ybind, and why it matters for settings and tests. if __name__ == "__main__":and circular imports.- Singletons in Python: the module,
functools.cache, and the__new__trap. importlib.reloadand module-level__getattr__.
What an import does
The first time a module is imported, Python:
- finds the file,
- creates an empty module object and stores it in
sys.modulesunder the module's name, - runs the file's top-level code, top to bottom, inside that module object.
Every later import of the same name finds it in sys.modules and returns the same object without running anything. So a module's top-level code runs once per process:
# config.py
print("loading config")
settings = {"env": "dev"}
Import config from ten different files and "loading config" prints once, and all ten share the one settings dictionary.
That makes a module the natural home for things there should be one of: configuration, a connection pool, a logger. Imports are also thread-safe: if two threads import the same module at once, the second waits until the first has finished running it.
What an import gives you
The two forms bind different things:
import configbinds the nameconfigto the module. Each time you writeconfig.settings, Python looks the attribute up again.from config import settingslookssettingsup once, at import time, and binds your own name to that object.
import config
from config import settings
config.settings = {"env": "prod"} # the module's name now points at a new dict
print(settings) # {'env': 'dev'} - your name still holds the old one
print(config.settings) # {'env': 'prod'}
That's the feature-flag bug: checkout.py used from flags import settings, so replacing flags.settings never reached it. Two ways out:
- Import the module, not the name, for anything that can be replaced while the program runs:
import flags, thenflags.settings[...]. - Change the object instead of replacing it:
flags.settings.update(load_flags())changes the one dictionary every name points at.
The same rule explains a classic testing mistake. unittest.mock.patch("payments.charge") replaces the name charge in the payments module - but if checkout.py did from payments import charge, it holds its own name, and the patch doesn't reach it. Patch where the name is used: patch("checkout.charge").
Running as a script: __main__
Every module has a __name__. When a file is imported, that's its module name ("config"). When you run a file directly - python app.py - Python runs it under the name "__main__". That's what this common guard checks:
def main():
...
if __name__ == "__main__":
main() # runs for "python app.py", not for "import app"
Use it in every file that is both a script and a module. It's essential with multiprocessing: on macOS and Windows (and by default on Linux since Python 3.14) worker processes start a fresh interpreter and import your main file, and without the guard each worker would run your startup code again and try to start workers of its own.
Circular imports
If two modules import each other at the top, one of them runs while the other is only half finished:
# shop/orders.py
from shop import payments
TAX_RATE = 0.2
# shop/payments.py
from shop.orders import TAX_RATE # orders hasn't reached this line yet
Importing shop.orders starts running it, which imports shop.payments, which asks for TAX_RATE - but orders stopped at its first line to import payments, so TAX_RATE doesn't exist yet. Python says so quite clearly: ImportError: cannot import name 'TAX_RATE' from partially initialized module 'shop.orders' (most likely due to a circular import).
Fixes, best first:
- Move the shared name into a third module that both import (
shop/rates.py). A cycle usually means two modules share something that belongs to neither. - Import inside the function that needs it, so the import happens when the function runs - long after both modules have finished loading.
- Use
import shop.ordersand readshop.orders.TAX_RATElater, inside functions. The module object exists (half-built) during the cycle; only reading the attribute too early fails.
Singletons in Python
Java needs a pattern for "exactly one instance". In Python a module already is one, so the simplest singleton is an instance created at the top of a module:
# config.py
settings = Settings.from_env() # created once, shared by every importer
# clients.py - or created lazily, on first use
from functools import cache
@cache
def get_client() -> ApiClient:
return ApiClient(...)
The Java-style alternative, overriding __new__ to return a cached instance, has a trap: Python calls __init__ on whatever __new__ returns, every time you call the class. So each Registry() gives you the same object - with its state wiped by __init__:
class Registry:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
def __init__(self):
self.items = [] # runs again on every Registry() call
r1 = Registry(); r1.items.append("a")
r2 = Registry()
print(r1 is r2, r1.items) # True []
And any lazy "create it if it's missing" check - if _instance is None: _instance = Registry() - can run in two threads at once, and both create one. Take a lock, use @cache on a function, or create the object at import time, where the import lock already makes it safe.
Reloading, and module __getattr__
importlib.reload(module) runs the module's code again inside the same module object. Every function and class in it is rebuilt as a new object. Objects created before the reload still belong to the old class, so isinstance(old_circle, shapes.Circle) becomes False - and modules that did from shapes import Circle still hold the old class. Reload is handy in a notebook; it's not a way to update a running server.
A module-level __getattr__ (Python 3.7+) runs when someone asks the module for a name it doesn't have. Two good uses: loading something heavy only when it's first used, and keeping an old name working while you rename it:
import warnings
def new_name():
...
def __getattr__(name):
if name == "old_name":
warnings.warn("old_name is deprecated; use new_name", DeprecationWarning, stacklevel=2)
return new_name
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
Which tool for which job?
| You want | Use |
|---|---|
| One shared instance | a module-level object |
| One instance, created on first use | a @functools.cache function (or a lock) |
| Settings that can change at run time | import config and read config.settings each time |
| Code that runs only as a script | if __name__ == "__main__": |
| Two modules that need each other | move the shared part out, or import inside the function |
| Patching a dependency in a test | patch("<module that uses it>.<name>") |
| A renamed or lazily loaded attribute | module-level __getattr__ |
Common mistakes
| Mistake | What happens | Do this instead |
|---|---|---|
from config import settings, then replacing config.settings | your name keeps the old object | import config, or change the object in place |
| Patching where a function is defined | code that imported the name isn't patched | patch where it's used |
| Two modules importing each other at the top | ImportError from a partially initialized module | a third module, or a local import |
No __main__ guard with multiprocessing | workers re-run the script's top level | put startup code under the guard |
A __new__ singleton with an __init__ | state is reset on every call | a module-level instance or @cache |
| A lazy singleton without a lock | two threads create two instances | a lock, or create it at import time |
importlib.reload in a running service | old objects keep the old classes | restart the process |
Check yourself
Try to answer before opening each one. Recalling beats re-reading.
1. Why does checkout.py never see the new flags?
Show answer
from flags import settings bound checkout's own name to the dictionary flags.settings held at import time. flags.settings = load_flags() moved only the module's name to a new dictionary. Use import flags and read flags.settings, or update the dictionary in place.
2. Ten modules import config. How many times does config.py's top-level code run?
Show answer
Once. The first import runs it and stores the module in sys.modules; the other nine get the same module object.
3. Why does Registry() keep returning an empty registry, even though it's the same object each time?
Show answer
__new__ returns the cached object, but Python then calls __init__ on it, and __init__ sets self.items = [] again. Use a module-level instance or a cached factory function instead.
Key takeaways
- An import finds the file, caches the module in
sys.modulesand runs its code once; later imports reuse it. import xlooks attributes up every time;from x import ybinds the object once. Patch where a name is used.- Guard script code with
if __name__ == "__main__":. - Break import cycles by moving shared code out or importing inside functions.
- A module-level instance is Python's singleton;
__new__singletons re-run__init__, and lazy ones need a lock. reloadmakes new classes; module__getattr__handles renamed and lazy attributes.