Chapter 14 of 24 | Python
Modules, imports, and declarations
A module is a Python source file that can define functions, classes, and values. Importing lets one module reuse another. Keep modules focused and give them descriptive names that do not accidentally shadow standard-library modules such as json or typing.
# geometry.py
def area_of_circle(radius):
if radius < 0:
raise ValueError("radius must be non-negative")
return 3.141592653589793 * radius * radius
# app.py
from geometry import area_of_circle
print(area_of_circle(2))
Python locates modules using its import system and search path. The current directory, installed packages, and environment configuration can affect which module is imported. A local file named after a standard module can unexpectedly take precedence; inspect module.__file__ when diagnosing a surprising import.
Import execution and the main guard
The first import generally executes a module's top-level statements and stores the resulting module object in sys.modules. Later imports normally reuse it. Keep top-level work lightweight: define functions and constants, but avoid starting servers, changing files, or running long computations merely because a module was imported.
def main():
print("This runs only as a script")
if __name__ == "__main__":
main()
Packages and relative imports
A package groups modules into a directory hierarchy. Absolute imports are explicit about the package root. Relative imports such as from .helpers import parse can be useful within a package, but usually require running the code as a package module rather than executing an internal file directly.
A package's __init__.py can establish package-level exports and initialization. Keep it small to avoid circular imports and hidden side effects. Namespace packages can combine package portions from multiple locations, but ordinary packages are easier to understand for many applications.
Import cycles and architecture
If module A imports B while B imports A, one module may observe the other before all its definitions have executed. Import cycles often indicate that responsibilities or shared interfaces should be reorganized. Move shared types into a lower-level module, pass dependencies into functions, or use local imports only when there is a well-understood reason.
Beginner-to-master progression
- Beginner: import a standard module and create a small reusable module.
- Practitioner: use main guards, understand search paths, and avoid top-level side effects.
- Advanced: study import hooks, module specs, namespace packages, circular dependency design, and the security implications of importing executable code.
Exercises
- Split a short program into a module and a script that imports it.
- Print a module's
__name__and__file__when run directly and when imported. - Create a package with two modules and explain its absolute and relative imports.
- Diagnose an import shadowing problem by inspecting the imported module path.
How import resolution works
Import resolution consults the import system, including finders and loaders, and caches loaded modules in sys.modules. The search path is available as sys.path, but changing it dynamically can make behavior difficult to reproduce. Prefer installing packages in the intended environment and using a clear package layout over appending arbitrary directories at runtime.
A module is usually initialized once per interpreter process, but deleting it from sys.modules and importing again can create complex state: existing callers still hold the old module object. Hot-reloading tools need to account for objects created from old definitions. Restarting a development process is often safer than trying to reload a dependency graph manually.
API exports and annotations
A package can provide a stable surface through documented imports from its top-level namespace, but re-exporting every internal name makes refactoring harder. Use __all__ to influence wildcard imports, not as a security boundary. Avoid from module import * in application code because it obscures where names came from.
Type-only imports may be guarded by if TYPE_CHECKING: to reduce runtime import cycles, but annotations may later be inspected at runtime. Use forward references or postponed annotation evaluation according to the supported Python versions, and test frameworks that inspect annotations.
Plugins and import hooks
Python allows custom import finders and loaders, and applications can discover plugins through packaging metadata or explicit registration. Dynamic imports increase flexibility but also create complexity in error reporting, security review, and startup behavior. Importing a plugin executes its top-level code; only load trusted extensions and define a narrow plugin interface.
Mastery checkpoint
- Build a package whose import has no external side effects and whose command-line behavior is behind a main guard.
- Inspect
sys.modulesandsys.pathto explain a module-resolution result. - Design a plugin discovery mechanism and specify trust, compatibility, and failure policies.
- Remove an import cycle by restructuring dependencies and explain the resulting dependency direction.