How Python Modules Work: Import, Packages, and Structure
python modules: How Python modules work: the import pipeline, search path, packages, circular imports, and caching — with practical guidance on structuring code.
python modules requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
A Python module is the smallest reusable unit of code in a Python program: a single file containing functions, classes, and variables that other files can import by name. When you write import config, Python finds config.py, compiles it to bytecode, executes its top-level statements once, and registers the resulting module object in sys.modules so the next import is nearly free. Understanding how this lookup and execution pipeline works makes it easier to structure a codebase, diagnose import errors, and avoid circular dependency failures.
How Python Finds a Module on the Search Path
When an import statement runs, Python checks sys.modules first. If the module is already imported, the cached object is returned and no file is read. If not, Python searches sys.path in order: the directory containing the entry script (or the current directory in interactive mode), every directory listed in PYTHONPATH, and finally the standard library and site-packages directories. The first file matching the module name stops the search.
This ordering matters in practice. A config.py in your project root shadows a standard library module of the same name, and a package installed in site-packages will not be found if a local file with the same name appears earlier in the path. You can inspect the effective path from any running program:
import sys for entry in sys.path: print(entry)
When the source file is found, Python compiles it to bytecode and stores the result in a __pycache__ directory next to the source. On later runs, the cached bytecode is reused when the source timestamp and size match, which removes the parse and compile step without changing the import semantics.
Creating a Module and Controlling What It Exports
A module is just a file. The module name is the filename without the .py extension. A minimal utility module might look like this:
# money.py TAX_RATE = 0.08 def with_tax(amount): return round(amount * (1 + TAX_RATE), 2)
Importing money executes the module body once. Function and class definitions create the module's attributes, but so does any other top-level statement. Code that runs at import time — logging setup, file reads, network calls — runs when the module is first imported, not when the program starts, which is often surprising for developers coming from languages with a single entry point.
The __all__ list controls what from money import * imports:
__all__ = ["with_tax", "TAX_RATE"]
It does not restrict import money or from money import with_tax; it only affects the star-import form. In most codebases, explicit imports are clearer than star imports, so __all__ is most useful in library packages that want to define a stable public surface.
import, from-import, and import-as Semantics
The three import forms bind different things in the caller's namespace:
import money from money import with_tax, TAX_RATE import money as m
import money binds the module object to the name money. Accessing money.with_tax always looks up the attribute on the current module object, so if the module is reloaded or mutated, the caller sees the change. from money import with_tax copies the current value of the attribute into the caller's namespace at import time. The local name with_tax is a separate binding; reassigning it does not affect the module, and changes made to the module later are not reflected in the local copy.
import money as m renames the module binding, which is useful when the module name is long or collides with another name. The same shadowing rules apply.
A common mistake is using from-imports for names that are expected to stay in sync with the module. If a module redefines a constant at runtime, the from-imported copy is stale. Use import module when you want attribute access to reflect the live module state, and from-import when you want a stable reference to a specific object.
Packages: Organizing Modules Into a Directory Tree
A package is a directory of modules with an __init__.py file. The file can be empty, but it marks the directory as importable and is executed when the package is first imported. A typical layout:
project/
main.py
payments/
__init__.py
gateway.py
models.py
With this structure, import payments.gateway imports the payments package first, executing payments/__init__.py, then imports the gateway submodule. The __init__.py can expose a convenient API:
# payments/__init__.py from .gateway import charge
so that callers can write from payments import charge instead of reaching into the submodule. Inside a package, relative imports address sibling modules without hard-coding the top-level package name:
# payments/gateway.py from .models import Payment
A leading dot refers to the current package; .. refers to the parent package. Relative imports fail outside a package, so they are only valid in modules that are part of a package.
Circular Imports and How They Fail
A circular import happens when module A imports module B while B imports A. Python handles this partially because sys.modules contains the in-progress module object. The failure occurs when one module tries to use an attribute of the other before that attribute has been defined.
# a.py import b VALUE = b.compute(10)
# b.py import a def compute(x): return x * 2 print(a.VALUE) # AttributeError: module 'a' has no attribute 'VALUE'
Importing a starts by importing b. Module b imports a, which is already in sys.modules but only partially initialized — VALUE has not been assigned yet. When b reads a.VALUE, the attribute does not exist and an AttributeError is raised at import time.
The fix is usually to move the cross-module usage out of module level. If b only uses a inside a function, the import succeeds because the attribute is read later, after a has finished initializing. Restructuring so that the dependency points in one direction is the more durable solution: move the shared constant or helper into a third module that neither a nor b depends on.
Module Caching, Reload, and Runtime Behavior
Every module that finishes importing is stored in sys.modules, keyed by its fully qualified name. This is why importing the same module twice does not execute its body twice. It also means that deleting a module from sys.modules and importing again re-executes the file, which is how some test suites force a fresh module state.
importlib.reload re-executes an already imported module in place:
import importlib import money importlib.reload(money)
The same module object is updated, so existing import money references see the new attributes. References created by from money import with_tax still point to the old function object, which is why reload is rarely the right tool outside interactive debugging. In production code, prefer restarting the process or using a configuration system that reads values on demand rather than relying on reload.
When to Split Code Into Modules
The decision to split a file into modules is a maintainability tradeoff. A module should group code that changes together and is reused together. Splitting too aggressively creates many small files whose import relationships are hard to follow; keeping everything in one file makes the module hard to read once it grows past a few hundred lines.
A practical rule is to extract a module when a set of functions and constants has a single responsibility, has no reason to change independently of its neighbors, and is imported from more than one place. Modules that are imported only from one location are often better left as functions inside that location. The import graph should stay acyclic and shallow: deep chains of packages make it harder to trace where a name comes from and increase the chance of circular imports.