{"id":627,"date":"2026-08-29T05:04:57","date_gmt":"2026-08-29T05:04:57","guid":{"rendered":"https:\/\/abrarqasim.com\/blog\/python-dependency-management-the-import-you-never-declared\/"},"modified":"2026-08-29T05:04:57","modified_gmt":"2026-08-29T05:04:57","slug":"python-dependency-management-the-import-you-never-declared","status":"publish","type":"post","link":"https:\/\/abrarqasim.com\/blog\/python-dependency-management-the-import-you-never-declared\/","title":{"rendered":"Python Dependency Management: The Import You Never Declared"},"content":{"rendered":"<p>I read Simon Willison&rsquo;s release notes the way other people read box scores, and last week a two-line postmortem in one of them sent me on a Saturday audit of every pyproject.toml I own. The short version: fresh installs of his <code>llm<\/code> CLI broke overnight, through no change of his own, because a library he imported directly was only being installed as a side effect of a different dependency. Then that dependency stopped needing it, and the floor disappeared.<\/p>\n<p>I found the same landmine in two of my own projects within the hour. Odds are decent it&rsquo;s sitting in one of yours, which is why this post is about python dependency management and the single rule that would have prevented the whole mess.<\/p>\n<h2 id=\"what-actually-broke\">What actually broke<\/h2>\n<p>The sequence, from <a href=\"https:\/\/simonwillison.net\/2026\/Aug\/21\/llm\/\" rel=\"nofollow noopener\" target=\"_blank\">Simon&rsquo;s 0.32.1 release note<\/a>: the OpenAI Python library dropped its usage of httpx. <code>llm<\/code> imports httpx in its own code, but never declared it; the package arrived as a transitive dependency of <code>openai<\/code>. When openai 3.0 stopped depending on it, every fresh <code>pip install llm<\/code> produced a tool that crashed on import. Existing installs kept working, which is the cruel part. Nothing looked wrong on any machine that already had the package.<\/p>\n<p>The immediate fix was pinning <code>openai&lt;3<\/code>. The <a href=\"https:\/\/simonwillison.net\/2026\/Aug\/22\/llm\/\" rel=\"nofollow noopener\" target=\"_blank\">proper fix landed in llm 0.33<\/a>, which switched to the new httpx2 library alongside the ecosystem. Anthropic&rsquo;s SDK made the same migration in its 1.0 release, and plugin authors got to update all over again.<\/p>\n<p>None of this is a criticism of Simon, to be clear. He diagnosed and shipped a fix within a day, in public, with his reasoning attached. That transparency is exactly why the bug is useful to the rest of us: it&rsquo;s a clean specimen of a failure mode that usually stays murky.<\/p>\n<h2 id=\"why-nobodys-tests-caught-it\">Why nobody&rsquo;s tests caught it<\/h2>\n<p>The nastiest property of this bug is that it&rsquo;s invisible everywhere except a clean machine. Your laptop has the package because it&rsquo;s been installed since forever. CI probably has it too, because dependency caches exist precisely to avoid reinstalling from scratch, and a cache that predates the upstream change will keep serving the old dependency tree indefinitely. The test suite imports httpx, finds it, and goes green. Everything agrees the project is fine, and everything is wrong.<\/p>\n<p>The only environment that tells the truth is a fresh install from your declared dependencies, resolved today. That&rsquo;s what new users get, which is why they hit the crash while maintainers see nothing. If your CI never does a cold, cacheless install of your published package, you&rsquo;re testing the environment you accumulated, not the one you ship. I now keep one job that does exactly that: build the wheel, install it into an empty venv with no cache, import the entry point. It&rsquo;s slow. It&rsquo;s also the only job that would have caught this.<\/p>\n<h2 id=\"the-rule-declare-what-you-import\">The rule: declare what you import<\/h2>\n<p>If your code contains <code>import httpx<\/code>, then httpx belongs in your dependency list. Full stop. It doesn&rsquo;t matter that some other package already brings it in. Your dependencies&rsquo; dependencies are their implementation details, and they can change in any release, including a patch release, without notice and without it being anyone&rsquo;s fault.<\/p>\n<p>I got this wrong for years, and I got it wrong for a reason that felt responsible at the time: I didn&rsquo;t want to duplicate entries. If <code>openai<\/code> needs httpx anyway, declaring it twice seemed redundant, and a shorter dependency list felt cleaner. But &ldquo;redundant&rdquo; was doing a lot of work in that sentence. The declaration isn&rsquo;t redundant; it&rsquo;s the only thing that makes your actual requirements visible to the resolver. Without it, your install works by coincidence.<\/p>\n<p>The test I use now: could I delete any single line from my dependency list, reinstall from scratch, and still import everything my code imports? If deleting a line breaks an import that line doesn&rsquo;t mention, something is undeclared somewhere.<\/p>\n<h2 id=\"before-and-after\">Before and after<\/h2>\n<p>Here&rsquo;s the shape of the bug in a pyproject.toml:<\/p>\n<pre><code class=\"language-toml\"># Before: httpx arrives by accident, as a side effect of openai\n[project]\nname = &quot;my-tool&quot;\ndependencies = [\n    &quot;openai&gt;=2.0&quot;,\n]\n<\/code><\/pre>\n<pre><code class=\"language-python\"># my_tool\/client.py\nimport httpx  # works today, undeclared, one upstream release from breaking\n<\/code><\/pre>\n<pre><code class=\"language-toml\"># After: everything the code imports at runtime is declared\n[project]\nname = &quot;my-tool&quot;\ndependencies = [\n    &quot;openai&gt;=2.0,&lt;3&quot;,\n    &quot;httpx&gt;=0.27&quot;,\n]\n<\/code><\/pre>\n<p>That&rsquo;s the entire fix. It&rsquo;s almost insultingly small, which is exactly why it never gets prioritized until a Friday deploy fails.<\/p>\n<p>A close cousin of the same bug lives in optional extras. If you declare a package under <code>[project.optional-dependencies]<\/code> but import it unconditionally at module load, everyone who installs without that extra gets the same crash on import. Either guard the import or promote the dependency to the main list. I&rsquo;ve shipped that one too, and the bug report politely asking why my &ldquo;optional&rdquo; feature was mandatory still stings a little.<\/p>\n<h2 id=\"finding-your-undeclared-imports\">Finding your undeclared imports<\/h2>\n<p>You don&rsquo;t have to eyeball this. <a href=\"https:\/\/github.com\/fpgmaas\/deptry\" rel=\"nofollow noopener\" target=\"_blank\">deptry<\/a> scans a project and reports exactly this class of problem, including imports that resolve only through transitive packages:<\/p>\n<pre><code class=\"language-bash\">pip install deptry\ndeptry .\n<\/code><\/pre>\n<p>It flags missing dependencies, unused declared dependencies, and transitive imports in one pass. On the first project I pointed it at, it found the httpx pattern twice and a requests import I&rsquo;d been getting from an SDK I was in the middle of removing. Ten minutes, three real bugs.<\/p>\n<p>For the inverse question, &ldquo;what is pulling this package in?&rdquo;, pipdeptree does the job:<\/p>\n<pre><code class=\"language-bash\">pip install pipdeptree\npipdeptree --reverse --packages httpx\n<\/code><\/pre>\n<p>Run that before you assume anything about where a package comes from. The answers surprise me about a third of the time, and I do this for a living.<\/p>\n<h2 id=\"pinning-is-not-the-same-thing-as-declaring\">Pinning is not the same thing as declaring<\/h2>\n<p>One confusion worth clearing up, because I see it in code review constantly: a lockfile does not solve this. A lockfile freezes the exact versions of whatever the resolver happened to select, including your accidental transitive deps. Your app installs reproducibly right up until you regenerate the lock, and then the floor can vanish just like it did for <code>llm<\/code>. Locking preserves a coincidence; declaring removes it.<\/p>\n<p>Version caps are the other half, and they&rsquo;re contested territory. The packaging community has argued for years about blanket upper bounds, with good reason: over-capping causes its own resolution misery downstream. My personal rule is narrower. I cap the major version of SDKs with a track record of breaking me, the way <code>openai&lt;3<\/code> saved llm&rsquo;s users, and I leave stable libraries uncapped. The <a href=\"https:\/\/packaging.python.org\/en\/latest\/specifications\/dependency-specifiers\/\" rel=\"nofollow noopener\" target=\"_blank\">dependency specifiers guide on packaging.python.org<\/a> covers the syntax; the judgment about which packages deserve caps is yours, and it comes from scar tissue.<\/p>\n<p>There&rsquo;s an extra reason I care about this lately: AI-generated code makes the undeclared-import problem worse. An agent writing Python will cheerfully import whatever happens to be importable in its environment, with zero awareness of what&rsquo;s declared. I already <a href=\"https:\/\/abrarqasim.com\/blog\/python-sandbox-for-ai-code-check-dev-kvm-first\" rel=\"noopener\">sandbox the Python that agents write<\/a> before running it; deptry in CI is the packaging-side complement, catching what the agent assumed but never declared. Most of the <a href=\"https:\/\/abrarqasim.com\/work\" rel=\"noopener\">automation pipelines I build for clients<\/a> now have both wired in from day one.<\/p>\n<h2 id=\"the-ten-minute-audit\">The ten minute audit<\/h2>\n<p>Here&rsquo;s the thing to do this week, on one repo, the one that would page you if installs broke:<\/p>\n<pre><code class=\"language-bash\">pip install deptry\ndeptry . || true          # see everything it flags\npipdeptree --warn fail    # surface version conflicts while you're here\n<\/code><\/pre>\n<p>Fix what deptry reports: add the missing declarations, delete the unused ones. Then regenerate your lockfile from the corrected list and run the test suite. If everything passes, you&rsquo;ve converted a pile of coincidences into an actual specification of what your project needs, and the next time a big SDK sheds a dependency over a weekend, you&rsquo;ll read the postmortem the way I read Simon&rsquo;s: with sympathy, and with coffee, and with nothing to fix.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>A transitive dependency broke fresh installs of a popular Python CLI overnight. What that failure taught me about python dependency management done properly.<\/p>\n","protected":false},"author":2,"featured_media":626,"comment_status":"","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"rank_math_title":"","rank_math_description":"A transitive dependency broke fresh installs of a popular Python CLI overnight. What that failure taught me about python dependency management done properly.","rank_math_focus_keyword":"python dependency management","rank_math_canonical_url":"","rank_math_robots":"","footnotes":""},"categories":[147,528],"tags":[686,688,687,685,530],"class_list":["post-627","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-backend","category-python","tag-dependencies","tag-deptry","tag-packaging","tag-pip","tag-python"],"_links":{"self":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/627","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/users\/2"}],"replies":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/comments?post=627"}],"version-history":[{"count":0,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/posts\/627\/revisions"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media\/626"}],"wp:attachment":[{"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/media?parent=627"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/categories?post=627"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/abrarqasim.com\/blog\/wp-json\/wp\/v2\/tags?post=627"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}