Modules and Packages
The Import System
Section titled “The Import System”The import system is the mechanism by which Python locates, loads, and binds module objects into the Current namespace. It is one of the most critical subsystems in the interpreter because every piece Of code you write depends on it, yet most developers treat it as magic. Understanding the import System is essential for debugging import errors, designing package layouts, and avoiding circular Dependency traps.
The import Statement
Section titled “The import Statement”Python provides three syntactic forms of the import statement:
import osimport os.pathfrom os.path import joinfrom os.path import join as path_joinimport json as json_moduleEach form does something subtly different at the namespace level:
import osbinds the module object to the nameosin the current namespace. The module”s attributes are accessed asos.path``os.environEtc. No names from insideosare injected into the current scope.from os.path import joinfirst importsos.path(which necessarily importsosas a dependency), then binds the attributejoinfromos.pathdirectly into the current namespace. The intermediate namesosandos.pathare not bound unless they already were.import json as json_moduleimports the module and binds it to the aliasjson_module. The original namejsonis not added to the namespace.
What Happens When Python Imports
Section titled “What Happens When Python Imports”When the interpreter encounters an import statement, it performs the following steps in order:
Check
sys.modules. This is a dictionary mapping fully qualified module names to already-loaded module objects. If the module is present, the import returns immediately — the existing object is bound to the target name. This means a module’s top-level code executes at most once per interpreter session, regardless of how many times it is imported or from how many locations.Find the module. Python searches
sys.meta_pathfinders in order. The default finder (importlib._bootstrap_external._frozen_importlib_external.PathFinder) searches the directories and zip files listed insys.path. If no finder can locate the module,ModuleNotFoundErroris raised.Load the module. The finder returns a loader (or in modern terms, a module spec), which is responsible for creating the module object, executing the module’s code, and setting the module’s attributes.
Execute the module code. The module’s
.pyfile is compiled to bytecode and executed in a new namespace. The resulting namespace becomes the module’s__dict__. All top-level assignments, function definitions, and class definitions in the module execute during this step.Cache the result. The module object is stored in
sys.modulesunder its fully qualified name.Bind the name. The module object (or specific attribute) is bound to the requested name in the importing scope.
import sys
## sys.modules is a dict of all loaded modulesprint(len(sys.modules)) # in standard practice 100+ even in a minimal script## The 'builtins' module is always present (contains print, len, etc.)print('builtins' in sys.modules) # Trueprint('os' in sys.modules) # False (until imported)
import osprint('os' in sys.modules) # True
# Importing again returns the SAME objectimport osprint(os is sys.modules['os']) # TrueThe critical implication of sys.modules caching: if module A imports module B, and module B Modifies a global variable at module level, module A will see that modification even if A imported B Before the modification, because both references point to the same module object. Conversely, if Module B is not yet in sys.modules when A imports it, B’s top-level code runs as a side effect of The import.
sys.path
Section titled “sys.path”sys.path is a list of strings that determines where Python looks for modules. It is populated in The following order (first match wins):
The directory containing the script being run (or the current directory if running interactively or with
-c). This is notos.getcwd()in all cases — it is the directory of the entry-point script as resolved by the OS. This is why runningpython subdir/myscript.pyaddssubdir/tosys.pathNot the project root.PYTHONPATHenvironment variable. If set, its value is split onos.pathsep(colon on Unix, semicolon on Windows) and each path is appended.Installation-dependent default paths. These are the site-packages directories where
pip installputs packages. The exact paths depend on the Python installation and can be queried withpython -m site.Site-specific additions from
.pthfiles in the site-packages directory.
import sys
# Examine the search pathfor p in sys.path: print(p)
# You can modify sys.path at runtime (but be careful)sys.path.insert(0, "/my/custom/modules")Modifying sys.path at runtime is a common but fragile pattern. It makes imports depend on the Order of execution and can cause hard-to-debug shadowing issues where a local module masks a Standard library module or an installed package. The recommended approach is to use proper package Installation via pip install -e . or to set PYTHONPATH in the environment.
PYTHONPATH
Section titled “PYTHONPATH”PYTHONPATH is the standard mechanism for augmenting the module search path without modifying code. It is analogous to PATH for executables or CLASSPATH for Java. Set it before invoking the Interpreter:
export PYTHONPATH=/my/project/src:$PYTHONPATHpython -m mypackage.mainIn production, you should rarely rely on PYTHONPATH. Instead, install packages properly into a Virtual environment so that sys.path is populated by the installation mechanism, not by Environment variable hacks. PYTHONPATH is useful for development and testing where you want to Avoid repeated installs.
Module Creation
Section titled “Module Creation”Every .py File Is a Module
Section titled “Every .py File Is a Module”In Python, there is no special declaration or registration step required to create a module. Any File with a .py extension is a module. The file’s stem (without extension) becomes the module’s Name. When imported, the file is executed as a script, and the resulting namespace becomes the Module’s __dict__.
Module Dunder Attributes
Section titled “Module Dunder Attributes”Every module object has a set of special attributes that provide metadata about the module. These Are set by the import machinery and are critical for understanding how Python resolves names:
"""Utility functions."""
import os
def get_cwd(): return os.getcwd()
def show_metadata(): print(__name__) # 'utils' (or '__main__' if run directly) print(__file__) # '/abs/path/to/utils.py' print(__package__) # None (top-level module) or 'mypackage' (if in a package) print(__doc__) # 'Utility functions.' print(__spec__) # ModuleSpec(name='utils', loader=..., origin='...') print(__loader__) # SourceFileLoader (deprecated in favor of __spec__) print(__cached__) # '/abs/path/to/__pycache__/utils.cpython-312.pyc'__name__ is the most important module attribute. It contains the fully qualified name of the Module as known to the import system. When a file is run directly with python utils.py``__name__ Is set to the string '__main__'. When imported as from myproject import utils``__name__ is 'myproject.utils'.
This distinction is the basis for the idiomatic test guard:
if __name__ == "__main__": print("Running as script") show_metadata()This guard prevents the code block from executing when the module is imported. It is not optional For modules that have side effects at import time (starting servers, opening files, spawning Processes) — without it, importing the module for testing or type checking would trigger those side Effects.
__file__ is the path to the file from which the module was loaded. For .pyc files, it still Points to the original .py source. For modules loaded from zip files or other non-filesystem Sources, __file__ may be a synthetic path or None. For namespace packages, __file__ is None.
__package__ is the parent package name. For a top-level module (not inside any package), it is None. For mypackage.submodule``__package__ is 'mypackage'. For mypackage.subpkg.module __package__ is 'mypackage.subpkg'. This attribute is critical for relative import resolution: When a module uses from . import siblingThe import system resolves the dot by looking at __package__.
__doc__ is the module’s docstring — the first string literal in the module body, if any. It Is accessible as module.__doc__ and is what help() displays.
__spec__ is a ModuleSpec instance that encapsulates all the information the import system Needs to load the module. It includes the name, loader, origin (filesystem path), submodule search Locations, and other metadata. This was introduced in Python 3.4 as part of the importlib overhaul (PEP 451) and supersedes the older __loader__ and __cached__ attributes.
__path__ is only defined for packages (not for regular modules). It is a list of strings that The import system uses to find submodules. For regular packages, it contains the single directory Where the package’s __init__.py lives. For namespace packages, it can contain multiple Directories.
import jsonprint(json.__path__) # AttributeError -- json is not a package
import xml.etree.ElementTree as ETprint(xml.__path__) # list of directories where xml submodules livePackages
Section titled “Packages”Regular Packages
Section titled “Regular Packages”A package is a module that contains other modules. In the filesystem, it is a directory containing An __init__.py file. The presence of __init__.py is what tells the import system that a Directory should be treated as a package rather than an ordinary directory.
mypackage/ __init__.py submodule.py subpkg/ __init__.py utils.pyWhen you write import mypackage.submoduleThe import system:
- Finds
mypackage/onsys.path(because it contains__init__.py). - Imports
mypackage(executesmypackage/__init__.py). - Looks inside
mypackage/forsubmodule.py. - Imports
mypackage.submodule(executesmypackage/submodule.py). - Sets the attribute
submoduleon themypackagemodule object.
Step 5 is crucial: after import mypackage.submoduleYou can access the submodule as mypackage.submodule. The mypackage.__init__.py does not need to explicitly import the submodule For this to work — the import machinery does it automatically.
The Role of __init__.py
Section titled “The Role of __init__.py”The __init__.py file serves several purposes:
Marks the directory as a package. Without it (prior to PEP 420), the directory is not recognized as a package.
Executes package-level initialization code. Any top-level code in
__init__.pyruns when the package is imported. This includes setting up package-level state, configuring logging, or importing submodules.Defines the package’s public API. By importing specific names in
__init__.pyYou control what users see when theyfrom mypackage import something.Sets
__all__to controlfrom mypackage import *behavior.
from .submodule import public_func, AnotherClassfrom .subpkg.utils import helper
__all__ = ["public_func", "helper"]With this __init__.pyUsers can write from mypackage import public_func without knowing the Internal module structure. This is the re-export pattern and is how most well-designed packages Expose their API.
Namespace Packages (PEP 420 / PEP 3.3)
Section titled “Namespace Packages (PEP 420 / PEP 3.3)”Since Python 3.3, directories without __init__.py can also function as packages. These are called namespace packages. They allow a single package name to be split across multiple directories on sys.path.
Consider this scenario:
/opt/app/namespace_pkg/ module_a.py/usr/local/lib/namespace_pkg/ module_b.pyIf both /opt/app and /usr/local/lib are on sys.pathThen import namespace_pkg.module_a Loads from the first directory, and import namespace_pkg.module_b loads from the second. Both are Recognized as part of the same package namespace_pkg.
The difference from regular packages:
| Property | Regular Package | Namespace Package |
|---|---|---|
__init__.py | Required | Absent |
__path__ | Single directory | Multiple directories |
__file__ | Points to __init__.py | None |
| When created | First import | First import of any submodule |
| Use case | Most applications | Plugin systems, vendor splits |
Namespace packages are the mechanism behind the vendor directory pattern used by pip and Setuptools to bundle dependencies without conflict. They are also used by large projects that split A single logical package across multiple repositories or deployment locations.
__all__
Section titled “__all__”__all__ is a list of strings that defines the public API of a module or package. It controls what from module import * imports:
__all__ = ["public_func", "PublicClass"]
def public_func(): pass
def _internal_helper(): pass
class PublicClass: passWhen another module does from mymodule import *Only public_func and PublicClass are bound. The _internal_helper function is excluded despite being a module-level name.
__all__ also serves as documentation: it tells readers (and tools like linters and IDEs) which Names are part of the stable public API. Names not in __all__ are considered internal.
For packages, __all__ in __init__.py controls from package import *:
__all__ = ["func_a", "func_b"]from .module_a import func_afrom .module_b import func_bIf __all__ is not defined, from package import * does not import any submodule names (it only Imports names defined directly in __init__.py). This is different from regular modules, where the Absence of __all__ causes all names not starting with _ to be imported.
Absolute vs Relative Imports
Section titled “Absolute vs Relative Imports”Absolute Imports
Section titled “Absolute Imports”An absolute import specifies the full dotted path from the top-level package:
from mypackage.subpkg.utils import helperimport mypackage.submoduleAbsolute imports are unambiguous: the name resolution does not depend on where the importing file is Located. PEP 8 recommends absolute imports as the default style.
Relative Imports
Section titled “Relative Imports”A relative import uses leading dots to specify the import relative to the current package:
from . import sibling # sibling module in the same packagefrom .sibling import func # func from sibling modulefrom .. import parent_pkg # parent packagefrom ..other_pkg import mod # other_pkg is a sibling of the parentThe number of dots indicates how many levels up to go:
.means the current package (same directory)...means the parent package (one directory up)....means the grandparent package (two directories up).
Relative imports are resolved using the __package__ attribute of the importing module. When Python Encounters from . import sibling in mypackage/subpkg/module.pyIt looks at __package__Which Is 'mypackage.subpkg'And resolves the import as mypackage.subpkg.sibling.
Implicit Relative Imports (Removed in Python 3)
Section titled “Implicit Relative Imports (Removed in Python 3)”In Python 2, if you wrote import sibling inside mypackage/subpkg/module.pyPython would first Look for mypackage.subpkg.sibling (implicit relative import) before looking at the top-level sys.path for a module named sibling. This led to shadowing bugs where a local module name would Silently override a standard library module.
Python 3 removed implicit relative imports entirely. import sibling always means a top-level Module. To import a sibling within the same package, you must use an explicit relative import: from . import sibling.
Why Absolute Imports Are Preferred
Section titled “Why Absolute Imports Are Preferred”Readability.
from mypackage.auth.oauth2 import TokenValidatortells you exactly where the symbol comes from.from .oauth2 import TokenValidatorrequires you to know which file you are reading and what the package structure looks like.Refactoring safety. Moving a file from one subpackage to another does not break absolute imports (as long as the module’s full path is updated). Relative imports break when files move because the dot-count changes.
Tool support. IDEs, linters, and static analysis tools can resolve absolute imports unambiguously. Relative imports require the tool to know the file’s location in the package.
No
__main__breakage. Relative imports fail when a module is run directly as a script (python mypackage/subpkg/module.py) because__package__is not set. Absolute imports do not have this limitation.
Relative Imports in __main__
Section titled “Relative Imports in __main__”When you run a file directly with python path/to/file.pyThe __package__ attribute is set to NoneAnd __name__ is set to '__main__'. This means any relative import in that file will Raise ImportError: attempted relative import with no known parent package. This is by design — The file is being run as a standalone script, not as part of a package.
The workaround is to use the -m flag:
python -m mypackage.subpkg.moduleThis tells Python to run module.py as a module within the mypackage.subpkg package, which sets __package__ correctly and enables relative imports.
Circular Imports
Section titled “Circular Imports”Why They Happen
Section titled “Why They Happen”Circular imports occur when two or more modules depend on each other, either directly or through a Chain of dependencies:
module_a.py imports module_b.pymodule_b.py imports module_a.pyThis creates a chicken-and-egg problem. When Python starts importing module_aIt begins executing module_a’s code. When it hits import module_bIt starts executing module_b. When module_b Hits import module_aIt looks in sys.modules and finds module_a — but module_a is only Partially initialized. Its top-level code has not finished executing, so the names that module_b Expects to find in module_a may not exist yet. This manifests as AttributeError: partially initialized module 'module_a' has no attribute 'SomeClass'.
The error message includes the phrase “partially initialized module,” which is Python’s way of Telling you that the module object exists in sys.modules but its namespace is incomplete.
Common Patterns That Cause Circular Imports
Section titled “Common Patterns That Cause Circular Imports”Mutual type dependencies. Module A defines a function that takes an argument of type B, and module B defines a function that takes an argument of type A.
Model-repo coupling. A model module imports a repository module for database access, and the repository module imports the model module to use the ORM classes.
Configuration coupling. A config module imports a constants module, which imports the config module for defaults.
Base class coupling. A base class module imports a mixin module, which imports the base class module to inherit from it.
Strategy 1: Restructure Code
Section titled “Strategy 1: Restructure Code”The most robust solution is to eliminate the circular dependency by extracting the shared code into A third module that both can import:
Before: auth.py -> user.py user.py -> auth.py
After: auth.py -> models.py user.py -> models.py models.py -> (no imports from auth or user)This is not always practical, but when it is, it produces the cleanest architecture. The shared Module becomes a dependency of both, breaking the cycle.
Strategy 2: Import Inside Functions
Section titled “Strategy 2: Import Inside Functions”Move the import from module level to inside the function that needs it. This defers the import until The function is actually called, by which time both modules are fully initialized:
def process(data): from module_b import transform return transform(data)This works because the import statement is not executed until process() is called. By that time, module_b has already been fully imported (or will be imported on demand, and since module_a is Already in sys.modulesThe cycle does not cause a partial initialization problem).
The trade-off is that every call to process() executes the import statement. In CPython, the Import machinery checks sys.modules first and returns the cached module object in O(1), so the Runtime cost is negligible. However, it does make the dependency less visible to readers and static Analysis tools.
Strategy 3: TYPE_CHECKING Guard
Section titled “Strategy 3: TYPE_CHECKING Guard”When the circular import exists only for type annotations (not for runtime behavior), you can guard The import with typing.TYPE_CHECKING:
from __future__ import annotationsfrom typing import TYPE_CHECKING
if TYPE_CHECKING: from module_b import SomeClass
def process(obj: SomeClass) -> None: ...typing.TYPE_CHECKING is False at runtime but True when type checkers (mypy, pyright, pyright) Analyze the code. Combined with from __future__ import annotations (PEP 563, which makes all Annotations lazy strings), the import never executes at runtime, breaking the cycle while preserving Type information.
This pattern is the standard solution for circular type dependencies and should be your first choice When the import is only needed for annotations.
Strategy 4: Lazy Imports
Section titled “Strategy 4: Lazy Imports”Python 3.7+ provides importlib.util.LazyLoader (though it is technically available since 3.5). For Python 3.12+, importlib gained improvements, and the lazy_loader third-party package provides a Clean API:
import importlibimport sysfrom importlib.util import find_spec, LazyLoader, module_from_spec
def lazy_import(name): spec = find_spec(name) if spec is None: raise ImportError(f"No module named {name}") loader = LazyLoader(spec.loader) spec.loader = loader module = module_from_spec(spec) sys.modules[name] = module return moduleA lazy module is a proxy that delays actual loading until the first attribute access. This can break Circular imports because the actual loading happens after all module-level imports have completed.
Choosing a Strategy
Section titled “Choosing a Strategy”| Strategy | When to Use | Trade-off |
|---|---|---|
| Restructure | Clean separation is possible | Requires architectural changes |
| Function-level import | Runtime dependency, simple fix | Hidden dependency, less discoverable |
TYPE_CHECKING guard | Only needed for annotations | Requires from __future__ import annotations |
| Lazy imports | Large codebase, many cycles | Added complexity, debugging difficulty |
Re-exports and Aliases
Section titled “Re-exports and Aliases”Re-exports in __init__.py
Section titled “Re-exports in __init__.py”A common pattern is to use __init__.py to re-export symbols from submodules, creating a flat Public API that hides the internal package structure:
from .core import Engine, Pipelinefrom .utils import validate, sanitizefrom .config import DEFAULT_TIMEOUT, MAX_RETRIES
__all__ = ["Engine", "Pipeline", "validate", "sanitize", "DEFAULT_TIMEOUT", "MAX_RETRIES"]Users can then write from mypackage import Engine instead of from mypackage.core import Engine. The internal structure (core``utils``config) becomes an implementation detail.
When using from .module import func as funcThe as func is not redundant — it ensures that the Name bound in __init__.py is exactly func regardless of what it was called in the source module. This is defensive programming that protects against future renames in submodules.
__all__ for Public API
Section titled “__all__ for Public API”__all__ serves a dual purpose in __init__.py:
- It controls
from package import *(though wildcard imports are generally discouraged). - It documents the public API. Tools like
pylint``pyrightAnd IDEs use__all__to determine which names are public vs. Internal.
Without __all__The public API is implicitly “everything that does not start with _.” With __all__It is explicitly “only these names.” The explicit version is strictly better because it Prevents accidental public exposure of helper names.
Hiding Imports with del
Section titled “Hiding Imports with del”When you import a symbol in __init__.py only to re-export it from a submodule, the intermediate Module name may also end up in the package namespace:
from .core import Engine # also binds 'core' in mypackage's namespaceAfter this import, both mypackage.Engine and mypackage.core are accessible. To prevent the Intermediate name from leaking, use del:
from .core import Enginedel core # 'core' is not actually bound, but if it were:from . import corefrom .core import Enginedel core # now only 'Engine' is accessible as mypackage.EngineWait — from .core import Engine does not bind the name core in the namespace. Only Engine is Bound. But if you also need import .core for side effects (e.g., registering plugins), then del Removes the unwanted binding:
from . import core # execute core's side effectsfrom .core import Enginedel core # remove 'core' from the public namespaceBackward Compatibility Shims
Section titled “Backward Compatibility Shims”When refactoring a package (renaming modules, moving functions between modules), you can maintain Backward compatibility by keeping the old import paths working:
# mypackage/old_module.py (deprecated, kept for backward compatibility)import warningsfrom mypackage.new_module import new_function as old_function
warnings.warn( "old_module is deprecated, use mypackage.new_module instead", DeprecationWarning, stacklevel=2,)
__all__ = ["old_function"]This pattern lets existing code continue to work while emitting a warning that guides users to the New location. The stacklevel=2 ensures the warning points to the caller’s code, not to the shim Itself.
Package Layout
Section titled “Package Layout”Flat Layout
Section titled “Flat Layout”In the flat layout, the package directory sits at the repository root:
myproject/ mypackage/ __init__.py module.py tests/ test_module.py pyproject.toml README.mdSrc Layout
Section titled “Src Layout”In the src layout, the package is nested under a src/ directory:
myproject/ src/ mypackage/ __init__.py module.py tests/ test_module.py pyproject.toml README.mdWhy Src Layout Matters
Section titled “Why Src Layout Matters”The src layout exists to solve a real problem: implicit namespace packages caused by accidentally Importing the local copy instead of the installed copy.
Consider the flat layout. When you run python -m pytest from the project root, the project root Directory is added to sys.path. This means import mypackage resolves to the local directory ./mypackage/Not to the installed copy in site-packages. This has two consequences:
Missing installed dependencies. If
mypackagedepends on other packages that are installed in site-packages, those are still onsys.pathand will be found. But ifmypackagehas C extensions or entry points that need to be installed, those will not be available from the local copy.Shadowing installed packages. If you have a package installed system-wide named
mypackageand a local directory also namedmypackageThe local one wins because the project root is earlier insys.path. This means you are testing the local (uninstalled) code, which may not match what users actually get when they install your package.
The src layout prevents this by moving the package out of the project root. When python -m pytest Runs from the project root, ./src/ is on sys.pathBut import mypackage will fail unless you Have done pip install -e . (which installs a .egg-link pointing to src/mypackage/). This Forces you to install the package properly, which catches configuration errors in pyproject.toml And setup.cfg.
pyproject.toml Package Discovery
Section titled “pyproject.toml Package Discovery”Modern Python packaging uses pyproject.toml to declare package metadata. Package discovery is Configured to match the chosen layout:
For flat layout:
[build-system]requires = ["setuptools>=68.0"]build-backend = "setuptools.build_meta"
[project]name = "mypackage"version = "1.0.0"
[tool.setuptools.packages.find]where = ["."]For src layout:
[build-system]requires = ["setuptools>=68.0"]build-backend = "setuptools.build_meta"
[project]name = "mypackage"version = "1.0.0"
[tool.setuptools.packages.find]where = ["src"]Alternatively, explicit package listing (works for both layouts):
[tool.setuptools.packages.find]include = ["mypackage*"]PEP 621 (standardized in Python 3.10+ and supported by setuptools, hatch, flit, poetry) defines the [project] table as the standard way to declare metadata. The build-backend-specific configuration ([tool.setuptools.packages.find]``[tool.hatch.build.targets.wheel]Etc.) is still needed to Control which files are included in the distribution.
Dynamic Imports
Section titled “Dynamic Imports”importlib.import_module()
Section titled “importlib.import_module()”The standard way to import a module by name at runtime:
import importlib
module = importlib.import_module("os.path")print(module.join("a", "b")) # a/b (or a\b on Windows)
relative_module = importlib.import_module(".submodule", package="mypackage")# equivalent to: from mypackage import submoduleThe first argument is the fully qualified module name (absolute) or a relative name starting with Dots. If relative, the package argument must specify the parent package name.
This is useful when the module name is determined at runtime (from configuration, command-line Arguments, or plugin discovery):
def load_backend(backend_name): module = importlib.import_module(f"myapp.backends.{backend_name}") return module.Backendimportlib.reload()
Section titled “importlib.reload()”Reloads a previously imported module, re-executing its top-level code:
import mymoduleimport importlib
mymodule = importlib.reload(mymodule)Key behaviors:
The module’s
__dict__is updated in place. Existing references to the module (including those held by other modules that imported it) will see the updated attributes because they all point to the same module object.However,
from module import namebindings are not updated. If module A didfrom mymodule import funcReloadingmymoduledoes not update A’sfuncbinding. A still holds a reference to the original function object. This is a common source of confusion.New top-level names added during reload appear in the module’s namespace. Deleted top-level names are removed. Modified names reflect the new values.
reload()is not recursive. It does not reload submodules.
reload() is primarily useful during interactive development (in the REPL or Jupyter notebooks) Where you want to pick up code changes without restarting the interpreter. It is not suitable for Production use because it is inherently racy (what if another thread is calling a function that gets Replaced mid-execution?) and because it cannot safely reload C extension modules.
Plugin Systems
Section titled “Plugin Systems”Dynamic imports are the foundation of plugin architectures:
import importlibfrom pathlib import Path
def load_plugins(plugin_dir: str): plugins = {} plugin_path = Path(plugin_dir) for py_file in plugin_path.glob("*.py"): if py_file.name.startswith("_"): continue module_name = py_file.stem spec = importlib.util.spec_from_file_location(module_name, py_file) if spec and spec.loader: module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) if hasattr(module, "register"): module.register(plugins) return pluginsThis pattern loads Python files from a directory at runtime, executes each one, and calls a Convention-named function (register) to let the plugin register itself. The plugin author has full Freedom to define any functions and classes, as long as they implement the register protocol.
When to Use Dynamic Imports
Section titled “When to Use Dynamic Imports”Use dynamic imports when:
- The module name is not known at authoring time (plugins, backends, drivers).
- You want to reduce startup time by deferring heavy imports until they are actually needed.
- You are implementing a lazy-loading strategy for optional dependencies.
Do not use dynamic imports as a substitute for proper dependency management. If you need a module, Import it at the top of the file. Dynamic imports that reference constant module names are a code Smell — they make the dependency invisible to static analysis tools and increase cognitive load for Readers.
Entry Points
Section titled “Entry Points”What Entry Points Are
Section titled “What Entry Points Are”Entry points are a packaging standard (defined in setuptools and later standardized in importlib.metadata per PEP 621) that allows a package to register named objects (functions, Classes) that other packages or the system can discover at runtime. They are the mechanism behind pip install creating command-line scripts, pytest discovering plugins, and many other Framework-specific plugin systems.
console_scripts
Section titled “console_scripts”The most common entry point type. It creates executable command-line scripts when a package is Installed:
[project.scripts]myapp = "mypackage.cli:main"myapp-admin = "mypackage.admin:main"When a user runs pip install mypackagePip creates a script named myapp (on Unix: a Python Script with a shebang line; on Windows: a .exe wrapper) that effectively does:
from mypackage.cli import mainmain()The entry point name (myapp) becomes the command name. The value is module:functionWhere module is the fully qualified module name and function is the callable to invoke.
gui_scripts
Section titled “gui_scripts”Identical to console_scripts but without a console window on Windows. Useful for GUI applications:
[project.gui-scripts]myapp-gui = "mypackage.gui:main"Discovering Entry Points at Runtime
Section titled “Discovering Entry Points at Runtime”Other packages can discover and load entry points using importlib.metadata:
from importlib.metadata import entry_points, version
eps = entry_points()
# Get all console_scriptsfor ep in eps.select(group="console_scripts"): print(f"{ep.name} -> {ep.value}")
# Discover plugins registered by other packagesfor ep in eps.select(group="myapp.plugins"): plugin_class = ep.load() print(f"Loaded plugin: {ep.name} from {ep.dist.name} v{version(ep.dist.name)}")The entry_points() function returns all registered entry points. The select(group=...) method Filters by group name. The load() method dynamically imports the module and returns the callable.
pkg_resources vs importlib.metadata
Section titled “pkg_resources vs importlib.metadata”pkg_resources is part of setuptools and has been the traditional way to access entry points:
from pkg_resources import iter_entry_points
for ep in iter_entry_points("myapp.plugins"): plugin = ep.load()importlib.metadata (stdlib since Python 3.8, backported as importlib_metadata for 3.7) is the Modern replacement. It is faster because it does not scan the entire sys.path at import time (the Way pkg_resources does). Prefer importlib.metadata in all new code.
Key differences:
| Feature | pkg_resources | importlib.metadata |
|---|---|---|
| Stdlib | No (setuptools) | Yes (3.8+) |
| Startup cost | High (scans sys.path) | Low (lazy) |
| Maintenance | Legacy | Active |
| API style | OOP-heavy | Functional |
How Entry Points Work Under the Hood
Section titled “How Entry Points Work Under the Hood”When pip installs a package, it writes metadata to the .dist-info directory in site-packages. The entry_points.txt file lists all entry points:
[console_scripts]myapp = mypackage.cli:main
[myapp.plugins]csv_export = myapp.plugins.csv:registerWhen importlib.metadata.entry_points() is called, it reads these files from all .dist-info Directories on sys.path and builds the registry. This is why entry points are discovered Automatically without any registration code in the application — the metadata is already on disk.
Common Pitfalls
Section titled “Common Pitfalls”Shadowing Standard Library Modules
Section titled “Shadowing Standard Library Modules”If you name a local file random.py``logging.pyOr any other stdlib module name, your file will Shadow the standard library module in sys.path. This causes bizarre errors where importing the Stdlib module loads your file instead:
# random.py (your file, not the stdlib)print("hello")
import randomrandom.randint(1, 10) # AttributeError: module 'random' has no attribute 'randint'Never name files after stdlib modules. This includes common names like collections.py``json.py email.py``types.py``builtins.pyAnd test.py.
__name__ vs __package__ Confusion
Section titled “__name__ vs __package__ Confusion”__name__ is the module’s full dotted name. __package__ is the parent package. They are often Confused but serve different purposes:
# In mypackage/subpkg/module.py:print(__name__) # 'mypackage.subpkg.module'print(__package__) # 'mypackage.subpkg'Relative imports use __package__Not __name__. If __package__ is None (which happens when a File is run directly), relative imports fail.
Mutable Module-Level State
Section titled “Mutable Module-Level State”Module-level mutable objects (lists, dicts, sets) are shared across all importers. If one module Appends to a list defined at module level in another module, all other modules see the change:
CACHE = []
# module_a.pyfrom config import CACHECACHE.append("a")
# module_b.pyfrom config import CACHEprint(CACHE) # ['a'] -- sees module_a's modificationThis is not a bug — it is a consequence of the fact that import returns a reference to the same Module object every time. But it is a common source of surprise. If you need independent copies, Import the module name and copy: list(config.CACHE).
from x import * Contamination
Section titled “from x import * Contamination”Wildcard imports dump all non-private names from the source module into the importing namespace. This pollutes the namespace, makes it impossible to determine where a name came from (use Ctrl+Shift+F in your editor), and can silently override existing names. PEP 8 explicitly Discourages it:
Wildcard imports (from
<module>import*) should be avoided, as they make it unclear which names are present in the namespace, confusing both readers and many automated tools.
The only acceptable use case is in __init__.py for explicitly controlled re-exports (combined with __all__).
Running a Package with -m
Section titled “Running a Package with -m”If you try to run a file inside a package directly (python mypackage/submodule.py), it runs with __name__ == '__main__' and __package__ == None. Any relative imports in that file (or in files It imports) will fail. Always use python -m mypackage.submodule to run modules within a package.
Editing sys.path Correctly
Section titled “Editing sys.path Correctly”If you must modify sys.pathInsert at the beginning (index 0) and be specific:
import sysfrom pathlib import Path
project_root = str(Path(__file__).resolve().parent.parent)if project_root not in sys.path: sys.path.insert(0, project_root)Appending to sys.path with sys.path.append(...) may not work if an earlier entry already Contains a module with the same name (the first match wins). Inserting at position 0 ensures your Path is searched first, but be aware that this can shadow installed packages.
Namespace Package __init__.py Conflicts
Section titled “Namespace Package __init__.py Conflicts”If a directory contains __init__.pyIt is a regular package, not a namespace package. If you Later add another directory with the same package name to sys.path expecting namespace package Semantics, it will not work — the first directory’s __init__.py takes precedence. To convert a Regular package to a namespace package, you must remove the __init__.py from all directories that Should participate in the namespace.
__pycache__ and Bytecode Staleness
Section titled “__pycache__ and Bytecode Staleness”Python caches compiled bytecode in __pycache__/*.pyc files. The filename includes the Python Version (e.g., module.cpython-312.pyc) to avoid conflicts between versions. If you edit a .py File but the .pyc timestamp check fails (due to filesystem issues, clock skew, or manual editing), Python may load stale bytecode. Symptoms include code changes not taking effect despite saving the File. The fix is to delete the __pycache__ directory and re-import.
Installing Packages in Editable Mode
Section titled “Installing Packages in Editable Mode”During development, use pip install -e . (or pip install -e ".[dev]" for extras) to install the Package in editable mode. This creates a link to the source directory so that changes are Immediately reflected without reinstallation. Without editable mode, you must run pip install . Every time you change the package structure (adding or removing modules, changing __init__.py Etc.).
__spec__ and Missing Module Attributes
Section titled “__spec__ and Missing Module Attributes”If a module is loaded by a custom loader (e.g., from a zip file, from a network, or from a frozen Executable), some standard attributes like __file__ and __cached__ may be None or missing Entirely. Code that assumes module.__file__ is always a valid path will break. Check for None Before using __file__Or use getattr(module, '__file__', None) for defensive access.
Intuition
Section titled “Intuition”Modules and packages are Python’s organizational system for code. Think of modules as drawers in a filing cabinet, and packages as cabinets with labels. When you import a module, Python searches for it in a specific order, loads it once, and caches it so all importers share the same object. The import system is like a librarian who knows exactly where every book lives and ensures each book is only checked out once per session.
Summary
Section titled “Summary”This topic covers the core concepts of modules and packages, including underlying theory, practical implementation, and key applications.
Key concepts include:
- CPU architecture and the fetch-decode-execute cycle
- memory hierarchy (cache, RAM, virtual)
- input/output systems
- operating systems and scheduling
- interrupts and polling
Understanding these concepts thoroughly is essential for both examinations and practical programming, and requires both theoretical knowledge and hands-on practice.
Worked Examples
Section titled “Worked Examples”Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
Cross-References
Section titled “Cross-References”- Essential Modules — Understanding modules is prerequisite to using the standard library effectively.
- Generators and Iterators — Modules can define generator functions and iterator classes that other modules import.
- Object-Oriented Programming — Packages organise classes into namespaces, connecting module structure to OOP design.