Skip to main content

Python Import Hooks Practice Problems & Exercises

Practice: Import Hooks

11 problems3 Easy4 Medium4 Hard75–95 min
← Back to lesson

Easy

#1Audit Every Import with sys.meta_pathEasy
import-hookssys.meta_pathauditing

Install a custom meta path finder that logs every module import attempt to stdout, then lets the standard machinery handle the actual load.

Python
import sys


class AuditFinder:
    def find_module(self, fullname, path=None):
        # Only log top-level imports (no dots) and skip our own machinery
        if "." not in fullname:
            print(f"[IMPORT] {fullname}")
        return None   # pass to next finder


# Install at the front of the chain
sys.meta_path.insert(0, AuditFinder())

import json
import math

# Cleanup so it doesn't interfere with later cells
sys.meta_path.remove(sys.meta_path[0])

print(f"json loads: {json.loads('[1,2,3]')}")
print(f"math sqrt: {math.sqrt(16)}")
Expected Output
[IMPORT] json
[IMPORT] math
json loads: [1, 2, 3]
math sqrt: 4.0
Hints

Hint 1: A meta path finder must implement find_module(fullname, path) or find_spec(fullname, path, target). Return None to pass on to the next finder.

Hint 2: Returning None means the standard import machinery continues normally. You are just observing.


#2Block Specific Module ImportsEasy
import-hooksblockingsys.meta_path

Write a BlocklistFinder that prevents importing specific modules. It should raise ImportError with a policy message for blocked modules.

Python
import sys


class BlocklistFinder:
    def __init__(self, blocked):
        self.blocked = set(blocked)

    def find_module(self, fullname, path=None):
        if fullname in self.blocked:
            raise ImportError(
                f"Import of '{fullname}' is blocked by policy"
            )
        return None


finder = BlocklistFinder(["subprocess", "socket"])
sys.meta_path.insert(0, finder)

try:
    import os
    print("os imported OK")
except ImportError as e:
    print(f"ImportError: {e}")

try:
    import subprocess
except ImportError as e:
    print(f"ImportError: {e}")
finally:
    sys.meta_path.remove(finder)
Expected Output
os imported OK
ImportError: Import of 'subprocess' is blocked by policy
Hints

Hint 1: A meta path finder can raise ImportError directly instead of returning None to block an import.

Hint 2: Check if fullname is in a blocklist. If yes, raise ImportError with a descriptive message.


#3Inspect sys.modules CacheEasy
import-hookssys.modulescacheinspection

Demonstrate how sys.modules caches imported modules and prevents re-execution. Show that two imports of the same module return the identical object.

Python
import sys

# Ensure json is not already cached (simulate fresh environment)
json_cached = sys.modules.pop("json", None)

print(f"'json' in sys.modules before import: {'json' in sys.modules}")

import json as j1
print(f"'json' in sys.modules after import: {'json' in sys.modules}")

import json as j2
print(f"Same object: {j1 is j2}")

# Restore original state
if json_cached is not None:
    sys.modules["json"] = json_cached
Expected Output
'json' in sys.modules before import: False
'json' in sys.modules after import: True
Same object: True
Hints

Hint 1: sys.modules is a dict mapping module name to module object. Python checks it first before going to finders.

Hint 2: Deleting a key from sys.modules forces a re-import on the next import statement.


Medium

#4Virtual Module via sys.modules InjectionMedium
import-hooksvirtual-modulesys.modulestypes.ModuleType

Create a virtual constants module entirely in memory and inject it into sys.modules. Then import it normally and access its attributes.

Python
import sys
import types


# Build the virtual module
constants_module = types.ModuleType("constants")
constants_module.__doc__ = "Virtual constants module"
constants_module.PI = 3.14159
constants_module.E = 2.71828
constants_module.GOLDEN_RATIO = 1.61803

# Inject into sys.modules BEFORE the import
sys.modules["constants"] = constants_module

import constants

print(f"constants.PI = {constants.PI}")
print(f"constants.E = {constants.E}")
print(f"constants.GOLDEN_RATIO = {constants.GOLDEN_RATIO}")

# Cleanup
del sys.modules["constants"]
Expected Output
constants.PI = 3.14159
constants.E = 2.71828
constants.GOLDEN_RATIO = 1.61803
Hints

Hint 1: Create a types.ModuleType object, set attributes on it, then insert it into sys.modules before the import statement.

Hint 2: Once in sys.modules, "import constants" will find it there without touching the filesystem.


#5Lazy Import ProxyMedium
import-hookslazy-importproxy__getattr__

Build a LazyModule proxy that defers the actual import until an attribute is first accessed. Replace the entry in sys.modules with the real module after loading.

Python
import sys
import importlib


class LazyModule:
    def __init__(self, name):
        self._name = name
        self._module = None

    def _load(self):
        if self._module is None:
            print(f"Accessing {self._name}.compute triggers load...")
            self._module = importlib.import_module(self._name)
            sys.modules[self._name] = self._module
        return self._module

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

    def __repr__(self):
        return f"<LazyModule '{self._name}' (not yet loaded)>"


# Install the lazy proxy — use 'math' as the "heavy" module
sys.modules["math"] = LazyModule("math")

# At this point, math is not really loaded (we replaced it with a proxy)
proxy = sys.modules["math"]
print(f"Module not loaded yet: {isinstance(proxy, LazyModule)}")

import math
result = math.floor(42.9)
print(f"Result: {result}")

print(f"Module now loaded: {not isinstance(sys.modules['math'], LazyModule)}")
Expected Output
Module not loaded yet: True
Accessing heavy.compute triggers load...
Result: 42
Module now loaded: True
Hints

Hint 1: Create a proxy object that stores the module name but does not import it until __getattr__ is called.

Hint 2: On first attribute access, import the real module, store it, and replace the proxy in sys.modules.


#6Import Time Logger with TimingMedium
import-hookstimingsys.meta_pathloader

Write a timing import hook that prints how long each module takes to load. It should not interfere with normal import behaviour.

Python
import sys
import time
import importlib.util


class TimingFinder:
    def find_module(self, fullname, path=None):
        # Only instrument modules we haven't seen yet
        if fullname in sys.modules:
            return None
        return TimingLoader(fullname)


class TimingLoader:
    def __init__(self, fullname):
        self.fullname = fullname

    def load_module(self, fullname):
        if fullname in sys.modules:
            return sys.modules[fullname]

        start = time.perf_counter()

        # Use the standard machinery but skip our own finder
        finder = None
        for f in sys.meta_path:
            if isinstance(f, TimingFinder):
                finder = f
                break
        if finder:
            sys.meta_path.remove(finder)

        try:
            __import__(fullname)
        finally:
            if finder:
                sys.meta_path.insert(0, finder)

        elapsed = time.perf_counter() - start
        print(f"[TIMING] {fullname} loaded in {elapsed:.3f}s")
        return sys.modules[fullname]


timing_finder = TimingFinder()
sys.meta_path.insert(0, timing_finder)

# Remove from cache to force a fresh load
for mod in ["hashlib", "hmac"]:
    sys.modules.pop(mod, None)

import hashlib
import hmac

sys.meta_path.remove(timing_finder)
print(f"hashlib: {hashlib}")
Expected Output
[TIMING] hashlib loaded in 0.XXXs
[TIMING] hmac loaded in 0.XXXs
hashlib: <module 'hashlib' ...>
Hints

Hint 1: Wrap the load_module (or exec_module) step of the loader to measure elapsed time.

Hint 2: A simpler approach: wrap the find_module result. If found, return a timing wrapper loader that delegates to the real loader but records duration.


#7Namespace Package SimulationMedium
import-hooksnamespace-packagefinderloader

Create a virtual plugins namespace where sub-modules are generated on the fly by a custom finder/loader rather than loaded from disk.

Python
import sys
import types


PLUGIN_REGISTRY = {
    "greeter": {
        "hello": lambda: "Hello from greeter plugin!",
    },
    "farewell": {
        "bye": lambda: "Goodbye from farewell plugin!",
    },
}


class PluginFinder:
    PREFIX = "plugins."

    def find_module(self, fullname, path=None):
        if fullname == "plugins" or fullname.startswith(self.PREFIX):
            return PluginLoader()
        return None


class PluginLoader:
    def load_module(self, fullname):
        if fullname in sys.modules:
            return sys.modules[fullname]

        mod = types.ModuleType(fullname)
        mod.__loader__ = self
        mod.__package__ = fullname

        if fullname == "plugins":
            mod.__path__ = []   # mark as package
        else:
            plugin_name = fullname[len("plugins."):]
            if plugin_name in PLUGIN_REGISTRY:
                for attr, value in PLUGIN_REGISTRY[plugin_name].items():
                    setattr(mod, attr, value)

        sys.modules[fullname] = mod
        return mod


plugin_finder = PluginFinder()
sys.meta_path.insert(0, plugin_finder)

import plugins.greeter
import plugins.farewell

print(f"plugins.greeter.hello() = {plugins.greeter.hello()}")
print(f"plugins.farewell.bye() = {plugins.farewell.bye()}")

sys.meta_path.remove(plugin_finder)
Expected Output
plugins.greeter.hello() = Hello from greeter plugin!
plugins.farewell.bye() = Goodbye from farewell plugin!
Hints

Hint 1: Register a meta path finder that intercepts imports starting with "plugins.".

Hint 2: For each sub-module, create a types.ModuleType, set the required attributes on it, and add it to sys.modules.


Hard

#8Import Alias RedirectorHard
import-hooksaliasredirectsys.meta_path

Build an import alias system. import simplejson should silently redirect to json. import np should redirect to a stub when numpy is absent.

Python
import sys
import types


class AliasedFinder:
    def __init__(self, aliases):
        self.aliases = aliases   # {alias: real_module_name}

    def find_module(self, fullname, path=None):
        if fullname in self.aliases:
            return AliasedLoader(self.aliases[fullname])
        return None


class AliasedLoader:
    def __init__(self, target):
        self.target = target

    def load_module(self, fullname):
        if fullname in sys.modules:
            return sys.modules[fullname]

        # Try to import the real target
        try:
            __import__(self.target)
            real_mod = sys.modules[self.target]
        except ImportError:
            # Create a stub
            real_mod = types.ModuleType(self.target)
            real_mod.__doc__ = f"Stub for {self.target}"
            real_mod._is_stub = True
            sys.modules[self.target] = real_mod

        sys.modules[fullname] = real_mod
        return real_mod


alias_finder = AliasedFinder({
    "simplejson": "json",
    "np": "numpy",
})
sys.meta_path.insert(0, alias_finder)

import simplejson
import json

print(f"simplejson is json: {simplejson is json}")

import np
print(f"np is numpy placeholder: {getattr(np, '_is_stub', False) or np.__name__ == 'numpy'}")

print(f"Redirected: simplejson.loads('[1,2,3]') = {simplejson.loads('[1,2,3]')}")

sys.meta_path.remove(alias_finder)
Expected Output
simplejson is json: True
np is numpy placeholder: True
Redirected: simplejson.loads('[1,2,3]') = [1, 2, 3]
Hints

Hint 1: A meta path finder intercepts find_module for the alias name and returns a loader that fetches the real module.

Hint 2: The loader.load_module should import the real target, alias it in sys.modules under both the real name and the alias name, and return it.


#9Source Transformer — Uppercase Constants on ImportHard
import-hookssource-transformastloader

Write a source-transforming import hook that intercepts a virtual module myconfig and injects uppercase constants computed at import time.

Python
import sys
import types


class ConfigLoader:
    """Generates a dynamic config module with injected constants."""

    CONFIG_CONSTANTS = {
        "MAGIC_NUMBER": 42,
        "APP_VERSION": "1.0.0",
        "DEBUG": False,
        "MAX_RETRIES": 3,
    }

    def load_module(self, fullname):
        if fullname in sys.modules:
            return sys.modules[fullname]

        mod = types.ModuleType(fullname)
        mod.__loader__ = self
        mod.__package__ = fullname
        mod.__spec__ = None

        # Inject constants
        for name, value in self.CONFIG_CONSTANTS.items():
            setattr(mod, name, value)

        # Simulate source transform: add a computed constant
        mod.COMPUTED = mod.MAGIC_NUMBER * 2

        sys.modules[fullname] = mod
        return mod


class ConfigFinder:
    def find_module(self, fullname, path=None):
        if fullname == "myconfig":
            return ConfigLoader()
        return None


config_finder = ConfigFinder()
sys.meta_path.insert(0, config_finder)

import myconfig

print(f"MAGIC_NUMBER = {myconfig.MAGIC_NUMBER}")
print(f"APP_VERSION = {myconfig.APP_VERSION!r}")
print(f"Transformed constants are accessible")

sys.meta_path.remove(config_finder)
del sys.modules["myconfig"]
Expected Output
MAGIC_NUMBER = 42
APP_VERSION = '1.0.0'
Transformed constants are accessible
Hints

Hint 1: A source-transforming loader overrides get_source and exec_module. In exec_module, fetch the source, transform it, then compile() and exec().

Hint 2: For this problem, simulate the transform by injecting constants into the module namespace directly rather than parsing real source.


#10Reloadable Module with Change DetectionHard
import-hooksreloadchange-detectionimportlib

Build a reloadable virtual module system where you can update the "source" and reload the module, picking up changes without restarting Python.

Python
import sys
import types
import importlib


# Simulated "file source" registry
MODULE_SOURCES = {
    "greetlib": """
def greet(name):
    return 'Hello, ' + name + '!'

version = 1
"""
}

reload_counts = {"greetlib": 0}


class DynamicFinder:
    def find_module(self, fullname, path=None):
        if fullname in MODULE_SOURCES:
            return DynamicLoader()
        return None


class DynamicLoader:
    def load_module(self, fullname):
        if fullname in sys.modules:
            return sys.modules[fullname]
        return self._build(fullname)

    def _build(self, fullname):
        mod = types.ModuleType(fullname)
        mod.__loader__ = self
        mod.__package__ = fullname
        source = MODULE_SOURCES[fullname]
        code = compile(source, f"<dynamic:{fullname}>", "exec")
        exec(code, mod.__dict__)
        sys.modules[fullname] = mod
        return mod


def hot_reload(fullname):
    loader = DynamicLoader()
    mod = sys.modules.get(fullname)
    if mod is None:
        return
    source = MODULE_SOURCES[fullname]
    code = compile(source, f"<dynamic:{fullname}>", "exec")
    exec(code, mod.__dict__)
    reload_counts[fullname] += 1


dyn_finder = DynamicFinder()
sys.meta_path.insert(0, dyn_finder)

import greetlib

print(f"Initial greeting: {greetlib.greet('World')}")

# Simulate source change
MODULE_SOURCES["greetlib"] = """
def greet(name):
    return 'Hi there, ' + name + '!'

version = 2
"""

hot_reload("greetlib")
print("Module reloaded after change")
print(f"New greeting: {greetlib.greet('World')}")
print(f"Reload count: {reload_counts['greetlib']}")

sys.meta_path.remove(dyn_finder)
del sys.modules["greetlib"]
Expected Output
Initial greeting: Hello, World!
Module reloaded after change
New greeting: Hi there, World!
Reload count: 1
Hints

Hint 1: importlib.reload(module) re-executes the module source. Store the module in sys.modules; reload replaces attributes in place.

Hint 2: Simulate "file change" by modifying the virtual module source stored in a dict before calling a custom reload function.


#11Full Custom Finder + Loader + Spec PipelineHard
import-hooksModuleSpecfind_specexec_moduleimportlib

Build a complete modern import hook using find_spec and exec_module (the current importlib API) that generates a virtual metrics module with counter and gauge primitives.

Python
import sys
import types
import importlib.util


class MetricsLoader:
    def create_module(self, spec):
        return None   # use default creation

    def exec_module(self, module):
        # Inject metrics primitives
        module.counter = 0
        module.gauge = 0.0

        def increment(by=1):
            module.counter += by

        def set_gauge(value):
            module.gauge = float(value)

        def reset():
            module.counter = 0
            module.gauge = 0.0

        module.increment = increment
        module.set_gauge = set_gauge
        module.reset = reset


class MetricsFinder:
    def find_spec(self, fullname, path, target=None):
        if fullname == "metrics":
            loader = MetricsLoader()
            spec = importlib.util.spec_from_loader(fullname, loader)
            return spec
        return None


metrics_finder = MetricsFinder()
sys.meta_path.insert(0, metrics_finder)

# Remove from cache if already there
sys.modules.pop("metrics", None)

import metrics

print(f"metrics.counter = {metrics.counter}")
metrics.increment()
print("metrics.increment() called")
print(f"metrics.counter = {metrics.counter}")

metrics.set_gauge(42.5)
print(f"metrics.gauge = {metrics.gauge}")
print(f"Module spec name: {metrics.__spec__.name}")

sys.meta_path.remove(metrics_finder)
del sys.modules["metrics"]
Expected Output
metrics.counter = 0
metrics.increment() called
metrics.counter = 1
metrics.gauge = 42.5
Module spec name: metrics
Hints

Hint 1: The modern import API uses find_spec (returns a ModuleSpec) instead of find_module. Pair it with a loader that implements exec_module(module).

Hint 2: importlib.util.spec_from_loader(name, loader) creates a ModuleSpec. importlib.util.module_from_spec(spec) creates the module object.

© 2026 EngineersOfAI. All rights reserved.