<?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: Vlad Shulcz</title>
    <description>The latest articles on DEV Community by Vlad Shulcz (@vshulcz).</description>
    <link>https://dev.to/vshulcz</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%2F3952384%2Ff1362989-8536-4b3b-946f-9f2b4e5a6235.png</url>
      <title>DEV Community: Vlad Shulcz</title>
      <link>https://dev.to/vshulcz</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/vshulcz"/>
    <language>en</language>
    <item>
      <title>I indexed 3.3 GB of my coding agents' logs so I'd stop re-solving the same bugs</title>
      <dc:creator>Vlad Shulcz</dc:creator>
      <pubDate>Thu, 16 Jul 2026 18:06:50 +0000</pubDate>
      <link>https://dev.to/vshulcz/i-indexed-33-gb-of-my-coding-agents-logs-so-id-stop-re-solving-the-same-bugs-6k7</link>
      <guid>https://dev.to/vshulcz/i-indexed-33-gb-of-my-coding-agents-logs-so-id-stop-re-solving-the-same-bugs-6k7</guid>
      <description>&lt;p&gt;A few weeks ago I spent most of an evening debugging a postgres connection leak that I was pretty sure I had already debugged. Not a similar one. The same one, in the same service, with the same &lt;code&gt;pgxpool&lt;/code&gt; symptoms. Somewhere around midnight I found my old fix — not in git history, and not in my notes, because of course I didn't write any. It was in a Claude Code transcript from April, sitting in a JSONL file under &lt;code&gt;~/.claude/projects/&lt;/code&gt;, where it had been the whole time.&lt;/p&gt;

&lt;p&gt;That file exists because every coding agent keeps full session transcripts on disk. All of them. Nobody reads these files, most people don't know they exist, and after a few months of agent-heavy work they quietly become the best engineering notebook you never wrote. Mine was 3.3 GB across three agents, roughly 50k messages.&lt;/p&gt;

&lt;p&gt;So the obvious first move: grep it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s2"&gt;"connection pool"&lt;/span&gt; ~/.claude/projects/ | &lt;span class="nb"&gt;head&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This technically works and practically doesn't. Claude Code transcripts are JSONL where the text lives at &lt;code&gt;.message.content&lt;/code&gt;, which is sometimes a string and sometimes an array of typed blocks. Tool outputs dominate the byte count, so most matches are inside a 40 KB &lt;code&gt;cat&lt;/code&gt; result rather than anything a human said. And that's one agent out of the several I actually use. Codex has a different layout (&lt;code&gt;~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl&lt;/code&gt;, plus a separate &lt;code&gt;history.jsonl&lt;/code&gt; that partially duplicates it). opencode keeps everything in SQLite. You can build a jq pipeline for one of these in ten minutes; you will not maintain jq pipelines for eight.&lt;/p&gt;

&lt;p&gt;I know eight because I ended up cataloguing them:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Agent&lt;/th&gt;
&lt;th&gt;Storage&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Claude Code&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.claude/projects/&amp;lt;encoded-path&amp;gt;/*.jsonl&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Codex CLI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;~/.codex/sessions/.../rollout-*.jsonl&lt;/code&gt; + &lt;code&gt;history.jsonl&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;opencode&lt;/td&gt;
&lt;td&gt;SQLite&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Cursor&lt;/td&gt;
&lt;td&gt;SQLite &lt;code&gt;state.vscdb&lt;/code&gt; for IDE chats, separate transcripts for the CLI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Gemini CLI&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;~/.gemini/tmp/&amp;lt;hash&amp;gt;/chats/&lt;/code&gt; — the same session in .json &lt;em&gt;and&lt;/em&gt; .jsonl&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;aider&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.aider.chat.history.md&lt;/code&gt;, one per repo, markdown&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Antigravity&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;transcript.jsonl&lt;/code&gt; under &lt;code&gt;~/.gemini/antigravity/brain/&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Grok Build&lt;/td&gt;
&lt;td&gt;&lt;code&gt;~/.grok/sessions/&amp;lt;url-encoded-cwd&amp;gt;/&amp;lt;id&amp;gt;/updates.jsonl&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&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%2Fo7akc2fy7k9rns7ctg0x.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%2Fo7akc2fy7k9rns7ctg0x.png" alt=" " width="800" height="322"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;None of this is documented. Some of it is actively weird. The Claude Code project directory encodes the project path by replacing &lt;code&gt;/&lt;/code&gt; with &lt;code&gt;-&lt;/code&gt;, which means &lt;code&gt;-Users-me-my-app&lt;/code&gt; is ambiguous: was that &lt;code&gt;my/app&lt;/code&gt; or a directory literally named &lt;code&gt;my-app&lt;/code&gt;? You can't tell from the string alone; you have to walk the filesystem and check what exists. Gemini writes the same session as both a checkpoint .json and an event-log .jsonl, and the .jsonl contains &lt;code&gt;$rewindTo&lt;/code&gt; events, so reading it means replaying edits, not just parsing lines.&lt;/p&gt;

&lt;p&gt;The result is &lt;a href="https://github.com/vshulcz/deja-vu" rel="noopener noreferrer"&gt;deja-vu&lt;/a&gt;, a single Go binary that parses all of this into one local index:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/vshulcz/deja-vu/main/install.sh | sh
deja &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--auto&lt;/span&gt;
deja &lt;span class="s2"&gt;"connection pool exhausted"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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%2Fk0uaihy5idwfn2flf2d0.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%2Fk0uaihy5idwfn2flf2d0.png" alt=" " width="800" height="682"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The screenshot shows the property I actually wanted: hits come back from Claude Code, Codex and Antigravity sessions at once. The April fix is findable regardless of which agent I happened to be talking to in April. And because it indexes what's already on disk, there's no cold-start problem — no proxy to install, no capture hook, no "it'll be useful in a month". The history is already there; it just wasn't queryable.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not embeddings
&lt;/h2&gt;

&lt;p&gt;Fair question, since every agent-memory project reaches for a vector store. I considered it and decided against, for a boring reason: when I search these logs, I'm searching for an error string, a function name, or a flag. &lt;code&gt;"SSL_ERROR_SYSCALL"&lt;/code&gt;, &lt;code&gt;resolveEncodedPath&lt;/code&gt;, &lt;code&gt;--force-with-lease&lt;/code&gt;. Exact tokens. Semantic similarity actively hurts here — I don't want things &lt;em&gt;like&lt;/em&gt; my error, I want the session where that literal string appeared, because that's where the fix is.&lt;/p&gt;

&lt;p&gt;So the index is embarrassingly classical: a records file, varint-encoded token postings, a small manifest. It rebuilds from scratch in about 14 seconds on my corpus and answers warm queries in single-digit milliseconds. There's no daemon; the CLI checks file sizes and mtimes on each run and incrementally ingests whatever changed. The whole thing is one static binary because it started as a tool I scp'd between machines, and I wanted that to keep working.&lt;/p&gt;

&lt;h2&gt;
  
  
  The parts that bit me
&lt;/h2&gt;

&lt;p&gt;Incremental indexing of files that other processes are actively writing turned out to be where all the actual engineering lives.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Torn tails.&lt;/strong&gt; An agent can be mid-write when the indexer runs, so the file ends in half a JSON line. Skipping the broken line is easy. The bug is on the &lt;em&gt;next&lt;/em&gt; run: if you resume from the new end-of-file, the message that was half-written last time is now complete — and you've skipped it forever. The fix is to store the offset of the last complete newline at index time and resume from there, not from wherever the file ended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;History that un-happens.&lt;/strong&gt; I assumed transcripts were append-only. Gemini and Grok both violate this: a rewind truncates the file and regrows it with different content. Size-based change detection sees "file got bigger" and happily appends, duplicating everything before the truncation point. Those files now get re-parsed in full whenever they change, and the session's records are replaced rather than extended.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The re-merge that ate seconds.&lt;/strong&gt; An early user with 592 Cursor sessions reported that every search took seconds. Cursor's SQLite database has no per-message watermark I was tracking, so every query re-merged the entire chat history. Embarrassing to explain, easy to fix once a real corpus hit it: track the DB's last-updated timestamp and query only newer rows. My test fixtures were too small to notice. Now there's a 10k-message fixture in CI precisely because of this.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;filepath.Dir&lt;/code&gt; at the root.&lt;/strong&gt; A walk-up loop that terminates on &lt;code&gt;dir != "/"&lt;/code&gt; runs forever on Windows, because &lt;code&gt;filepath.Dir("C:\\")&lt;/code&gt; is &lt;code&gt;"C:\\"&lt;/code&gt;. Found by a hung Windows CI job, ten minutes of nothing followed by a goroutine dump pointing at &lt;code&gt;filepath.Clean&lt;/code&gt;. The loop now breaks when &lt;code&gt;Dir(dir) == dir&lt;/code&gt;, which is what I should have written the first time.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wiring it into the agents
&lt;/h2&gt;

&lt;p&gt;Search from a shell is half of it. The other half is the agents using the memory themselves: &lt;code&gt;deja mcp&lt;/code&gt; is a stdio MCP server with a &lt;code&gt;recall&lt;/code&gt; tool, and &lt;code&gt;deja install --auto&lt;/code&gt; writes it into whatever agent configs exist on the machine — &lt;code&gt;.claude.json&lt;/code&gt;, Codex's &lt;code&gt;config.toml&lt;/code&gt;, Cursor's &lt;code&gt;mcp.json&lt;/code&gt;, Grok's &lt;code&gt;config.toml&lt;/code&gt;, and so on, each with a &lt;code&gt;.bak&lt;/code&gt; and an uninstall that removes only its own block.&lt;/p&gt;

&lt;p&gt;Where the agent has a hook mechanism, install goes further: Claude Code gets a SessionStart hook so relevant past sessions are injected before you type anything. Codex and opencode have equivalents. The others don't expose any way to inject context (Grok has hooks, but stdout from passive events is discarded), so for them the MCP tool is as deep as it goes. aider has no MCP client at all; &lt;code&gt;deja ctx "query" &amp;gt; ctx.md&lt;/code&gt; and passing the file in is the workaround, and I won't pretend it's elegant.&lt;/p&gt;

&lt;p&gt;One more thing, since Grok Build's cloud upload made the rounds this week: deja reads local files and writes a local index. The only network operation in the entire tool is &lt;code&gt;deja sync ssh &amp;lt;host&amp;gt;&lt;/code&gt;, which you run yourself — I use it to keep a laptop and a mac mini seeing each other's sessions. Batches go over plain SSH, dedup is content-based, nothing else leaves the machine. Transcripts are also full of pasted secrets, so everything passes through redaction patterns (AWS keys, JWTs, PEM blocks, the usual shapes) before hitting the index; patterns aren't understanding, though, so read &lt;code&gt;deja share&lt;/code&gt; output before sending it to anyone.&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%2Fagpnwvvjm82fsqecwzey.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%2Fagpnwvvjm82fsqecwzey.png" alt=" " width="800" height="666"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it doesn't do
&lt;/h2&gt;

&lt;p&gt;It won't find "that auth thing we discussed" — it's token search, not semantic. Cursor IDE chats need the &lt;code&gt;sqlite3&lt;/code&gt; CLI on PATH because I refused to take a C dependency for one harness. And eight undocumented formats means eight parsers that upstream can break at any moment; the test suite has fixtures for every format and a cross-harness suite in CI, but realistically, field reports are part of the maintenance model. If your agent isn't covered or its format changed, an issue with a redacted sample is the fastest path: the Grok Build parser went from issue to merged PR in a day, and most of it was written by the person who opened it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/vshulcz/deja-vu" rel="noopener noreferrer"&gt;github.com/vshulcz/deja-vu&lt;/a&gt; — MIT, no runtime dependencies.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>go</category>
      <category>productivity</category>
      <category>opensource</category>
    </item>
    <item>
      <title>How I made dependency injection in Python 130x faster: from reflection to compiling the graph</title>
      <dc:creator>Vlad Shulcz</dc:creator>
      <pubDate>Thu, 18 Jun 2026 10:05:53 +0000</pubDate>
      <link>https://dev.to/vshulcz/how-i-made-dependency-injection-in-python-130x-faster-from-reflection-to-compiling-the-graph-2pke</link>
      <guid>https://dev.to/vshulcz/how-i-made-dependency-injection-in-python-130x-faster-from-reflection-to-compiling-the-graph-2pke</guid>
      <description>&lt;p&gt;There's a stock argument against DI containers in Python: a container is just overhead, pass the dependency into the constructor yourself and stop overengineering. It sounds reasonable, and I believed it for a long time. But "sounds reasonable" isn't a number, so at some point I sat down and measured: what does a container actually cost on every resolve, and can you even catch up to hand-wiring without breaking the semantics?&lt;/p&gt;

&lt;p&gt;Short version: you can get very close. But the number isn't really the point — the road there is, because most of the steps are tricks that pay off well beyond DI: spotting overhead that's invisible in a single call, deleting code that provably never runs, and keeping &lt;code&gt;exec&lt;/code&gt;-based codegen from quietly breaking production along the way.&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%2F4bxcneizmvryfchjou0k.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%2F4bxcneizmvryfchjou0k.png" alt="How a DI resolve went from 52.9 to 0.40 µs/op" width="800" height="463"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The graph is small but typical for a backend: singletons on top (config and a client), transient repository, email sender and audit below, and a &lt;code&gt;RegisterUser&lt;/code&gt; use case that pulls all three. The benchmark resolves this graph in a loop; next to it, as a lower bound, the same objects built by hand. Same machine for every measurement. The numbers are synthetic and tied to the shape of the graph — this is not a universal ranking of containers, it's a reproducible check of one shape, and the script is open.&lt;/p&gt;

&lt;p&gt;The starting point was sobering: by hand — &lt;strong&gt;0.27 µs/op&lt;/strong&gt;, the naive container — &lt;strong&gt;52.9&lt;/strong&gt;. Almost two hundred times slower. For reference, &lt;code&gt;punq&lt;/code&gt; — a real reflection-based container — does about 57 µs/op on the same graph, so the number isn't made up. That's just what you get if you inspect constructors on every resolve.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where 53 microseconds come from
&lt;/h2&gt;

&lt;p&gt;The naive resolver reaches into the constructor on every call: it takes &lt;code&gt;inspect.signature&lt;/code&gt;, calls &lt;code&gt;get_type_hints&lt;/code&gt;, recursively resolves the dependencies from the annotations, and constructs the object. The problem is that &lt;code&gt;get_type_hints&lt;/code&gt; and signature parsing are expensive — annotation evaluation, MRO walking, allocations. Once, it's nothing. A million times in a row, it's those tens of microseconds.&lt;/p&gt;

&lt;p&gt;The obvious move: parse the graph once. At registration (or on the first resolve) you read the constructor and store a "plan" — which dependencies, in what order, with what lifetime. After that, resolving follows the plan, with no &lt;code&gt;signature&lt;/code&gt; or &lt;code&gt;get_type_hints&lt;/code&gt; at all.&lt;/p&gt;

&lt;p&gt;That one change removes almost all of the cost: &lt;strong&gt;52.9 → 0.818 µs/op&lt;/strong&gt;, roughly 65×. And then comes the part people usually leave alone. They shouldn't.&lt;/p&gt;

&lt;h2&gt;
  
  
  Twist one: a check that could never fire
&lt;/h2&gt;

&lt;p&gt;With the plan cached, every "fast" constructor was wrapped in a cycle guard:&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;def&lt;/span&gt; &lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&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;cls&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;resolving&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;                 &lt;span class="c1"&gt;# cycle guard
&lt;/span&gt;        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="nc"&gt;CyclicDependencyException&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;resolving&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;dep0&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nf"&gt;dep1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scope&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;resolving&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A set lookup, an insert, a &lt;code&gt;try/finally&lt;/code&gt; — per node, per resolve. Looks mandatory. But here's the catch: a fast constructor is only built when the subgraph is already proven acyclic. While building the plan, the moment it hits a cycle the compiler returns &lt;code&gt;None&lt;/code&gt;, and that graph falls back to the slow, interpreted path — which is where the guard lives. So on the fast path, &lt;code&gt;cls in resolving&lt;/code&gt; can never become true.&lt;/p&gt;

&lt;p&gt;It's a guard that physically cannot fire. I removed it from the fast path; cycle detection stays where it actually works — in the interpreter and in a separate graph-validation step. A cyclic graph simply never gets a fast constructor and is caught as before. The takeaway: if an invariant is already guaranteed earlier in the code, re-checking it at runtime just costs you. A few percent, for free.&lt;/p&gt;

&lt;h2&gt;
  
  
  An allocation on every call
&lt;/h2&gt;

&lt;p&gt;The profiler flagged one more small thing that's expensive because of how often it happens. &lt;code&gt;resolve(SomeType)&lt;/code&gt; for the most common case — resolve by type, no name, no scope — built a key tuple &lt;code&gt;(interface, None)&lt;/code&gt; and read a couple of registration attributes. Nanoseconds per call, but there are millions of calls. A direct &lt;code&gt;type → constructor&lt;/code&gt; dict for that case (cleared when registrations change or a test override kicks in) gets rid of both the tuple allocation and the extra reads.&lt;/p&gt;

&lt;h2&gt;
  
  
  Twist two: compile the graph — and nearly break production
&lt;/h2&gt;

&lt;p&gt;The biggest reserve was in the shape of the fast path itself. A transient graph was built as a tree of nested closures: resolving the use case called the use-case closure, which called the repository closure, which called the singleton client's getter. One function call per node. Worse, a shared singleton needed by two siblings was fetched twice.&lt;/p&gt;

&lt;p&gt;The classic fix: glue the whole transient chain into one flat function — inline the constructors and compute each shared singleton once instead of twice. This is exactly what compilers call common-subexpression elimination (CSE).&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%2Fck1e0dsfxjfqelpbr2x4.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%2Fck1e0dsfxjfqelpbr2x4.png" alt="A tree of nested calls becomes one flat function" width="799" height="351"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Mechanically it's code generation: from the graph's plan I build the source of a function and bring it to life through &lt;code&gt;exec&lt;/code&gt; inside a closure with the needed symbols. The leaves — singletons, scopes, instances — stay as the previous constructors (I don't touch their caching or lazy creation, I reuse them as-is); only the transient part becomes flat, which is exactly what runs on every resolve. Not a single class name or user value goes into the generated source — only opaque generated symbols, so there's nothing to inject through it.&lt;/p&gt;

&lt;p&gt;That gave the main win: &lt;strong&gt;0.818 → 0.401 µs/op&lt;/strong&gt;. From the naive version, that's about 130×, and the container now trails hand-wiring by less than 1.5×.&lt;/p&gt;

&lt;p&gt;And this is where I nearly stopped. &lt;code&gt;exec&lt;/code&gt;-based code generation in a library is a special kind of risk. A bug in it doesn't fail with a stack trace. It quietly builds the wrong object in someone's production: hands you the wrong implementation, loses a shared singleton, swaps argument order. For DI, that's the worst case — "works, but wrong."&lt;/p&gt;

&lt;p&gt;So I didn't ship it on vibes. I did the thing I'd recommend to anyone touching code generation: equivalence fuzzing. The idea is simple: generate thousands of random acyclic graphs with mixed lifetimes, optional and default parameters; resolve each one two ways — the compiled path and a separate, deliberately dumb reference resolver; and compare not the values, but the structure of the result. The same classes, and the same pattern of shared references — where a singleton must be the same object and a transient must be different ones.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;4000 random graphs — the structure matched on every one
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Whatever the compiler can't handle — factories, property injection, injecting the container itself, cycles — it honestly returns &lt;code&gt;None&lt;/code&gt; and falls back to the old path. That harness, not "looks right," is the reason &lt;code&gt;exec&lt;/code&gt;-generated code made it into a release at all. If you build something similar: a separate reference plus a structural comparison is worth more than a hundred hand-written tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest limits
&lt;/h2&gt;

&lt;p&gt;So I don't oversell it. The numbers are synthetic and tied to the shape of the graph: with lots of scopes, async resources, or factories the picture is different — run it on yours. And if your wiring is mostly about async resource lifecycles or rich scopes — opening and closing pools or sessions as part of resolution — that's a different design with real merits, and libraries like dishka and dependency-injector are built for exactly that; this one isn't. The flat compilation speeds up exactly transient chains with shared singletons; if you're mostly factories or property injection, there's no win, because those nodes go down the interpreted path anyway. And below ~0.4 µs in pure Python without a C extension you can't meaningfully go: the next step is Cython, and that's a different conversation about dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  Wrap-up
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th&gt;Resolve, µs/op&lt;/th&gt;
&lt;th&gt;What changed&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;by hand&lt;/td&gt;
&lt;td&gt;0.271&lt;/td&gt;
&lt;td&gt;lower bound&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;naive container&lt;/td&gt;
&lt;td&gt;52.9&lt;/td&gt;
&lt;td&gt;reflection on every resolve&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ cached plan&lt;/td&gt;
&lt;td&gt;0.818&lt;/td&gt;
&lt;td&gt;parse constructors once&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;+ flat function, CSE, dispatch dict&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;0.401&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;compiling the graph&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three things stuck with me from this one:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Measure first.&lt;/strong&gt; "Containers are expensive" was intuition; the number (200× slower than hand-wiring) showed where the cost actually sat, and that 99% of it goes away with one plan cache.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't cling to code that provably never runs.&lt;/strong&gt; Re-checking an invariant that's already guaranteed earlier is a cost with no upside.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;exec&lt;/code&gt;-based codegen only behind an equivalence fuzzer.&lt;/strong&gt; Silent wiring bugs are worse than crashes; a separate reference catches them in batches.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I did all of this on a small typed DI container I maintain — &lt;a href="https://github.com/vshulcz/injex" rel="noopener noreferrer"&gt;Injex&lt;/a&gt;, code and benchmark are open if you want the implementation details. But the techniques, not the library, are the point.&lt;/p&gt;

</description>
      <category>python</category>
      <category>performance</category>
      <category>programming</category>
      <category>ddd</category>
    </item>
    <item>
      <title>Fast dependency injection in Python without a provider framework</title>
      <dc:creator>Vlad Shulcz</dc:creator>
      <pubDate>Tue, 09 Jun 2026 07:54:07 +0000</pubDate>
      <link>https://dev.to/vshulcz/fast-dependency-injection-in-python-without-a-provider-framework-3dko</link>
      <guid>https://dev.to/vshulcz/fast-dependency-injection-in-python-without-a-provider-framework-3dko</guid>
      <description>&lt;p&gt;Python does not need a dependency injection container by default.&lt;br&gt;
For small apps, direct constructor calls are clearer than any framework:&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;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;database_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EmailSender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;smtp_url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;use_case&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is fine once.&lt;/p&gt;

&lt;p&gt;The problem starts when the same object graph appears in more than one place:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI startup;&lt;/li&gt;
&lt;li&gt;Typer commands;&lt;/li&gt;
&lt;li&gt;background workers;&lt;/li&gt;
&lt;li&gt;scripts;&lt;/li&gt;
&lt;li&gt;tests.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At that point, the question is not "how do I inject everything?"&lt;/p&gt;

&lt;p&gt;The better question is:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Where should application wiring live?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The boundary I want
&lt;/h2&gt;

&lt;p&gt;For web apps, I like this split:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;FastAPI owns HTTP adaptation;&lt;/li&gt;
&lt;li&gt;Typer owns CLI adaptation;&lt;/li&gt;
&lt;li&gt;workers own job adaptation;&lt;/li&gt;
&lt;li&gt;the application owns service wiring.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;That means repositories, gateways, and use cases should depend on normal Python types, not framework primitives.&lt;/p&gt;

&lt;p&gt;A service should not need to know whether it was called from an HTTP request, a CLI command, a queue worker, or a test.&lt;/p&gt;

&lt;h2&gt;
  
  
  FastAPI Depends is not the whole composition root
&lt;/h2&gt;

&lt;p&gt;FastAPI Depends is excellent at the request boundary.&lt;/p&gt;

&lt;p&gt;It handles request data, authentication, headers, cookies, and per-request adapters.&lt;/p&gt;

&lt;p&gt;But if the same service graph is used outside HTTP, the real composition root should live in plain Python.&lt;/p&gt;

&lt;p&gt;A useful shape is:&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;def&lt;/span&gt; &lt;span class="nf"&gt;build_services&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApiClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EmailSender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;Services&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;register_user&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nc"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;FastAPI can adapt that graph:&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="nd"&gt;@asynccontextmanager&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;lifespan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;FastAPI&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_services&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;load_settings&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;get_register_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;register_user&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Workers and CLIs can use the same builder directly:&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;services&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;build_services&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;load_settings&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;services&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;register_user&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ada@example.com&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;The rule of thumb:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;FastAPI adapts HTTP. The application owns service wiring.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Start with factories
&lt;/h2&gt;

&lt;p&gt;I still think plain factories are the best default.&lt;/p&gt;

&lt;p&gt;If your graph is small, this is better than adding any container:&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;def&lt;/span&gt; &lt;span class="nf"&gt;build_register_user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;ApiClient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EmailSender&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nc"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A DI container becomes useful later, when the same graph starts repeating across entrypoints and tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why I built Injex
&lt;/h2&gt;

&lt;p&gt;I built Injex for that middle ground.&lt;/p&gt;

&lt;p&gt;Not a full provider framework.&lt;br&gt;
Not framework-specific dependency injection.&lt;br&gt;
Not a replacement for FastAPI Depends.&lt;/p&gt;

&lt;p&gt;The niche is small Python apps that want:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;explicit registrations;&lt;/li&gt;
&lt;li&gt;constructor injection from type hints;&lt;/li&gt;
&lt;li&gt;singleton, transient, and scoped lifetimes;&lt;/li&gt;
&lt;li&gt;test overrides;&lt;/li&gt;
&lt;li&gt;graph validation before startup;&lt;/li&gt;
&lt;li&gt;zero runtime dependencies.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example:&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;injex&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;


&lt;span class="n"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_instance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Settings&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;settings&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_singleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ApiClient&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_transient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_transient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmailSender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_transient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_valid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;use_case&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Application classes stay plain:&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;RegisterUser&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;__init__&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;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EmailSender&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;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repo&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;email&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No decorators required for constructor injection.&lt;br&gt;
No provider DSL.&lt;br&gt;
No runtime dependencies.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed in 1.3.0
&lt;/h2&gt;

&lt;p&gt;Injex 1.3.0 focused on two things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;cleaner internals;&lt;/li&gt;
&lt;li&gt;faster repeated resolves.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Internally, the package is now split into focused modules:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;container.py;&lt;/li&gt;
&lt;li&gt;planning.py;&lt;/li&gt;
&lt;li&gt;registry.py;&lt;/li&gt;
&lt;li&gt;errors.py.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For performance, Injex now caches dependency plans and uses a fast path for common constructor-injection graphs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Benchmark
&lt;/h2&gt;

&lt;p&gt;I added a reproducible benchmark for a small service graph:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;singleton Settings;&lt;/li&gt;
&lt;li&gt;singleton ApiClient(settings);&lt;/li&gt;
&lt;li&gt;transient UserRepository(client);&lt;/li&gt;
&lt;li&gt;transient EmailSender(client);&lt;/li&gt;
&lt;li&gt;transient AuditLog(settings);&lt;/li&gt;
&lt;li&gt;transient RegisterUser(repo, email, audit).&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Local result
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Library&lt;/th&gt;
&lt;th&gt;Median resolve time&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;manual wiring&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.265 µs/op&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Injex&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.818 µs/op&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wireup, same scope&lt;/td&gt;
&lt;td&gt;&lt;code&gt;0.879 µs/op&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wireup, scope per operation&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.559 µs/op&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;dependency-injector&lt;/td&gt;
&lt;td&gt;&lt;code&gt;1.727 µs/op&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;lagom&lt;/td&gt;
&lt;td&gt;&lt;code&gt;9.794 µs/op&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;punq&lt;/td&gt;
&lt;td&gt;&lt;code&gt;56.795 µs/op&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is not a universal ranking.&lt;/p&gt;

&lt;p&gt;Different graphs, lifetimes, async resources, framework integrations, and request scope models can change results.&lt;/p&gt;

&lt;p&gt;The benchmark exists to answer a narrower question:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Can explicit typed wiring stay small and fast?&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;For this graph, yes.&lt;/p&gt;

&lt;p&gt;Reproduce it:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;uv run --with punq --with lagom --with dependency-injector --with wireup \&lt;br&gt;
  python benchmarks/resolve_graph.py&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;When I would not use Injex&lt;/p&gt;

&lt;p&gt;I would skip Injex when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a few constructor calls are still clear;&lt;/li&gt;
&lt;li&gt;a framework dependency system covers every entrypoint;&lt;/li&gt;
&lt;li&gt;the app needs a large provider/configuration DSL;&lt;/li&gt;
&lt;li&gt;the team does not want a container at all.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Manual wiring is still the baseline.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where Injex fits
&lt;/h2&gt;

&lt;p&gt;I would consider Injex when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a service layer is reused by API, CLI, workers, and tests;&lt;/li&gt;
&lt;li&gt;constructors already describe dependencies with type hints;&lt;/li&gt;
&lt;li&gt;tests need temporary external-service overrides;&lt;/li&gt;
&lt;li&gt;startup should catch missing registrations before first request/job;&lt;/li&gt;
&lt;li&gt;the team wants explicit wiring without a large DI framework.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;p&gt;Repo:&lt;br&gt;
&lt;a href="https://github.com/vshulcz/injex" rel="noopener noreferrer"&gt;https://github.com/vshulcz/injex&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Docs:&lt;br&gt;
&lt;a href="https://vshulcz.github.io/injex/" rel="noopener noreferrer"&gt;https://vshulcz.github.io/injex/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Performance notes:&lt;br&gt;
&lt;a href="https://vshulcz.github.io/injex/docs/performance.html" rel="noopener noreferrer"&gt;https://vshulcz.github.io/injex/docs/performance.html&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Compared to FastAPI Depends:&lt;br&gt;
&lt;a href="https://github.com/vshulcz/injex/blob/main/docs/fastapi-depends.md" rel="noopener noreferrer"&gt;https://github.com/vshulcz/injex/blob/main/docs/fastapi-depends.md&lt;/a&gt;&lt;/p&gt;

</description>
      <category>python</category>
      <category>fastapi</category>
      <category>architecture</category>
      <category>opensource</category>
    </item>
    <item>
      <title>When Python Manual Wiring Turns Into Copy-Paste Architecture</title>
      <dc:creator>Vlad Shulcz</dc:creator>
      <pubDate>Tue, 26 May 2026 11:07:59 +0000</pubDate>
      <link>https://dev.to/vshulcz/when-python-manual-wiring-turns-into-copy-paste-architecture-2ib6</link>
      <guid>https://dev.to/vshulcz/when-python-manual-wiring-turns-into-copy-paste-architecture-2ib6</guid>
      <description>&lt;p&gt;I usually avoid dependency injection containers in Python.&lt;/p&gt;

&lt;p&gt;Not because dependency injection is bad. Most of us already do it when we pass objects into constructors. I avoid containers because small Python apps usually do not need one.&lt;/p&gt;

&lt;p&gt;Manual wiring is often the right starting point:&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;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;email_sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;EmailSender&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;register_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email_sender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is explicit, easy to debug, and boring in a good way.&lt;/p&gt;

&lt;p&gt;The problem starts when this code stops living in one place.&lt;/p&gt;

&lt;h2&gt;
  
  
  The Code Drifts
&lt;/h2&gt;

&lt;p&gt;The same services need to be created across completely different entrypoints.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;api/routes.py&lt;/code&gt;:&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;register_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;EmailSender&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In &lt;code&gt;cli/commands.py&lt;/code&gt;:&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="c1"&gt;# Oops, forgot to update this one when dependencies changed?
&lt;/span&gt;&lt;span class="n"&gt;register_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="nc"&gt;MockEmailSender&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The code does not fail all at once. It drifts.&lt;/p&gt;

&lt;p&gt;One entrypoint gets a new dependency:&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;register_user&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;repo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email_sender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;audit_log&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Another still creates the old version. A test uses a fake for one service but accidentally keeps a real client for another. A worker starts fine, then fails only when the first real job touches the missing path.&lt;/p&gt;

&lt;p&gt;That is the kind of boring failure I wanted to avoid. It was not a big architecture disaster. It was worse in a boring way: tiny setup details repeated in enough places that forgetting one became easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem is not dependency injection
&lt;/h2&gt;

&lt;p&gt;Most Python code already uses dependency injection in the boring sense:&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;RegisterUser&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;__init__&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;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email_sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EmailSender&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;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repo&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;email_sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email_sender&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The class is fine. It receives dependencies instead of creating them.&lt;/p&gt;

&lt;p&gt;The mess appears around the class, when every entrypoint has to remember how to build the same graph.&lt;/p&gt;

&lt;h2&gt;
  
  
  Framework DI stops at the framework boundary
&lt;/h2&gt;

&lt;p&gt;FastAPI &lt;code&gt;Depends&lt;/code&gt; is useful. Framework dependency systems are good when the framework owns the entrypoint.&lt;/p&gt;

&lt;p&gt;But application services often need to run elsewhere:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;maintenance commands;&lt;/li&gt;
&lt;li&gt;scheduled jobs;&lt;/li&gt;
&lt;li&gt;message consumers;&lt;/li&gt;
&lt;li&gt;tests;&lt;/li&gt;
&lt;li&gt;local scripts.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I do not want my service layer to care whether it is being called from an API route, a CLI command, or a worker. Construction can stay at the edge. The service layer can stay plain.&lt;/p&gt;

&lt;h2&gt;
  
  
  I wanted something smaller than a full DI container
&lt;/h2&gt;

&lt;p&gt;Python already has mature DI libraries. Some of them support provider objects, configuration systems, framework integrations, and dozens of advanced features.&lt;/p&gt;

&lt;p&gt;That is useful if you need it. I wanted a smaller tool for the middle ground:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Normal constructor injection (no magic syntax).&lt;/li&gt;
&lt;li&gt;Type hints as the wiring contract.&lt;/li&gt;
&lt;li&gt;Singleton, transient, and scoped lifetimes.&lt;/li&gt;
&lt;li&gt;Temporary overrides in tests.&lt;/li&gt;
&lt;li&gt;Validation before startup.&lt;/li&gt;
&lt;li&gt;Zero runtime dependencies &amp;amp; no decorators required on your services.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;I built &lt;code&gt;injex&lt;/code&gt; as one attempt at that middle ground.&lt;/p&gt;

&lt;p&gt;Here is how the wiring can live in one place.&lt;/p&gt;

&lt;p&gt;In a real app, these classes would live in separate modules. They are inline here to keep the example short:&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="c1"&gt;# app/ioc.py
&lt;/span&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="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;injex&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Container&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;UserRepository&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;save&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;email&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;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="bp"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;SqlUserRepository&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserRepository&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;save&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;email&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;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mi"&gt;42&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;EmailSender&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;send_welcome&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;email&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="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Welcome, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;RegisterUser&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;__init__&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;repo&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email_sender&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;EmailSender&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;repo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;repo&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;email_sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;email_sender&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;execute&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;email&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;int&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&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;repo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;save&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Centralized configuration
&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Container&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Bind abstract interfaces to concrete implementations
&lt;/span&gt;&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_singleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UserRepository&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SqlUserRepository&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_singleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmailSender&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_transient&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_valid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now each entrypoint uses the same graph.&lt;/p&gt;

&lt;p&gt;In your &lt;code&gt;api/routes.py&lt;/code&gt; or &lt;code&gt;cli/commands.py&lt;/code&gt;, you just resolve what you need:&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;app.ioc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;RegisterUser&lt;/span&gt;

&lt;span class="n"&gt;use_case&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;use_case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;ada@example.com&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;The service class stays plain Python. No decorators, no base class, no framework imports. The container only owns construction.&lt;/p&gt;

&lt;h2&gt;
  
  
  The part I care about most: validation
&lt;/h2&gt;

&lt;p&gt;The most useful feature for me is not resolving objects. It is finding broken wiring before anything important runs.&lt;/p&gt;

&lt;p&gt;If a dependency is missing, or a constructor annotation is wrong, I want to know at startup. Not after the first HTTP request. Not after the first worker job. Not in a rarely used CLI path at 3 AM.&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;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assert_valid&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you forgot to register &lt;code&gt;AuditLog&lt;/code&gt;, startup fails immediately with a clear error:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Container validation failed with 1 error(s):
- RegisterUser: Dependency 'audit_log' is not registered: AuditLog.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or you can collect errors explicitly in your CI/CD sanity-check script:&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;errors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;validate&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;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;error&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;errors&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;error&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;SystemExit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The crucial detail is that validation checks the dependency graph without constructing service instances. That matters when constructors open files, spin up thread pools, or establish heavy database connections.&lt;/p&gt;

&lt;h2&gt;
  
  
  Test overrides
&lt;/h2&gt;

&lt;p&gt;Tests are where copy-pasted wiring becomes especially annoying.&lt;/p&gt;

&lt;p&gt;You either rebuild the whole graph for each test, or you mutate shared setup and hope it gets restored correctly. I wanted replacement to be explicit and temporary:&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;FakeEmailSender&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;__init__&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;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent_to&lt;/span&gt; &lt;span class="o"&gt;=&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;send_welcome&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;email&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="bp"&gt;None&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;sent_to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;fake_sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;FakeEmailSender&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# Explicit context manager for tests
&lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;override&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;EmailSender&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;instance&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fake_sender&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;use_case&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RegisterUser&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;use_case&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;fake_sender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sent_to&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;test@example.com&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="c1"&gt;# Outside the block, the original configuration is restored.
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That keeps tests explicit without permanently mutating the production container or introducing side effects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Scoped lifetimes
&lt;/h2&gt;

&lt;p&gt;Some objects should live for one request, one job, or one message, but not for the whole lifecycle of the process.&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;RequestContext&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_scoped&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;scope_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_scope&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;scope_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create_scope&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;scope_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;scope_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;scope_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;scope_b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;RequestContext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is ideal for message consumers (like Celery or RabbitMQ workers) that reuse long-lived clients but still need isolated per-job state.&lt;/p&gt;

&lt;h2&gt;
  
  
  Multiple implementations
&lt;/h2&gt;

&lt;p&gt;Sometimes resolving one implementation is wrong. You want every handler, plugin, or pipeline step registered for the same 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="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_singleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Notifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EmailNotifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_singleton&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Notifier&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SlackNotifier&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Resolve all registered implementations at once
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;notifier&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;container&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;resolve_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Notifier&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;notifier&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;notify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Deployment complete&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 is enough for simple fan-out notifications, plugin architectures, and processing pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  When I would not use it
&lt;/h2&gt;

&lt;p&gt;A container should pay rent. I would still skip one if:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the wiring fits in one file;&lt;/li&gt;
&lt;li&gt;there is only one entrypoint;&lt;/li&gt;
&lt;li&gt;tests can easily pass fakes directly via standard constructors;&lt;/li&gt;
&lt;li&gt;the framework dependency system (like FastAPI) already covers 100% of your operational surface;&lt;/li&gt;
&lt;li&gt;the project needs a massive provider/configuration ecosystem.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Manual wiring is still the baseline. A container should only appear when it removes repeated construction code instead of adding ceremony.&lt;/p&gt;

&lt;h2&gt;
  
  
  Closing
&lt;/h2&gt;

&lt;p&gt;I am not trying to make Python look like Java. I still prefer manual wiring when an application is small enough.&lt;/p&gt;

&lt;p&gt;I built &lt;code&gt;injex&lt;/code&gt; for the awkward middle: the graph is repeated enough to hurt, but not complex enough to justify a large provider framework. It is small, type-hint driven, zero-dependency, and focused entirely on explicit app wiring.&lt;/p&gt;

&lt;p&gt;If you have run into this problem in Python services, CLIs, workers, or tests, I would appreciate your feedback on the API and examples!&lt;/p&gt;

&lt;p&gt;Links:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;GitHub: &lt;a href="https://github.com/vshulcz/injex" rel="noopener noreferrer"&gt;github.com/vshulcz/injex&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;PyPI: &lt;a href="https://pypi.org/project/injex/" rel="noopener noreferrer"&gt;pypi.org/project/injex&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Docs: &lt;a href="https://vshulcz.github.io/injex/" rel="noopener noreferrer"&gt;vshulcz.github.io/injex&lt;/a&gt; &lt;/li&gt;
&lt;/ul&gt;

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