How two years of work on ImportSpy turned an import-validation problem into a runtime-contract model for modular Python systems.
Plugin architectures are interesting again.
They never really disappeared, of course. But today the same architectural idea shows up everywhere: extensible backends, developer platforms, automation systems, AI tools, agent capabilities, embedded Python runtimes, and applications that load functionality dynamically.
The implementation often starts with something deceptively simple:
import importlib
plugin = importlib.import_module("my_plugin")
If the import succeeds, the plugin exists.
But does that mean the host should actually allow it to participate in the system?
After working on this problem for roughly two years while developing ImportSpy, I no longer think that is primarily an import problem.
I think it is an admission problem.
And that distinction changed the architecture of the project.
The starting point
Imagine a Python application that supports independently developed plugins.
At the beginning, the contract between the host and a plugin may be mostly implicit. The host expects something like:
class Plugin:
def initialize(self, config: dict) -> None:
...
def run(self) -> None:
...
A loader can import the module, inspect it, and start using it.
module = importlib.import_module(plugin_name)
plugin_class = getattr(module, "Plugin")
plugin = plugin_class()
plugin.initialize(config)
plugin.run()
This works until the system starts evolving.
A plugin developed six months later may expose a different signature. Another may depend on Python 3.12 while one deployment still runs Python 3.11. Another expects an environment variable to exist. Another relies on a dependency the host intentionally forbids. Another assumes Linux-specific behavior. Another exposes the right methods but expects capabilities that only exist in a different deployment.
All of these modules may still be perfectly valid Python modules.
Some may even import successfully.
That is where the first important distinction appears:
importable != compatible
Python answers one question:
Can this module be imported?
The host application needs an answer to a different question:
Is this module admissible in this runtime?
Why existing mechanisms only solve part of the problem
Python already gives us several good mechanisms for expressing software contracts.
An abstract base class can describe a required interface:
from abc import ABC, abstractmethod
class Plugin(ABC):
@abstractmethod
def initialize(self, config: dict) -> None:
...
@abstractmethod
def run(self) -> None:
...
A Protocol can make the relationship more flexible:
from typing import Protocol
class PluginProtocol(Protocol):
def initialize(self, config: dict) -> None:
...
def run(self) -> None:
...
Static typing is useful too.
But these mechanisms mostly describe structural compatibility.
The systems I was thinking about had assumptions that extended beyond Python interfaces:
Python >= 3.11
OS = Linux
API_TOKEN must exist
Plugin class must exist
run() must have the expected signature
Dependency X must be available
Dependency Y must not be imported
Deployment must expose capability Z
At this point the compatibility problem spans several layers:
- Python structure
- signatures and annotations
- environment
- dependencies
- interpreter characteristics
- operating system
- deployment assumptions
- runtime invariants
No single Python interface mechanism naturally represents all of them.
The first design mistake: thinking about imports
The original idea behind ImportSpy was much closer to:
Validate a Python module when it is imported.
That framing is intuitive. If the module is incompatible, reject it as early as possible.
Failing early is attractive because it keeps invalid components away from deeper execution paths and improves diagnostics: the error appears near the moment the incompatible component attempts to enter the system.
But the more I worked on the project, the more I realized that importing was only one possible enforcement point.
The real concept was somewhere else.
The architectural shift: admission instead of import validation
The model eventually became:
module -> contract -> validation -> admission / rejection
The important operation is no longer simply:
import module
It becomes:
admit module into this runtime
That sounds like a small semantic change. It is not.
It changes the responsibility of the system.
The module is no longer considered acceptable merely because Python knows how to load it. Instead, the host establishes an explicit architectural boundary. Before a component crosses that boundary, the system can verify whether the assumptions on both sides are compatible.
That model became the foundation of what ImportSpy is today.
Making architectural assumptions executable
Keeping requirements only in documentation does not solve much.
You can write:
This plugin requires Python 3.11 and Linux.
But nothing guarantees that those assumptions are actually respected.
You can scatter checks throughout application code:
if sys.version_info < (3, 11):
raise RuntimeError("Python 3.11+ required")
if platform.system() != "Linux":
raise RuntimeError("Linux required")
if "API_TOKEN" not in os.environ:
raise RuntimeError("API_TOKEN is missing")
That works technically.
Architecturally, though, the contract becomes distributed across the implementation.
ImportSpy takes a different approach: describe the assumptions declaratively and compile them into an internal model that can be validated by the runtime.
A simplified contract might express requirements such as:
filename: plugin.py
version: "1.2.3"
functions:
- name: initialize
arguments:
- name: config
annotation: dict
classes:
- name: Plugin
methods:
- name: run
runtime:
python: ">=3.11"
os: linux
required_environment:
- API_TOKEN
The exact syntax is less important than the architectural property it gives us.
The assumptions become:
- explicit
- inspectable
- versionable
- testable
- enforceable
Most importantly, they are no longer trapped inside someone's understanding of the system.
ImportSpy's actual contracts are YAML-based and are compiled into a SpyModel, which represents the minimum acceptable contract for a module.
Compatibility should describe the minimum, not the whole runtime
One design decision turned out to be particularly important.
A contract should generally describe the minimum acceptable context, not an exact copy of the runtime.
Suppose a module requires capabilities A, B, and C, while the host provides A, B, C, D, and E.
The host should normally be compatible.
Conceptually:
required capabilities ⊆ available capabilities
ImportSpy uses this kind of subset compatibility strategy.
The contract establishes a baseline. The runtime is allowed to provide more.
This matters in long-lived systems because exact equality creates unnecessarily fragile contracts. A new deployment capability should not invalidate every existing module. The important question is whether the assumptions required by the component remain satisfied.
Keep policy separate from implementation
Another requirement emerged while thinking about real systems.
I did not want every module to contain host-governance logic just so another system could determine whether that module was compatible.
The cleaner model is:
implementation != admission policy
The module remains ordinary Python code.
The contract describes expectations about it.
The host decides whether those expectations are satisfied.
This separation makes the mechanism easier to reason about and also matters for legacy code and externally developed extensions: the component should not need to be rewritten simply to participate in a compatibility check.
Where should enforcement happen?
Once I stopped thinking exclusively about imports, another question appeared.
There is no single correct enforcement point.
1. Import-time validation
The advantage is obvious: failure happens extremely early.
An incompatible module is rejected before it can propagate deeper into the application lifecycle.
The trade-off is that Python imports are easier to reason about when they remain predictable and free of unnecessary behavior.
2. Controlled initialization
Another architecture creates an explicit lifecycle:
discover -> load -> validate -> initialize -> activate
This gives the host much more control over when a component becomes active and often makes diagnostics easier to handle.
The trade-off is that Python code may already have executed during import, depending on how the module is loaded.
3. Plugin manager as admission controller
For extensible applications, this is often the cleanest conceptual model.
The plugin manager stops being only a discovery mechanism and becomes a governance layer. It can inspect the component, evaluate the contract, produce diagnostics, and decide whether the component is allowed to activate.
ImportSpy supports validation at import time as well as controlled initialization, which lets the architecture choose the boundary that fits the system rather than forcing one lifecycle on every application.
A concrete failure mode
Consider a host expecting this plugin interface:
class Processor:
def process(self, payload: bytes) -> bytes:
...
A new version of a plugin exposes:
class Processor:
def process(self, payload: str) -> str:
...
The module is valid Python.
The import can succeed.
The class exists.
The method exists.
A simplistic loader may accept it.
Without an admission check, the incompatibility appears later, somewhere inside an execution path that may be far removed from plugin loading.
With a contract-aware boundary, the system changes the failure path:
plugin discovered
|
v
contract checked
|
v
signature mismatch
|
v
REJECT
The contract system does not magically eliminate the error.
It changes where the error is allowed to exist.
A late integration failure becomes an early compatibility failure with structured diagnostics.
Why this matters in long-lived systems
Small systems can survive on implicit knowledge surprisingly well.
Someone on the team knows:
Don't upgrade that plugin beyond version 2.
Someone else remembers:
That component requires this environment variable.
Another developer knows:
That implementation only works on the Linux deployment.
The architecture exists partly in code and partly in people's heads.
Then time passes.
People leave. Infrastructure changes. New deployments appear. Dependencies evolve. Python versions move forward.
The hidden contract is still there.
The people who understood it may not be.
This is where architectural drift becomes expensive.
The problem is not necessarily bad code. The problem is that assumptions have become invisible.
A runtime contract moves some of that knowledge out of people's heads and into something that can be inspected, versioned, tested, and enforced.
That is the part of this idea that interests me most.
Not merely catching errors.
Making architectural assumptions visible.
This became ImportSpy
This design process is what eventually shaped ImportSpy.
I have been working on it for roughly two years.
Today I describe it as a runtime contract enforcement engine for Python modules rather than simply an import validator.
ImportSpy allows developers to declare and verify structural, contextual, and execution constraints on modules. Contracts are authored in a YAML-based DSL, compiled into an internal SpyModel, and can be validated at import time or during controlled initialization.
The current model can express requirements around:
- module structure
- required classes and functions
- signatures and annotations
- runtime invariants
- Python and operating-system constraints
- environment requirements
- dependency restrictions
- deployment compatibility
The project is open source:
GitHub: https://github.com/atellaluca/ImportSpy
Documentation: https://importspy.atellaluca.com/
One thing I explicitly do not claim is sandboxing.
ImportSpy is an architectural governance tool, not a hard-security boundary. Validating a contract does not make arbitrary Python code safe to execute. Process isolation, containers, permissions, and OS-level controls still belong to the security layer.
An unexpected connection with modern AI tool systems
One reason I think this problem is becoming more relevant is that we are recreating plugin architectures in new forms.
Consider a modern system exposing tools or capabilities to an AI agent:
Host / Agent
|
v
Capability registry
|
v
Tool / Extension
The terminology changed.
The architectural question did not.
Before the host allows an extension to participate, it still needs to understand things such as:
- What interface does it expose?
- What does it depend on?
- What environment does it expect?
- Which capabilities does it require?
- Is this implementation compatible with this runtime?
A schema can describe the callable interface.
It does not necessarily describe the complete execution contract.
That is why I think runtime admission deserves to be considered separately from discovery.
Loading components is easy.
Deciding whether they belong in the current runtime is the harder problem.
The most important lesson from the project
The biggest change in ImportSpy was not a new validator or another feature.
It was changing the question.
I originally asked:
How can I validate an import?
Today I ask:
What conditions must be true before this component is allowed to participate in the system?
That produces a much more useful architecture.
It moves the discussion from Python mechanics to system boundaries.
Once the problem is modeled that way, imports become only one possible place where policy can be enforced.
Where I would take this next
There are several directions I find interesting.
One is contract evolution. If modules and runtimes evolve independently, contracts eventually need compatibility semantics of their own.
Another is diagnostics. Rejecting a component is useful. Explaining precisely why it was rejected is much more useful.
And perhaps the most interesting direction is treating contracts as architectural artifacts that participate across the whole lifecycle:
development -> CI -> deployment -> runtime
The same assumptions could then be checked at several stages instead of being discovered only after deployment.
How are you handling this boundary?
I suspect many mature Python systems already implement some version of this idea without calling it a runtime contract.
Maybe it lives inside a plugin manager.
Maybe it is encoded in initialization logic.
Maybe the system relies mostly on packaging metadata and Protocols.
Maybe compatibility is negotiated between components.
Or maybe the architecture deliberately avoids dynamic admission altogether.
If you maintain a large Python application, plugin platform, extensible backend, embedded runtime, or tool system, I would be interested in hearing where you draw this boundary.
Because after two years of working on ImportSpy, the part I find most interesting is no longer how to intercept an import.
It is deciding when a piece of code has earned the right to become part of the running system.




Top comments (0)