How imports really work and why it matters for building maintainable software.
When you first learn Python, import
feels magical. You write import math
, and suddenly you have access to math.sqrt()
. But under the hood, Python is doing a lot more than you might think.
This article is a deep dive into modules, packages, and namespaces the three pillars of Python’s import system. By the end, you’ll not only know how to use them, but also how they work behind the scenes, so you can write cleaner, faster, and more scalable Python code.
🧩 Understanding Modules, The Building Blocks
A module is simply a single Python file.
Example:
# greetings.py
def hello(name):
return f"Hello, {name}!"
You can now use it from another file:
# app.py
import greetings
print(greetings.hello("Anik"))
When you run app.py
, you’ll see:
Hello, Anik!
Behind the Scenes: What Happens on import
When Python encounters import greetings
, here’s what really happens:
-
Check the module cache (
sys.modules
): If already loaded, just reuse it. -
Find the module: Python searches in
sys.path
a list of directories:
- The current working directory
- Any directories in
PYTHONPATH
- Standard library directories
- Installed third-party libraries (site-packages)
-
Load & execute the file: The file is read, compiled to bytecode (
.pyc
), and executed. -
Cache it in
sys.modules
so subsequent imports are instant.
-
Load & execute the file: The file is read, compiled to bytecode (
You can inspect this yourself:
import sys, greetings
print(sys.modules['greetings'])
This will show a live reference to the loaded module object.
💡 Fun fact: That’s why importing the same module multiple times doesn’t re-run the code. It just reuses the cached object.
🔄 Module Execution & __name__
Every module has a built-in variable __name__
.
- If a file is being run directly,
__name__ == "__main__"
. - If it’s imported as a module,
__name__ == "module_name"
.
Example:
# greetings.py
print(f"Running as {__name__}")
if __name__ == "__main__":
print("This only runs if you execute greetings.py directly.")
Run it directly:
$ python greetings.py
Running as __main__
This only runs if you execute greetings.py directly.
Import it:
>>> import greetings
Running as greetings
This is how libraries provide both importable functions and CLI behavior in one file.
🏗 Real-Life Example: Building a Calculator Module
Instead of writing one giant script, break it into modules:
calculator/
__init__.py
operations.py
utils.py
app.py
operations.py
def add(a, b): return a + b
def subtract(a, b): return a - b
utils.py
def format_result(value):
return f"Result: {value}"
app.py
from operations import add
from utils import format_result
print(format_result(add(10, 5)))
Output:
Result: 15
This structure is easier to maintain, test, and expand as your project grows.
🔀 Import Variants (and When to Use Them)
Python gives you multiple import styles:
import math # Full import
import math as m # Alias
from math import sqrt # Selective import
from math import * # Import everything (avoid!)
Best Practices
✅ Use import x
or import x as y
for clarity.
✅ Use from x import y
only for a few names.
❌ Avoid from x import *
it pollutes the namespace and makes code harder to read.
⚙️ Dynamic Imports with importlib
Sometimes you don’t know what to import until runtime.
Example: plugin systems.
import importlib
module_name = "math"
math_module = importlib.import_module(module_name)
print(math_module.sqrt(25))
This is how Django loads apps dynamically and how pytest discovers test modules.
🔄 Reloading Modules
During development, you might want to reload a module after editing it.
import importlib, greetings
importlib.reload(greetings)
This re-executes the module’s code, replacing old definitions.
Useful in REPL sessions, but be careful it won’t reset global state perfectly.
📦 Enter Packages Organizing Your Modules
A package is just a folder with an __init__.py
file (optional in Python 3.3+).
Example:
my_package/
__init__.py
module_a.py
module_b.py
You can now do:
import my_package.module_a
or
from my_package import module_b
__init__.py
The Package Gatekeeper
You can leave it empty, or use it to define what the package exports:
# __init__.py
from .module_a import function_a
__all__ = ['function_a']
Now users can just do:
from my_package import function_a
🧩 Namespace Packages, When One Folder Isn’t Enough
Imagine you want multiple teams to contribute to the same package from different repositories.
Namespace packages make this possible.
Example layout:
repo1/mypackage/
a.py
repo2/mypackage/
b.py
Both folders get added to sys.path
, and you can do:
import mypackage.a, mypackage.b
This is how large libraries like google.cloud.*
work.
🗂 Best Practices for Structuring Packages
- Group related functionality together.
- Keep
__init__.py
clean just re-export important functions/classes. - Avoid circular imports (split code or use local imports).
- Use relative imports inside packages (
from .module import function
).
📦 Importing from Zip Archives
Python can even import directly from a .zip
file:
import sys
sys.path.append('my_modules.zip')
import some_module
Useful for shipping self-contained applications or plugins.
🧠 Behind the Scenes: Bytecode & Caching
When Python imports a module, it creates a .pyc
file (compiled bytecode) inside __pycache__/
.
This makes future imports faster because Python skips recompilation.
You can inspect bytecode with dis
:
import dis, greetings
dis.dis(greetings.hello)
This shows the compiled instructions, a fun way to peek under the hood.
🏆 Key Takeaways
-
Modules are single
.py
files; packages are directories with modules. - Python imports are cached in
sys.modules
, so importing twice is free. -
__name__ == "__main__"
allows files to act as both scripts and libraries. -
importlib
gives you dynamic and reloadable imports. - Good package structure is critical for maintainable code.
- Namespace packages enable distributed package development.
- Python can import from directories, zip files, and even remote paths (with tools like
zipimport
).
Top comments (0)