Dynamic Imports with importlib

· 4 min read · Updated March 15, 2026 · intermediate
python stdlib imports runtime

Sometimes you need to import a module whose name you only know at runtime. Maybe it’s a plugin system, a configuration-driven choice, or a factory pattern. Python’s importlib module gives you exactly this capability—dynamic imports that happen when your code runs, not when it’s parsed.

Why Dynamic Imports?

Static imports at the top of your file are the norm:

import json
data = json.loads('{"key": "value"}')

This works great when you know at coding time which module you need. But consider these scenarios:

  • A plugin loader that discovers modules in a directory
  • A data pipeline that chooses a parser based on file extension
  • A testing framework that imports test modules by name
  • Supporting multiple database backends based on configuration

For these cases, you need to defer the import until you know what you’re importing. That’s where importlib comes in.

Your First Dynamic Import

The simplest use case is importing a module by name string:

import importlib

# Import a module dynamically
json_module = importlib.import_module("json")
data = json_module.loads('{"key": "value"}')
# output: {"key": "value"}

The import_module() function takes a module name and returns the imported module object. You can then use it exactly like a statically imported module.

Importing by Name

What if you need to import something you only have as a string? importlib handles that too:

import importlib

# Import a specific function from a module
json_loads = importlib.import_module("json").loads
result = json_loads('{"name": "Alice"}')
# output: {"name": "Alice"}

# Or use getattr on the module
json = importlib.import_module("json")
loads_func = getattr(json, "loads")

Reloading Modules

One powerful feature is reloading a module after it has changed:

import importlib

# Initial import
my_module = importlib.import_module("my_module")

# ... later, after my_module.py has been modified ...
my_module = importlib.reload(my_module)

This is particularly useful in interactive development or when building systems that need to pick up code changes without restarting.

Using importlib.metadata

Python 3.8+ includes importlib.metadata for discovering installed packages:

from importlib.metadata import version, distributions

# Get the version of an installed package
print(version("requests"))
# output: 2.32.3

# List all installed packages
for dist in distributions():
    if dist.name == "numpy":
        print(f"Found numpy: {dist.version}")
# output: Found numpy: 1.26.4

This replaces the old pkg_resources approach and is significantly faster.

Dynamic Attribute Access

Beyond importing modules, you often need to access attributes dynamically:

import importlib

# Get a class from a module by name
module = importlib.import_module("collections")
Counter = getattr(module, "Counter")

# Use it normally
counts = Counter(["a", "b", "a"])
print(dict(counts))
# output: {"a": 2, "b": 1}

This pattern is common in factory methods and plugin systems.

Practical Example: Plugin Loader

Here’s a realistic pattern for loading plugins dynamically:

import importlib
import os
import sys

def load_plugins(plugin_dir):
    """Discover and load all plugins from a directory."""
    plugins = {}
    
    sys.path.insert(0, plugin_dir)
    
    for filename in os.listdir(plugin_dir):
        if filename.endswith(".py") and not filename.startswith("_"):
            module_name = filename[:-3]
            try:
                module = importlib.import_module(module_name)
                if hasattr(module, "register"):
                    plugins[module_name] = module
                    print(f"Loaded plugin: {module_name}")
            except Exception as e:
                print(f"Failed to load {module_name}: {e}")
    
    return plugins

# Usage
# plugins = load_plugins("./plugins")

This pattern forms the basis of extensible applications.

Example: Configuration-Driven Imports

Choose different implementations based on configuration:

import importlib

def get_database_connection(config):
    driver = config["database"]["driver"]  # e.g., "sqlite3", "postgresql"
    module = importlib.import_module(driver)
    connect_func = getattr(module, "connect")
    return connect_func(**config["database"]["params"])

# Configuration
config = {
    "database": {
        "driver": "sqlite3",
        "params": {"database": "app.db"}
    }
}

# Works with any database driver
# conn = get_database_connection(config)

Error Handling

Dynamic imports can fail for various reasons—the module might not exist, might have a syntax error, or might lack a required attribute. Handle these gracefully:

import importlib

def safe_import(module_name, attribute=None):
    try:
        module = importlib.import_module(module_name)
        if attribute:
            return getattr(module, attribute)
        return module
    except ModuleNotFoundError:
        print(f"Module {module_name} not found")
        return None
    except AttributeError:
        print(f"Attribute {attribute} not found in {module_name}")
        return None
    except ImportError as e:
        print(f"Import error: {e}")
        return None

# Usage
json = safe_import("json")
loads = safe_import("json", "loads")
missing = safe_import("nonexistent_module")

Working with Package Imports

Importlib handles subpackages just like regular modules:

import importlib

# Import from a package
urllib_parse = importlib.import_module("urllib.parse")
urljoin = urllib_parse.urljoin

# This also works
result = urljoin("http://example.com/path/", "file.html")
print(result)
# output: http://example.com/file.html

Getting Started

The importlib module is built into Python’s standard library. Start with simple cases like importing a module by name, then build up to more complex patterns like plugin loaders.

For most applications, the key is deciding which imports to make dynamic and which to keep static. Reserve dynamic imports for cases where the module name isn’t known until runtime.

See Also