# Python import hooks **Python import hooks** let you customize how modules are found and loaded. Using `sys.meta_path`, `sys.path_hooks`, and the `importlib` module, you can intercept imports to load from non-standard locations (zip files, URLs, generated code), transform source before execution, or enforce module restrictions. Use import hooks for plugin systems, lazy-loading, compressed package distribution, or transparent source transformation. ## Example This example shows import hooks intercepting module loads. ```python # run: python3 import_hooks.py # description: customizing module loading with import hooks import sys import importlib.abc import importlib.machinery from importlib.util import spec_from_file_location, module_from_spec # Simple import hook: transform module names class DashToUnderscoreFinder(importlib.abc.MetaPathFinder): """Allow importing with dashes instead of underscores.""" def find_spec(self, fullname, path, target=None): # Only handle top-level modules we care about if '-' not in fullname: return None # Try importing with underscores canonical_name = fullname.replace('-', '_') try: return importlib.util.find_spec(canonical_name) except (ImportError, AttributeError): return None # Register the finder sys.meta_path.insert(0, DashToUnderscoreFinder()) # Example usage (would work if modules existed) # import some-module # would import some_module instead # Logging hook: track all imports class ImportLogger(importlib.abc.MetaPathFinder): """Log all import attempts.""" def find_spec(self, fullname, path, target=None): print(f"Importing: {fullname}") return None # delegate to next finder sys.meta_path.append(ImportLogger()) print("Importing os and sys:") import os import sys print("\n" + "="*60 + "\n") # Version-specific imports import builtins original_import = builtins.__import__ def version_aware_import(name, *args, **kwargs): """Allow version-specific modules.""" if 'v2' in name: print(f"Loading v2 module: {name}") return original_import(name, *args, **kwargs) # Don't actually override (would break things) # builtins.__import__ = version_aware_import # Transforming source code class TransformingLoader(importlib.abc.Loader): def exec_module(self, module): # Could transform source here print(f"Transforming module: {module.__name__}") # Path hook: custom file system class CustomPathFinder: """Handle custom path entries.""" def find_spec(self, name): return None # Not implemented # sys.path_hooks.append(CustomPathFinder) # Lazy import example class LazyImporter(importlib.abc.MetaPathFinder): def find_spec(self, fullname, path, target=None): if fullname.startswith('lazy_'): print(f"LazyImporter: would load {fullname} lazily") return None sys.meta_path.append(LazyImporter()) # Test lazy loader import lazy_module # would trigger lazy importer ``` ## Common patterns **MetaPathFinder** (finds modules): - Implement `find_spec(fullname, path, target)` - Return `ModuleSpec` if found, `None` to let others try - Called in order added to `sys.meta_path` **Loader** (loads modules): - Implement `exec_module(module)` to execute code - Implement `create_module(spec)` to customize module creation (usually None) - Loader's `exec_module` runs module code in module's namespace **PathFinder** (for `sys.path` entries): - Implement `find_spec(name, path)` for custom path entries - Return `ModuleSpec` or None - Attached to `sys.path_hooks` **Use cases**: - Loading modules from zip files (built-in, via `zipimport`) - Loading from remote URLs - Transforming source code (e.g., transpiling, auto-formatting) - Restricting which modules can be imported - Lazy-loading modules on first access - Plugin systems (dynamic module loading) **Finding and spec creation**: - `importlib.util.find_spec(name)`: find module spec - `importlib.util.spec_from_file_location(name, path)`: create spec for file - `importlib.util.module_from_spec(spec)`: create module object **Module execution**: - `spec.loader.exec_module(module)`: load module code into namespace - Code executes with `module.__dict__` as namespace - Module attributes become module's attributes **Import interception**: - `sys.modules[name]`: dict of already-imported modules - Check here first before searching; can pre-populate to provide mock modules - Import loop prevention: add to `sys.modules` before executing **Order matters**: - Finders tried in order; first match wins - `sys.meta_path` is checked before built-in finders - Insert at position 0 for highest priority **Performance considerations**: - Finders are called for every import - Return None quickly if not responsible - Cache results to avoid repeated work - Late imports have performance cost