Python Import Hooks Practice Problems & Exercises
Practice: Import Hooks
← Back to lessonEasy
Install a custom meta path finder that logs every module import attempt to stdout, then lets the standard machinery handle the actual load.
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.0Hints
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.
Write a BlocklistFinder that prevents importing specific modules. It should raise ImportError with a policy message for blocked modules.
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 policyHints
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.
Demonstrate how sys.modules caches imported modules and prevents re-execution. Show that two imports of the same module return the identical object.
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_cachedExpected Output
'json' in sys.modules before import: False
'json' in sys.modules after import: True
Same object: TrueHints
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
Create a virtual constants module entirely in memory and inject it into sys.modules. Then import it normally and access its attributes.
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.61803Hints
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.
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.
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: TrueHints
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.
Write a timing import hook that prints how long each module takes to load. It should not interfere with normal import behaviour.
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.
Create a virtual plugins namespace where sub-modules are generated on the fly by a custom finder/loader rather than loaded from disk.
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
Build an import alias system. import simplejson should silently redirect to json. import np should redirect to a stub when numpy is absent.
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.
Write a source-transforming import hook that intercepts a virtual module myconfig and injects uppercase constants computed at import time.
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 accessibleHints
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.
Build a reloadable virtual module system where you can update the "source" and reload the module, picking up changes without restarting 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: 1Hints
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.
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.
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: metricsHints
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.
