Skip to content
Codeloom
Python

Python Dynamic Imports with importlib

Master Python's importlib module for dynamic imports, plugin systems, lazy loading, and runtime module discovery with practical examples.

·7 min read · By Codeloom
Intermediate 11 min read

What you'll learn

  • Import modules dynamically at runtime with importlib
  • Build a plugin system using dynamic discovery
  • Implement lazy module loading for faster startup
  • Reload and inspect modules programmatically

Prerequisites

  • Python modules and packages
  • Basic understanding of import system

Why Dynamic Imports?

Standard import statements are resolved at module load time. That works fine for most programs, but some situations demand more flexibility:

  • Plugin systems where available modules are discovered at runtime
  • Lazy loading to speed up application startup
  • Conditional imports based on configuration or environment
  • Testing where you need to reload modules between tests

Python’s importlib module provides a programmatic interface to the import system, giving you full control over how and when modules are loaded.

importlib.import_module Basics

The most common function is import_module, which takes a module name as a string and returns the module object:

import importlib

# Equivalent to: import json
json_module = importlib.import_module('json')
data = json_module.dumps({'key': 'value'})
print(data)  # '{"key": "value"}'

# Equivalent to: from os import path
path_module = importlib.import_module('os.path')
print(path_module.exists('/tmp'))  # True or False

# Import a submodule using the 'package' parameter
# Equivalent to: from email import mime
mime = importlib.import_module('.mime', package='email')

The key advantage is that the module name is a string, so it can come from configuration, user input, or any other runtime source.

Dynamic Import Patterns

Import Based on Configuration

import importlib

def get_database_backend(config):
    """Load the database backend specified in config."""
    backend_name = config.get('database_backend', 'sqlite')

    # Map short names to module paths
    backends = {
        'sqlite': 'myapp.db.sqlite_backend',
        'postgres': 'myapp.db.postgres_backend',
        'mysql': 'myapp.db.mysql_backend',
    }

    module_path = backends.get(backend_name)
    if module_path is None:
        raise ValueError(f"Unknown database backend: {backend_name}")

    module = importlib.import_module(module_path)
    return module.DatabaseBackend()  # Each module exports this class

Import and Access Attributes

import importlib

def import_attribute(dotted_path):
    """
    Import a class or function from a dotted path like
    'myapp.utils.helpers.format_date'.
    """
    module_path, _, attribute_name = dotted_path.rpartition('.')
    if not module_path:
        raise ValueError(f"Invalid dotted path: {dotted_path}")

    module = importlib.import_module(module_path)

    try:
        return getattr(module, attribute_name)
    except AttributeError:
        raise ImportError(
            f"Module '{module_path}' has no attribute '{attribute_name}'"
        )

# Usage
# formatter = import_attribute('myapp.utils.helpers.format_date')
# result = formatter('2026-07-06')

This pattern is used extensively in frameworks like Django (for middleware, backends, and template tags) and Flask (for extensions).

Building a Plugin System

One of the most powerful uses of importlib is discovering and loading plugins at runtime.

Directory-Based Plugins

import importlib
import importlib.util
import pathlib

class PluginManager:
    """Discover and load plugins from a directory."""

    def __init__(self, plugin_dir):
        self.plugin_dir = pathlib.Path(plugin_dir)
        self.plugins = {}

    def discover(self):
        """Find all Python files in the plugin directory."""
        if not self.plugin_dir.exists():
            raise FileNotFoundError(f"Plugin directory not found: {self.plugin_dir}")

        for path in self.plugin_dir.glob('*.py'):
            if path.name.startswith('_'):
                continue  # Skip __init__.py and private modules

            plugin_name = path.stem
            self.plugins[plugin_name] = self._load_plugin(path)

        return self.plugins

    def _load_plugin(self, path):
        """Load a single plugin from a file path."""
        spec = importlib.util.spec_from_file_location(
            name=path.stem,
            location=str(path)
        )
        module = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(module)
        return module

    def get_plugin(self, name):
        """Get a loaded plugin by name."""
        if name not in self.plugins:
            raise KeyError(f"Plugin '{name}' not found")
        return self.plugins[name]


# Example plugin file: plugins/uppercase.py
# def transform(text):
#     return text.upper()
#
# PLUGIN_NAME = "Uppercase Transformer"

# Usage
manager = PluginManager('./plugins')
manager.discover()

for name, plugin in manager.plugins.items():
    print(f"Loaded: {getattr(plugin, 'PLUGIN_NAME', name)}")
    # result = plugin.transform("hello world")

Entry Point-Based Plugins

For installable plugins (pip packages), use importlib.metadata:

import importlib.metadata

def load_entry_point_plugins(group_name):
    """
    Load plugins registered via entry_points in pyproject.toml.

    A package registers plugins like this in pyproject.toml:
    [project.entry-points."myapp.plugins"]
    csv_export = "myapp_csv:CSVExporter"
    json_export = "myapp_json:JSONExporter"
    """
    plugins = {}
    entry_points = importlib.metadata.entry_points()

    # In Python 3.12+, entry_points() returns a SelectableGroups
    # Use the group parameter or .select()
    for ep in entry_points.select(group=group_name):
        plugin_class = ep.load()  # Imports and returns the object
        plugins[ep.name] = plugin_class
        print(f"Loaded plugin: {ep.name} -> {plugin_class}")

    return plugins

# Usage
# exporters = load_entry_point_plugins('myapp.plugins')
# csv_exporter = exporters['csv_export']()

Lazy Module Loading

Importing large modules at startup slows down your application. Lazy loading defers the import until the module is actually used.

Simple Lazy Import

class LazyImport:
    """Import a module only when an attribute is first accessed."""

    def __init__(self, module_name):
        self._module_name = module_name
        self._module = None

    def _load(self):
        if self._module is None:
            import importlib
            self._module = importlib.import_module(self._module_name)
        return self._module

    def __getattr__(self, name):
        module = self._load()
        return getattr(module, name)

# These don't trigger any imports yet
np = LazyImport('numpy')
pd = LazyImport('pandas')

# numpy is imported here, when first used
# result = np.array([1, 2, 3])

Using importlib.util.LazyLoader

Python 3.4+ includes a built-in lazy loader:

import importlib
import importlib.util

def lazy_import(name):
    """Use Python's built-in LazyLoader."""
    spec = importlib.util.find_spec(name)
    if spec is None:
        raise ModuleNotFoundError(f"No module named '{name}'")

    loader = importlib.util.LazyLoader(spec.loader)
    spec.loader = loader
    module = importlib.util.module_from_spec(spec)

    import sys
    sys.modules[name] = module
    loader.exec_module(module)
    return module

# Module is registered but not executed yet
# heavy_module = lazy_import('some_heavy_library')
# Execution happens on first attribute access
# heavy_module.do_something()

Reloading Modules

During development or in long-running applications, you might need to reload a module to pick up changes:

import importlib
import my_config

# After editing my_config.py
importlib.reload(my_config)
print(my_config.SETTING)  # Now reflects the updated file

# Reload with dependency tracking
def reload_with_deps(module):
    """Reload a module and all its submodules."""
    import sys
    import types

    module_name = module.__name__
    # Find all submodules
    to_reload = [
        name for name, mod in sys.modules.items()
        if isinstance(mod, types.ModuleType)
        and name.startswith(module_name)
    ]

    # Reload in reverse order (deepest first)
    for name in sorted(to_reload, key=len, reverse=True):
        try:
            importlib.reload(sys.modules[name])
        except Exception as e:
            print(f"Failed to reload {name}: {e}")

    return importlib.reload(module)

A few caveats about reload:

  • Objects already imported via from module import X are not updated
  • Class instances keep their old class even after reload
  • It can cause subtle bugs with isinstance checks

Inspecting Module Metadata

importlib.metadata lets you query installed package information:

import importlib.metadata

# Get package version
version = importlib.metadata.version('requests')
print(f"requests version: {version}")

# Get all metadata
meta = importlib.metadata.metadata('requests')
print(f"Author: {meta['Author']}")
print(f"License: {meta['License']}")
print(f"Summary: {meta['Summary']}")

# List all installed packages
installed = importlib.metadata.distributions()
for dist in installed:
    name = dist.metadata['Name']
    version = dist.metadata['Version']
    # print(f"{name}=={version}")

# Find which package provides a specific module
packages = importlib.metadata.packages_distributions()
# packages is like {'requests': ['requests'], 'yaml': ['PyYAML'], ...}

Checking If a Module Exists

Before importing, you can check whether a module is available:

import importlib.util

def module_exists(name):
    """Check if a module can be imported without importing it."""
    return importlib.util.find_spec(name) is not None

# Conditional feature support
if module_exists('uvloop'):
    import uvloop
    uvloop.install()
    print("Using uvloop for better async performance")
else:
    print("uvloop not available, using default event loop")

# Try multiple options
def get_json_library():
    """Use the fastest available JSON library."""
    for lib_name in ['orjson', 'ujson', 'json']:
        if module_exists(lib_name):
            return importlib.import_module(lib_name)
    raise RuntimeError("No JSON library available")

json_lib = get_json_library()

Practical Example: A Command Dispatcher

Here is a complete example combining several importlib techniques into a command-line tool framework:

import importlib
import importlib.util
import pathlib
import sys

class CommandDispatcher:
    """
    Discovers command modules from a directory and dispatches
    based on command-line arguments.

    Each command module must define:
    - COMMAND_NAME: str
    - COMMAND_HELP: str
    - def execute(args: list[str]) -> int
    """

    def __init__(self, commands_dir):
        self.commands_dir = pathlib.Path(commands_dir)
        self.commands = {}
        self._discover()

    def _discover(self):
        for path in self.commands_dir.glob('cmd_*.py'):
            spec = importlib.util.spec_from_file_location(
                path.stem, str(path)
            )
            module = importlib.util.module_from_spec(spec)
            spec.loader.exec_module(module)

            name = getattr(module, 'COMMAND_NAME', path.stem)
            self.commands[name] = module

    def run(self, command_name, args):
        if command_name not in self.commands:
            print(f"Unknown command: {command_name}")
            self.print_help()
            return 1

        module = self.commands[command_name]
        return module.execute(args)

    def print_help(self):
        print("Available commands:")
        for name, module in sorted(self.commands.items()):
            help_text = getattr(module, 'COMMAND_HELP', 'No description')
            print(f"  {name:20s} {help_text}")


# Example command file: commands/cmd_greet.py
# COMMAND_NAME = "greet"
# COMMAND_HELP = "Greet someone by name"
# def execute(args):
#     name = args[0] if args else "World"
#     print(f"Hello, {name}!")
#     return 0

Wrapping Up

importlib transforms Python’s import system from a static mechanism into a dynamic, programmable tool. Use import_module for simple dynamic imports, util.spec_from_file_location for loading modules from arbitrary file paths, and metadata for querying installed packages. The most impactful real-world application is building plugin systems that discover and load code at runtime without hardcoding module names. Start with import_module for basic needs and reach for the lower-level spec APIs only when you need to load from non-standard locations.