<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Luca</title>
    <description>The latest articles on DEV Community by Luca (@atellaluca).</description>
    <link>https://dev.to/atellaluca</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1202495%2F23bed0da-b0b3-4d46-92d0-14617d573c5d.png</url>
      <title>DEV Community: Luca</title>
      <link>https://dev.to/atellaluca</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/atellaluca"/>
    <language>en</language>
    <item>
      <title>Case Study: The Missing Admission Layer in Python Plugin Systems</title>
      <dc:creator>Luca</dc:creator>
      <pubDate>Thu, 24 Sep 2026 17:51:32 +0000</pubDate>
      <link>https://dev.to/atellaluca/case-study-the-missing-admission-layer-in-python-plugin-systems-4k80</link>
      <guid>https://dev.to/atellaluca/case-study-the-missing-admission-layer-in-python-plugin-systems-4k80</guid>
      <description>&lt;h2&gt;
  
  
  How two years of work on ImportSpy turned an import-validation problem into a runtime-contract model for modular Python systems.
&lt;/h2&gt;

&lt;p&gt;Plugin architectures are interesting again.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;The implementation often starts with something deceptively simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;importlib&lt;/span&gt;

&lt;span class="n"&gt;plugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;importlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;import_module&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;my_plugin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If the import succeeds, the plugin exists.&lt;/p&gt;

&lt;p&gt;But does that mean the host should actually allow it to participate in the system?&lt;/p&gt;

&lt;p&gt;After working on this problem for roughly two years while developing &lt;strong&gt;ImportSpy&lt;/strong&gt;, I no longer think that is primarily an import problem.&lt;/p&gt;

&lt;p&gt;I think it is an &lt;strong&gt;admission problem&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;And that distinction changed the architecture of the project.&lt;/p&gt;




&lt;h2&gt;
  
  
  The starting point
&lt;/h2&gt;

&lt;p&gt;Imagine a Python application that supports independently developed plugins.&lt;/p&gt;

&lt;p&gt;At the beginning, the contract between the host and a plugin may be mostly implicit. The host expects something like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Plugin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A loader can import the module, inspect it, and start using it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;module&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;importlib&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;import_module&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plugin_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plugin_class&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Plugin&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plugin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;plugin_class&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plugin&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This works until the system starts evolving.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;All of these modules may still be perfectly valid Python modules.&lt;/p&gt;

&lt;p&gt;Some may even import successfully.&lt;/p&gt;

&lt;p&gt;That is where the first important distinction appears:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;importable != compatible
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python answers one question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can this module be imported?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The host application needs an answer to a different question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Is this module admissible in this runtime?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdcvx13o8cghrn7k9ezk1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdcvx13o8cghrn7k9ezk1.png" alt="Two Python modules can both be importable while only one matches the runtime contract expected by the host." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Why existing mechanisms only solve part of the problem
&lt;/h2&gt;

&lt;p&gt;Python already gives us several good mechanisms for expressing software contracts.&lt;/p&gt;

&lt;p&gt;An abstract base class can describe a required interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;abc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;abstractmethod&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Plugin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nd"&gt;@abstractmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="nd"&gt;@abstractmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A &lt;code&gt;Protocol&lt;/code&gt; can make the relationship more flexible:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Protocol&lt;/span&gt;


&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;PluginProtocol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;initialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Static typing is useful too.&lt;/p&gt;

&lt;p&gt;But these mechanisms mostly describe &lt;strong&gt;structural compatibility&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The systems I was thinking about had assumptions that extended beyond Python interfaces:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Python &amp;gt;= 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
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;At this point the compatibility problem spans several layers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Python structure&lt;/li&gt;
&lt;li&gt;signatures and annotations&lt;/li&gt;
&lt;li&gt;environment&lt;/li&gt;
&lt;li&gt;dependencies&lt;/li&gt;
&lt;li&gt;interpreter characteristics&lt;/li&gt;
&lt;li&gt;operating system&lt;/li&gt;
&lt;li&gt;deployment assumptions&lt;/li&gt;
&lt;li&gt;runtime invariants&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;No single Python interface mechanism naturally represents all of them.&lt;/p&gt;




&lt;h2&gt;
  
  
  The first design mistake: thinking about imports
&lt;/h2&gt;

&lt;p&gt;The original idea behind ImportSpy was much closer to:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Validate a Python module when it is imported.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That framing is intuitive. If the module is incompatible, reject it as early as possible.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;But the more I worked on the project, the more I realized that &lt;strong&gt;importing was only one possible enforcement point&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The real concept was somewhere else.&lt;/p&gt;




&lt;h2&gt;
  
  
  The architectural shift: admission instead of import validation
&lt;/h2&gt;

&lt;p&gt;The model eventually became:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module -&amp;gt; contract -&amp;gt; validation -&amp;gt; admission / rejection
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important operation is no longer simply:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import module
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;admit module into this runtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That sounds like a small semantic change. It is not.&lt;/p&gt;

&lt;p&gt;It changes the responsibility of the system.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;That model became the foundation of what ImportSpy is today.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdvlgkkuaolb0zqifvtpj.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdvlgkkuaolb0zqifvtpj.png" alt="ImportSpy admission flow showing a module evaluated against a declarative contract, validated by the host, then admitted or rejected." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  Making architectural assumptions executable
&lt;/h2&gt;

&lt;p&gt;Keeping requirements only in documentation does not solve much.&lt;/p&gt;

&lt;p&gt;You can write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;This plugin requires Python 3.11 and Linux.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But nothing guarantees that those assumptions are actually respected.&lt;/p&gt;

&lt;p&gt;You can scatter checks throughout application code:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;version_info&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;11&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Python 3.11+ required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;platform&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;system&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Linux&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Linux required&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API_TOKEN&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;API_TOKEN is missing&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That works technically.&lt;/p&gt;

&lt;p&gt;Architecturally, though, the contract becomes distributed across the implementation.&lt;/p&gt;

&lt;p&gt;ImportSpy takes a different approach: describe the assumptions declaratively and compile them into an internal model that can be validated by the runtime.&lt;/p&gt;

&lt;p&gt;A simplified contract might express requirements such as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;filename&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;plugin.py&lt;/span&gt;
&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;1.2.3"&lt;/span&gt;

&lt;span class="na"&gt;functions&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;initialize&lt;/span&gt;
    &lt;span class="na"&gt;arguments&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;config&lt;/span&gt;
        &lt;span class="na"&gt;annotation&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;dict&lt;/span&gt;

&lt;span class="na"&gt;classes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Plugin&lt;/span&gt;
    &lt;span class="na"&gt;methods&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;run&lt;/span&gt;

&lt;span class="na"&gt;runtime&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;python&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;&amp;gt;=3.11"&lt;/span&gt;
  &lt;span class="na"&gt;os&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;linux&lt;/span&gt;

&lt;span class="na"&gt;required_environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;API_TOKEN&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The exact syntax is less important than the architectural property it gives us.&lt;/p&gt;

&lt;p&gt;The assumptions become:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicit&lt;/li&gt;
&lt;li&gt;inspectable&lt;/li&gt;
&lt;li&gt;versionable&lt;/li&gt;
&lt;li&gt;testable&lt;/li&gt;
&lt;li&gt;enforceable&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Most importantly, they are no longer trapped inside someone's understanding of the system.&lt;/p&gt;

&lt;p&gt;ImportSpy's actual contracts are YAML-based and are compiled into a &lt;code&gt;SpyModel&lt;/code&gt;, which represents the minimum acceptable contract for a module.&lt;/p&gt;




&lt;h2&gt;
  
  
  Compatibility should describe the minimum, not the whole runtime
&lt;/h2&gt;

&lt;p&gt;One design decision turned out to be particularly important.&lt;/p&gt;

&lt;p&gt;A contract should generally describe the &lt;strong&gt;minimum acceptable context&lt;/strong&gt;, not an exact copy of the runtime.&lt;/p&gt;

&lt;p&gt;Suppose a module requires capabilities A, B, and C, while the host provides A, B, C, D, and E.&lt;/p&gt;

&lt;p&gt;The host should normally be compatible.&lt;/p&gt;

&lt;p&gt;Conceptually:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;required capabilities ⊆ available capabilities
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;ImportSpy uses this kind of subset compatibility strategy.&lt;/p&gt;

&lt;p&gt;The contract establishes a baseline. The runtime is allowed to provide more.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Keep policy separate from implementation
&lt;/h2&gt;

&lt;p&gt;Another requirement emerged while thinking about real systems.&lt;/p&gt;

&lt;p&gt;I did not want every module to contain host-governance logic just so another system could determine whether that module was compatible.&lt;/p&gt;

&lt;p&gt;The cleaner model is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;implementation != admission policy
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The module remains ordinary Python code.&lt;/p&gt;

&lt;p&gt;The contract describes expectations about it.&lt;/p&gt;

&lt;p&gt;The host decides whether those expectations are satisfied.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where should enforcement happen?
&lt;/h2&gt;

&lt;p&gt;Once I stopped thinking exclusively about imports, another question appeared.&lt;/p&gt;

&lt;p&gt;There is no single correct enforcement point.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Import-time validation
&lt;/h3&gt;

&lt;p&gt;The advantage is obvious: failure happens extremely early.&lt;/p&gt;

&lt;p&gt;An incompatible module is rejected before it can propagate deeper into the application lifecycle.&lt;/p&gt;

&lt;p&gt;The trade-off is that Python imports are easier to reason about when they remain predictable and free of unnecessary behavior.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Controlled initialization
&lt;/h3&gt;

&lt;p&gt;Another architecture creates an explicit lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;discover -&amp;gt; load -&amp;gt; validate -&amp;gt; initialize -&amp;gt; activate
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This gives the host much more control over when a component becomes active and often makes diagnostics easier to handle.&lt;/p&gt;

&lt;p&gt;The trade-off is that Python code may already have executed during import, depending on how the module is loaded.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Plugin manager as admission controller
&lt;/h3&gt;

&lt;p&gt;For extensible applications, this is often the cleanest conceptual model.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqdbijyl031do2ti4r5kb.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fqdbijyl031do2ti4r5kb.png" alt="Comparison of three enforcement points for Python runtime contracts: import-time validation, controlled initialization, and plugin-manager admission control." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  A concrete failure mode
&lt;/h2&gt;

&lt;p&gt;Consider a host expecting this plugin interface:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Processor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A new version of a plugin exposes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Processor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;payload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="bp"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The module is valid Python.&lt;/p&gt;

&lt;p&gt;The import can succeed.&lt;/p&gt;

&lt;p&gt;The class exists.&lt;/p&gt;

&lt;p&gt;The method exists.&lt;/p&gt;

&lt;p&gt;A simplistic loader may accept it.&lt;/p&gt;

&lt;p&gt;Without an admission check, the incompatibility appears later, somewhere inside an execution path that may be far removed from plugin loading.&lt;/p&gt;

&lt;p&gt;With a contract-aware boundary, the system changes the failure path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;plugin discovered
      |
      v
contract checked
      |
      v
signature mismatch
      |
      v
REJECT
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The contract system does not magically eliminate the error.&lt;/p&gt;

&lt;p&gt;It changes &lt;strong&gt;where the error is allowed to exist&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;A late integration failure becomes an early compatibility failure with structured diagnostics.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why this matters in long-lived systems
&lt;/h2&gt;

&lt;p&gt;Small systems can survive on implicit knowledge surprisingly well.&lt;/p&gt;

&lt;p&gt;Someone on the team knows:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Don't upgrade that plugin beyond version 2.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Someone else remembers:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;That component requires this environment variable.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Another developer knows:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;That implementation only works on the Linux deployment.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;The architecture exists partly in code and partly in people's heads.&lt;/p&gt;

&lt;p&gt;Then time passes.&lt;/p&gt;

&lt;p&gt;People leave. Infrastructure changes. New deployments appear. Dependencies evolve. Python versions move forward.&lt;/p&gt;

&lt;p&gt;The hidden contract is still there.&lt;/p&gt;

&lt;p&gt;The people who understood it may not be.&lt;/p&gt;

&lt;p&gt;This is where architectural drift becomes expensive.&lt;/p&gt;

&lt;p&gt;The problem is not necessarily bad code. The problem is that assumptions have become invisible.&lt;/p&gt;

&lt;p&gt;A runtime contract moves some of that knowledge out of people's heads and into something that can be inspected, versioned, tested, and enforced.&lt;/p&gt;

&lt;p&gt;That is the part of this idea that interests me most.&lt;/p&gt;

&lt;p&gt;Not merely catching errors.&lt;/p&gt;

&lt;p&gt;Making architectural assumptions visible.&lt;/p&gt;




&lt;h2&gt;
  
  
  This became ImportSpy
&lt;/h2&gt;

&lt;p&gt;This design process is what eventually shaped &lt;strong&gt;ImportSpy&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;I have been working on it for roughly two years.&lt;/p&gt;

&lt;p&gt;Today I describe it as a &lt;strong&gt;runtime contract enforcement engine for Python modules&lt;/strong&gt; rather than simply an import validator.&lt;/p&gt;

&lt;p&gt;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 &lt;code&gt;SpyModel&lt;/code&gt;, and can be validated at import time or during controlled initialization.&lt;/p&gt;

&lt;p&gt;The current model can express requirements around:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;module structure&lt;/li&gt;
&lt;li&gt;required classes and functions&lt;/li&gt;
&lt;li&gt;signatures and annotations&lt;/li&gt;
&lt;li&gt;runtime invariants&lt;/li&gt;
&lt;li&gt;Python and operating-system constraints&lt;/li&gt;
&lt;li&gt;environment requirements&lt;/li&gt;
&lt;li&gt;dependency restrictions&lt;/li&gt;
&lt;li&gt;deployment compatibility&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The project is open source:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;GitHub:&lt;/strong&gt; &lt;a href="https://github.com/atellaluca/ImportSpy" rel="noopener noreferrer"&gt;https://github.com/atellaluca/ImportSpy&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Documentation:&lt;/strong&gt; &lt;a href="https://importspy.atellaluca.com/" rel="noopener noreferrer"&gt;https://importspy.atellaluca.com/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;One thing I explicitly do &lt;strong&gt;not&lt;/strong&gt; claim is sandboxing.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;




&lt;h2&gt;
  
  
  An unexpected connection with modern AI tool systems
&lt;/h2&gt;

&lt;p&gt;One reason I think this problem is becoming more relevant is that we are recreating plugin architectures in new forms.&lt;/p&gt;

&lt;p&gt;Consider a modern system exposing tools or capabilities to an AI agent:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Host / Agent
     |
     v
Capability registry
     |
     v
Tool / Extension
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The terminology changed.&lt;/p&gt;

&lt;p&gt;The architectural question did not.&lt;/p&gt;

&lt;p&gt;Before the host allows an extension to participate, it still needs to understand things such as:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What interface does it expose?&lt;/li&gt;
&lt;li&gt;What does it depend on?&lt;/li&gt;
&lt;li&gt;What environment does it expect?&lt;/li&gt;
&lt;li&gt;Which capabilities does it require?&lt;/li&gt;
&lt;li&gt;Is this implementation compatible with this runtime?&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A schema can describe the callable interface.&lt;/p&gt;

&lt;p&gt;It does not necessarily describe the complete execution contract.&lt;/p&gt;

&lt;p&gt;That is why I think runtime admission deserves to be considered separately from discovery.&lt;/p&gt;

&lt;p&gt;Loading components is easy.&lt;/p&gt;

&lt;p&gt;Deciding whether they belong in the current runtime is the harder problem.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp8i58bm15d7383uiwvs1.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fp8i58bm15d7383uiwvs1.png" alt="A host or AI agent discovers tools, validates interface, environment, dependencies, capabilities, and runtime requirements, then admits or rejects the extension." width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The most important lesson from the project
&lt;/h2&gt;

&lt;p&gt;The biggest change in ImportSpy was not a new validator or another feature.&lt;/p&gt;

&lt;p&gt;It was changing the question.&lt;/p&gt;

&lt;p&gt;I originally asked:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;How can I validate an import?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Today I ask:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;What conditions must be true before this component is allowed to participate in the system?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That produces a much more useful architecture.&lt;/p&gt;

&lt;p&gt;It moves the discussion from Python mechanics to system boundaries.&lt;/p&gt;

&lt;p&gt;Once the problem is modeled that way, imports become only one possible place where policy can be enforced.&lt;/p&gt;




&lt;h2&gt;
  
  
  Where I would take this next
&lt;/h2&gt;

&lt;p&gt;There are several directions I find interesting.&lt;/p&gt;

&lt;p&gt;One is &lt;strong&gt;contract evolution&lt;/strong&gt;. If modules and runtimes evolve independently, contracts eventually need compatibility semantics of their own.&lt;/p&gt;

&lt;p&gt;Another is &lt;strong&gt;diagnostics&lt;/strong&gt;. Rejecting a component is useful. Explaining precisely why it was rejected is much more useful.&lt;/p&gt;

&lt;p&gt;And perhaps the most interesting direction is treating contracts as architectural artifacts that participate across the whole lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;development -&amp;gt; CI -&amp;gt; deployment -&amp;gt; runtime
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The same assumptions could then be checked at several stages instead of being discovered only after deployment.&lt;/p&gt;




&lt;h2&gt;
  
  
  How are you handling this boundary?
&lt;/h2&gt;

&lt;p&gt;I suspect many mature Python systems already implement some version of this idea without calling it a runtime contract.&lt;/p&gt;

&lt;p&gt;Maybe it lives inside a plugin manager.&lt;/p&gt;

&lt;p&gt;Maybe it is encoded in initialization logic.&lt;/p&gt;

&lt;p&gt;Maybe the system relies mostly on packaging metadata and &lt;code&gt;Protocol&lt;/code&gt;s.&lt;/p&gt;

&lt;p&gt;Maybe compatibility is negotiated between components.&lt;/p&gt;

&lt;p&gt;Or maybe the architecture deliberately avoids dynamic admission altogether.&lt;/p&gt;

&lt;p&gt;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.&lt;/p&gt;

&lt;p&gt;Because after two years of working on ImportSpy, the part I find most interesting is no longer how to intercept an import.&lt;/p&gt;

&lt;p&gt;It is deciding &lt;strong&gt;when a piece of code has earned the right to become part of the running system&lt;/strong&gt;.&lt;/p&gt;

</description>
      <category>python</category>
      <category>architecture</category>
      <category>opensource</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
