Modules and Packages
Comprehensive guide on Modules and Packages.
Modules and Packages
Overview#
As programs grow, it becomes essential to split code across multiple
files and organize related files into directories. Python calls a single
file a module and a directory of related modules a package. This
notebook covers creating and importing modules, the different import
styles and when to use each, how packages and __init__.py work, relative
vs absolute imports, the if __name__ == "__main__": pattern, and how
Python actually finds modules when you import them.
1. What Is a Module?#
Any .py file is a module. Its name (used for importing) is the filename
without the .py extension.
Python 3.12 (WASM)# math_utils.py
def add(a, b):
return a + b
def subtract(a, b):
return a - b
PI = 3.14159
Python 3.12 (WASM)# main.py
import math_utils
print(math_utils.add(2, 3)) # 5
print(math_utils.PI) # 3.14159
When a module is imported, Python runs the entire file top to bottom once, and the resulting names (functions, variables, classes) become attributes accessible through the module name.
2. Different Import Styles#
Python 3.12 (WASM)# Import the whole module, access members with a prefix
import math_utils
math_utils.add(1, 2)
# Import specific names directly into the current namespace
from math_utils import add, PI
add(1, 2) # no prefix needed
print(PI)
# Import everything (generally discouraged)
from math_utils import *
add(1, 2) # works, but unclear where 'add' came from when reading code
# Import with an alias
import math_utils as mu
mu.add(1, 2)
from math_utils import add as sum_two
sum_two(1, 2)
Why from module import * is discouraged#
Python 3.12 (WASM)# module_a.py
def process(): ...
# module_b.py
def process(): ... # same name!
# main.py
from module_a import *
from module_b import *
process() # which one runs? Whichever was imported LAST silently wins
Wildcard imports pollute the current namespace and make it unclear where a name came from, which hurts both readability and tooling (like autocomplete and static analysis). Prefer explicit imports, or import the module itself and use a prefix.
3. The Module Search Path#
When you import something, Python searches, in order:
- The directory of the script being run (or the current directory in interactive mode).
- Directories listed in the
PYTHONPATHenvironment variable. - Standard library installation directories.
- Site-packages directories (where
pip-installed packages live).
Python 3.12 (WASM)import sys
print(sys.path) # the actual list of directories Python searches, in order
If a module can't be found, Python raises ModuleNotFoundError. This is
often the cause of "why can't Python find my file" issues — the script's
working directory or the module's location isn't on sys.path.
4. Module Caching: sys.modules#
A module is only executed the first time it's imported in a given program run — subsequent imports reuse the cached module object, they don't re-run the file.
Python 3.12 (WASM)import sys
import math_utils
print("math_utils" in sys.modules) # True
import math_utils # does NOT re-execute math_utils.py — it's already cached
This is why side effects at the top level of a module (like print()
statements, or opening a database connection) only happen once, no matter
how many times or from how many other files the module gets imported.
5. if __name__ == "__main__":#
Every module has a built-in __name__ attribute. When a file is run
directly (python script.py), Python sets __name__ to "__main__".
When the same file is imported by another module, __name__ is set to
the module's actual name instead.
Python 3.12 (WASM)# math_utils.py
def add(a, b):
return a + b
def main():
print(add(2, 3))
print(add(10, 20))
if __name__ == "__main__":
main()
bashpython math_utils.py # prints 5, 30 — main() runs because __name__ == "__main__"
Python 3.12 (WASM)# main.py
import math_utils # math_utils.py runs, but main() is NOT called here,
# because __name__ inside math_utils.py is "math_utils", not "__main__"
This pattern lets a file work both as a reusable module (when imported) and as a standalone script (when run directly) — extremely common in real-world Python code, including almost every library's example/demo scripts.
6. Packages: Organizing Modules into Directories#
A package is a directory containing an __init__.py file (which can be
empty), plus one or more modules.
Architecture & Data Flowmy_package/ ├── __init__.py ├── math_utils.py ├── string_utils.py └── validators/ ├── __init__.py ├── email.py └── phone.py
Python 3.12 (WASM)# Importing from a package
from my_package import math_utils
from my_package.string_utils import capitalize_words
from my_package.validators.email import is_valid_email
math_utils.add(1, 2)
capitalize_words("hello world")
is_valid_email("test@example.com")
The role of __init__.py#
__init__.py runs automatically when the package (or any module within
it) is first imported. It can be empty, just marking the directory as a
package, or it can control what gets exposed when someone imports the
package itself.
Python 3.12 (WASM)# my_package/__init__.py
from .math_utils import add, subtract
from .string_utils import capitalize_words
__all__ = ["add", "subtract", "capitalize_words"]
With this __init__.py, users can do:
Python 3.12 (WASM)from my_package import add, capitalize_words # works because of __init__.py's re-exports
instead of needing to know the internal module structure
(my_package.math_utils.add).
__all__ controls exactly what names are imported when someone uses
from my_package import * — only names listed there are exposed that way.
7. Relative vs Absolute Imports#
Absolute imports specify the full path from the project's root package. Relative imports use dots to reference modules relative to the current module's location, and only work inside a package.
Python 3.12 (WASM)# my_package/validators/email.py
# Absolute import — full path from the top-level package
from my_package.string_utils import capitalize_words
# Relative import — "..") goes up one package level
from ..string_utils import capitalize_words
# Relative import — "." refers to the current package
from . import phone
| Syntax | Meaning |
|---|---|
from . import x | Import x from the current package |
from .module import x | Import x from a sibling module in the same package |
from .. import x | Import x from the parent package |
from ..module import x | Import x from a module in the parent package |
Absolute imports are generally preferred for clarity and are recommended by Python's own style guide (PEP 8), except in cases where relative imports meaningfully reduce coupling to the top-level package name within a large, self-contained package.
8. Reloading a Module#
Since modules are cached after the first import, changes to a module's source won't be reflected in an already-running session unless explicitly reloaded — mainly relevant in interactive sessions or notebooks.
Python 3.12 (WASM)import importlib
import math_utils
importlib.reload(math_utils) # re-executes math_utils.py, updating the cached module
This is rarely needed in normal scripts (which start fresh each run), but is common when iterating in a long-running interactive session.
9. Organizing a Real Project#
A typical, well-organized project layout:
Architecture & Data Flowmy_project/ ├── src/ │ └── my_project/ │ ├── __init__.py │ ├── main.py │ ├── config.py │ └── utils/ │ ├── __init__.py │ ├── validation.py │ └── formatting.py ├── tests/ │ ├── test_validation.py │ └── test_formatting.py ├── requirements.txt └── README.md
Placing the actual package inside a src/ directory (the "src layout") is
a widely recommended convention — it avoids accidentally importing
uninstalled local code instead of the properly installed package, which
can hide packaging bugs during development.
Notes#
- A module's top-level code (outside of function/class definitions) runs exactly once, the first time it's imported, and is cached afterward.
import module_namenever re-runs the module's code on subsequent imports within the same program — this is important to remember when a module has side effects like opening a file or connecting to a database.__name__ == "__main__"is checked at the bottom of a script, not the top, so that all functions and classes are defined before the check runs.- Circular imports (module A imports module B, which imports module A)
are a common source of
ImportErrorin larger projects — usually resolved by restructuring shared code into a separate module both can import from. sys.pathcan be modified at runtime (sys.path.append(...)), but this is generally a sign that a project's structure or installation needs fixing rather than a good permanent solution.
Best Practices Checklist#
- Avoid
from module import *— prefer explicit imports or a module prefix - Use
if __name__ == "__main__":to separate reusable code from script-only execution logic - Keep
__init__.pyfiles focused on re-exporting a clean public interface, not heavy logic - Prefer absolute imports for clarity, reserving relative imports for tightly coupled code within the same package
- Define
__all__in__init__.pywhen you want to control exactly what's exposed via wildcard imports - Avoid circular imports by extracting shared logic into its own module
- Use a
src/layout for real projects to avoid accidental imports of uninstalled local code - Remember modules are only executed once per program run — avoid relying on re-execution for state resets