Python Modules
A module is the basic unit of code reuse in Python — every .py file is a module. This article covers the import mechanism, how to organize packages, and best practices.
Three Sources of Modules
- Built-in modules: installed with the interpreter, e.g.
os,sys,json,re. - Third-party modules: installed via
pip install, e.g.requests,pydantic. - Custom modules:
.pyfiles you write yourself within your project.
The import Statement
# import the entire module (recommended: keeps the namespace clear)
import os
import json
print(os.getcwd())
data = json.dumps({"key": "value"})
# import specific names (reduces prefix usage, suitable for frequently used names)
from pathlib import Path
from datetime import datetime
p = Path(".")
now = datetime.now()
# use aliases (when names are too long or to avoid conflicts)
import numpy as np
from collections import defaultdict as dd
# import multiple modules (avoid writing them on one line — hurts readability)
import os
import sys
import refrom module import * imports all public names from the module into the current namespace and can easily cause name collisions. It is not recommended. Module authors can define an __all__ list in their file to control what * exports.What Happens During Import
# foo.py
x = 1
print("foo module executed")
def get():
return xThe first time you run import foo, three things happen:
- All top-level code in
foo.pyis executed (only once). - A new namespace is created to hold the names produced during execution.
- The name
foois created in the current namespace, pointing to the new namespace.
import foo # prints "foo module executed"
import foo # no re-execution (already cached in sys.modules)
print(foo.x) # 1
print(foo.get()) # 1sys.modules — Module Cache
import sys
# check which modules are already loaded
print("json" in sys.modules) # True (if json was imported earlier)
# force a reload (useful during development after modifying a module)
import importlib
import foo
importlib.reload(foo)Module Search Path
When importing, Python searches for modules in this order:
sys.modules(in-memory cache)- Built-in modules (
sys.builtin_module_names) - Directories in
sys.path(left to right)
import sys
print(sys.path)
# ['', '/usr/lib/python312.zip', '/usr/lib/python3.12', ...]
# The first element '' represents the directory of the running script
# temporarily add a search path
sys.path.insert(0, "/my/custom/modules")Packages
A package is a directory containing an __init__.py file, used to organize multiple modules:
mypackage/
├── __init__.py # package entry point, can be empty
├── core.py
├── utils.py
└── sub/
├── __init__.py
└── helper.py# import modules from a package
import mypackage.core
from mypackage import utils
from mypackage.sub import helper
from mypackage.sub.helper import some_func
# __init__.py can define the package's public interface
# mypackage/__init__.py
# from .core import CoreClass
# from .utils import helper_func
# __all__ = ["CoreClass", "helper_func"]Relative Imports (used inside a package)
# inside mypackage/core.py
from .utils import format_data # same package
from ..other_pkg import something # parent packageRelative imports are only valid inside a package and cannot be used in top-level scripts.
The if __name__ == "__main__" Guard
Every .py file has the built-in variable __name__:
- When run as the main script:
__name__ == "__main__" - When imported as a module:
__name__ == "module_name"
# mymodule.py
def add(a: int, b: int) -> int:
return a + b
# only runs when executed directly; skipped when imported
if __name__ == "__main__":
print(add(1, 2)) # 3
print(add(10, 20)) # 30Circular Import Issues
# a.py
from b import func_b # ← b.py hasn't finished loading a.py yet
# b.py
from a import func_a # ← a.py hasn't finished loading b.py yetCircular imports usually indicate a design problem. Solutions:
- Extract shared data/functions into a third module.
- Move the import statement inside a function (lazy import).
# b.py — lazy import
def func_b():
from a import func_a # imported at call time, avoids circular dependency at initialization
return func_a()Standard Convention: Import Order
According to PEP 8, import statements should appear at the top of the file, grouped into three sections separated by blank lines:
# 1. Standard library modules
import os
import sys
from pathlib import Path
# 2. Third-party modules
import requests
from pydantic import BaseModel
# 3. Local / project modules
from myapp.core import Engine
from myapp.utils import loggerTools like ruff or isort can automatically sort imports for you.