<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Software-Design | Bas Nijholt</title><link>https://www.nijho.lt/tag/software-design/</link><atom:link href="https://www.nijho.lt/tag/software-design/index.xml" rel="self" type="application/rss+xml"/><description>Software-Design</description><generator>Hugo</generator><language>en-us</language><copyright>© 2026</copyright><lastBuildDate>Mon, 19 Oct 2026 00:00:00 +0000</lastBuildDate><image><url>https://www.nijho.lt/media/icon_hu_408a8f6cef43e327.png</url><title>Software-Design</title><link>https://www.nijho.lt/tag/software-design/</link></image><item><title>Roll your own, fork the rest 🍴</title><link>https://www.nijho.lt/post/roll-your-own-fork-the-rest/</link><pubDate>Mon, 19 Oct 2026 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/roll-your-own-fork-the-rest/</guid><description>&lt;h2 id="introduction-a-follow-up-to-something-i-wrote"&gt;Introduction: a follow-up to something I wrote &lt;a class="anchor" href="#introduction-a-follow-up-to-something-i-wrote" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;In April 2025 I published &lt;a href="https://www.nijho.lt/post/dependencies/"&gt;Confessions of an asymmetric hypocrite&lt;/a&gt;, about my split personality on dependencies.&#10;Strict minimalism for the libraries I publish, and then, for applications:&lt;/p&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;For &lt;em&gt;applications&lt;/em&gt; (especially CLIs in isolated environments), I&amp;rsquo;m far more liberal with dependencies to build features quickly.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;p&gt;I also wrote that I &amp;ldquo;strongly prefer dependencies that are already widely adopted and trusted by the community&amp;rdquo; because &amp;ldquo;these are less likely to cause unexpected issues&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;Then I spent a year building &lt;a href="https://www.nijho.lt/post/mindroom/"&gt;MindRoom&lt;/a&gt;, which is an application, exactly the category where I said I could relax.&lt;/p&gt;&#10;&lt;p&gt;I want to report back.&#10;&amp;ldquo;Prefer &lt;span class="hover-tip" data-tooltip="Code that so many people already run in production that the common bugs have been found and fixed before you get there." tabindex="0"&gt;battle-tested&lt;/span&gt; dependencies&amp;rdquo; was the right rule.&#10;I just had the wrong idea of what battle-tested means.&#10;I thought a project with tens of thousands of GitHub stars qualified.&#10;Now I think it means &lt;strong&gt;millions of downloads, every day.&lt;/strong&gt;&lt;/p&gt;&#10;&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#introduction-a-follow-up-to-something-i-wrote"&gt;Introduction: a follow-up to something I wrote&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#1-stars-measure-interest-not-usage"&gt;1. Stars measure interest, not usage&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-a-popular-protocol-is-not-a-popular-library"&gt;2. A popular protocol is not a popular library&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-i-checked-the-rule-against-my-own-code"&gt;3. I checked the rule against my own code&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-roll-your-own-the-library-gives-you-the-easy-10"&gt;4. Roll your own: the library gives you the easy 10%&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-fork-your-own-when-the-problem-is-hard"&gt;5. Fork your own: when the problem is hard&lt;/a&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#agno-patches-instead-of-a-fork"&gt;Agno: patches instead of a fork&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10; &lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#6-i-am-not-dunking-on-open-source-including-mine"&gt;6. I am not dunking on open source, including mine&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#7-my-rule-for-the-next-uv-add"&gt;7. My rule for the next &lt;code&gt;uv add&lt;/code&gt;&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Stars measure interest, not usage. For every dependency, ask how many projects actually run the code you are about to rely on, and how much you rely on it.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Millions of downloads a day?&lt;/strong&gt; Depend on it.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Fewer, and it only gives you the easy part?&lt;/strong&gt; Roll your own. The hard part was going to be yours anyway.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Fewer, and hard to build?&lt;/strong&gt; Fork it, and treat the fork as yours.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;If I started MindRoom over, I would write my own HTTP requests to the model providers instead of using an AI framework.&lt;/p&gt;&#10;&lt;/div&gt;&lt;/aside&gt;&#10;&lt;blockquote&gt;&#10;&lt;p&gt;&lt;strong&gt;Update 2026-10-27:&lt;/strong&gt; A week after publishing, I took my own advice twice.&#10;MindRoom&amp;rsquo;s knowledge search no longer uses ChromaDB, and code MindRoom owns has replaced Agno&amp;rsquo;s runtime; Agno stays only as the tool library.&#10;The full stories are in &lt;a href="#comments"&gt;the comments&lt;/a&gt;.&lt;/p&gt;&#10;&lt;/blockquote&gt;&#10;&lt;h2 id="1-stars-measure-interest-not-usage"&gt;1. Stars measure interest, not usage &lt;a class="anchor" href="#1-stars-measure-interest-not-usage" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When I picked &lt;a href="https://github.com/agno-agi/agno" target="_blank" rel="noopener"&gt;Agno&lt;/a&gt; as MindRoom&amp;rsquo;s &lt;span class="hover-tip" data-tooltip="A library that wraps the model providers&amp;#39; APIs and adds agents, tools, memory, knowledge bases and session storage on top." tabindex="0"&gt;AI framework&lt;/span&gt;, it looked like the safe choice.&#10;Today it has over 42,000 stars and 6,000 forks.&lt;sup id="fnref:1"&gt;&lt;a href="#fn:1" class="footnote-ref" role="doc-noteref"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;It also gets about &lt;strong&gt;78,000 downloads a day&lt;/strong&gt; on PyPI.&#10;&lt;a href="https://github.com/encode/httpx" target="_blank" rel="noopener"&gt;httpx&lt;/a&gt;, one of Python&amp;rsquo;s standard HTTP libraries, gets about &lt;strong&gt;25 million&lt;/strong&gt;.&lt;sup id="fnref:2"&gt;&lt;a href="#fn:2" class="footnote-ref" role="doc-noteref"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;A star is something you click once, when a README looks exciting.&#10;A download happens every time some project installs the package.&lt;/p&gt;&#10;&lt;p&gt;Most of those downloads are &lt;span class="hover-tip" data-tooltip="Continuous integration: test and build pipelines that install a fresh environment on every push, often many times a day per project." tabindex="0"&gt;CI&lt;/span&gt;, not people, so downloads are not users either.&#10;But a package with millions of downloads a day is installed by a huge number of different projects.&#10;Many of them use the same functions you are about to use, so they hit the bugs before you do.&lt;/p&gt;&#10;&lt;p&gt;Battle-tested is also not the same as old.&#10;I use &lt;a href="https://github.com/astral-sh/uv" target="_blank" rel="noopener"&gt;uv&lt;/a&gt; for everything, and it has only been around for a &lt;span class="hover-tip" data-tooltip="Astral released the first version of uv in February 2024." tabindex="0"&gt;couple of years&lt;/span&gt;.&#10;I run into bugs in uv too, but there is simply no better alternative, and it gets about 3.6 million downloads a day.&lt;/p&gt;&#10;&lt;h2 id="2-a-popular-protocol-is-not-a-popular-library"&gt;2. A popular protocol is not a popular library &lt;a class="anchor" href="#2-a-popular-protocol-is-not-a-popular-library" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;The other place my old idea failed was &lt;a href="https://matrix.org" target="_blank" rel="noopener"&gt;Matrix&lt;/a&gt;.&lt;/p&gt;&#10;&lt;p&gt;Building on Matrix felt obviously correct.&#10;An open protocol, about a decade old, with serious security reviews from &lt;span class="hover-tip" data-tooltip="Governments run their own Matrix deployments, for example Tchap for the French civil service and BwMessenger for the German armed forces." tabindex="0"&gt;large organizations and governments&lt;/span&gt;.&#10;Millions of people use it.&lt;/p&gt;&#10;&lt;p&gt;Matrix is still a good protocol and I am still on it.&#10;But even though millions of people use Matrix, there are only a few client libraries.&lt;/p&gt;&#10;&lt;p&gt;MindRoom streams agent replies by editing the same message many times a second.&#10;Almost every part of the ecosystem I touched had a problem with that: &lt;span class="hover-tip" data-tooltip="The most widely used Matrix app, made by the company behind the protocol." tabindex="0"&gt;Element&lt;/span&gt;, &lt;span class="hover-tip" data-tooltip="A Matrix web client focused on a simple, Discord-like interface. MindRoom&amp;#39;s chat app is a fork of it." tabindex="0"&gt;Cinny&lt;/span&gt;, &lt;span class="hover-tip" data-tooltip="The Python Matrix client library MindRoom&amp;#39;s agents use to log in, read and send messages." tabindex="0"&gt;matrix-nio&lt;/span&gt;, and the &lt;span class="hover-tip" data-tooltip="matrix-js-sdk, the library Element Web and Cinny are built on." tabindex="0"&gt;Matrix JavaScript SDK&lt;/span&gt;.&#10;In the JavaScript SDK, edits that arrived while a thread was still loading were silently dropped, so you saw stale text until you reloaded.&lt;sup id="fnref:3"&gt;&lt;a href="#fn:3" class="footnote-ref" role="doc-noteref"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;What counts is how many projects use the library you build on.&#10;Element&amp;rsquo;s users do not test matrix-js-sdk for you. Other people building on matrix-js-sdk do.&#10;It gets roughly 100,000 downloads a day. matrix-nio, the Python client, about 23,000.&lt;/p&gt;&#10;&lt;h2 id="3-i-checked-the-rule-against-my-own-code"&gt;3. I checked the rule against my own code &lt;a class="anchor" href="#3-i-checked-the-rule-against-my-own-code" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I pulled the daily download numbers for the packages I use and looked for workarounds, patches and forks in my own source.&lt;sup id="fnref:4"&gt;&lt;a href="#fn:4" class="footnote-ref" role="doc-noteref"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;div class="table-wrap"&gt;&#10;&lt;table&gt;&#10;&lt;thead&gt;&#10;&lt;tr&gt;&#10;&lt;th&gt;Package&lt;/th&gt;&#10;&lt;th&gt;Downloads per day&lt;/th&gt;&#10;&lt;th&gt;What it cost me&lt;/th&gt;&#10;&lt;/tr&gt;&#10;&lt;/thead&gt;&#10;&lt;tbody&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;requests&lt;/td&gt;&#10;&lt;td&gt;49M&lt;/td&gt;&#10;&lt;td&gt;nothing I had to work around&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;pydantic&lt;/td&gt;&#10;&lt;td&gt;35M&lt;/td&gt;&#10;&lt;td&gt;nothing&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;numpy&lt;/td&gt;&#10;&lt;td&gt;33M&lt;/td&gt;&#10;&lt;td&gt;nothing&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;httpx&lt;/td&gt;&#10;&lt;td&gt;25M&lt;/td&gt;&#10;&lt;td&gt;nothing&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;aiohttp&lt;/td&gt;&#10;&lt;td&gt;18M&lt;/td&gt;&#10;&lt;td&gt;nothing&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;FastAPI&lt;/td&gt;&#10;&lt;td&gt;17M&lt;/td&gt;&#10;&lt;td&gt;nothing&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;chromadb&lt;/td&gt;&#10;&lt;td&gt;270K&lt;/td&gt;&#10;&lt;td&gt;workarounds for a bug that holds the GIL&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;matrix-js-sdk&lt;/td&gt;&#10;&lt;td&gt;~100K&lt;/td&gt;&#10;&lt;td&gt;patched&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;mem0ai&lt;/td&gt;&#10;&lt;td&gt;79K&lt;/td&gt;&#10;&lt;td&gt;largely reimplemented&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;agno&lt;/td&gt;&#10;&lt;td&gt;78K&lt;/td&gt;&#10;&lt;td&gt;&lt;strong&gt;126 documented workarounds&lt;/strong&gt;&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;tr&gt;&#10;&lt;td&gt;matrix-nio&lt;/td&gt;&#10;&lt;td&gt;23K&lt;/td&gt;&#10;&lt;td&gt;forked&lt;/td&gt;&#10;&lt;/tr&gt;&#10;&lt;/tbody&gt;&#10;&lt;/table&gt;&#10;&lt;/div&gt;&#10;&lt;p&gt;Above a million a day, I had nothing to work around.&#10;Below it, every package on this list cost me a patch, a fork, a rewrite, or a pile of workarounds.&lt;/p&gt;&#10;&lt;p&gt;Take &lt;a href="https://github.com/chroma-core/chroma" target="_blank" rel="noopener"&gt;ChromaDB&lt;/a&gt;, the vector database behind MindRoom&amp;rsquo;s knowledge bases.&#10;In some cases it holds Python&amp;rsquo;s &lt;span class="hover-tip" data-tooltip="The Global Interpreter Lock: only one thread can run Python code at a time. Native code is supposed to release it while it waits, so other threads can keep going." tabindex="0"&gt;GIL&lt;/span&gt; during I/O, which blocks every other thread in the process.&#10;I opened &lt;a href="https://github.com/chroma-core/chroma/pull/7692" target="_blank" rel="noopener"&gt;a pull request&lt;/a&gt; to fix it, and nobody has looked at it yet.&#10;Instead of forking, I run every knowledge read in a short-lived child process, because a process can be killed and a blocked thread cannot.&lt;sup id="fnref:5"&gt;&lt;a href="#fn:5" class="footnote-ref" role="doc-noteref"&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;My fork of matrix-nio has about 8,900 lines added and 2,000 removed in the library code since the last upstream release.&lt;sup id="fnref:6"&gt;&lt;a href="#fn:6" class="footnote-ref" role="doc-noteref"&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;This does not mean every small package needs replacing.&#10;MindRoom pulls in a few hundred packages in total, and plenty of them have modest download numbers.&#10;The scrutiny should match how much I depend on a package.&#10;Something I use for one small, specific purpose is fine.&#10;Agno and matrix-nio are the base that MindRoom is built on, which is probably also why they hurt the most.&lt;/p&gt;&#10;&lt;h2 id="4-roll-your-own-the-library-gives-you-the-easy-10"&gt;4. Roll your own: the library gives you the easy 10% &lt;a class="anchor" href="#4-roll-your-own-the-library-gives-you-the-easy-10" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Take memory libraries, &lt;a href="https://github.com/mem0ai/mem0" target="_blank" rel="noopener"&gt;Mem0&lt;/a&gt; and friends.&#10;I have &lt;a href="https://www.nijho.lt/post/file-based-rag-memory/"&gt;already written about this&lt;/a&gt;: I cloned LlamaIndex, LangChain, Letta, Mem0 and PydanticAI, studied how they work, and re-implemented the parts I needed. My memory system ended up &amp;ldquo;mostly modeled after Mem0&amp;rdquo;.&lt;/p&gt;&#10;&lt;p&gt;The core idea is not hard.&#10;With &lt;span class="hover-tip" data-tooltip="Retrieval-augmented generation: look up relevant text first and paste it into the prompt, so the model answers from your documents instead of from memory." tabindex="0"&gt;RAG&lt;/span&gt;, you chunk, embed, retrieve, re-rank and reconcile. Once you have read a couple of implementations you can write it, and afterwards you understand every line.&lt;/p&gt;&#10;&lt;p&gt;The same is true of an agent framework.&#10;It builds a JSON payload, posts it to an HTTP endpoint, parses a &lt;span class="hover-tip" data-tooltip="Providers send the reply as server-sent events: a long-lived HTTP response with one small JSON chunk per token or tool-call fragment." tabindex="0"&gt;streaming response&lt;/span&gt;, keeps a list of messages, and loops when the model asks for a tool call.&#10;With AI, that is a day or two of work.&lt;/p&gt;&#10;&lt;p&gt;The hard part is making it reliable at scale: never blocking the &lt;span class="hover-tip" data-tooltip="In async Python, one thread runs all tasks by switching between them. A single call that blocks freezes every conversation at once." tabindex="0"&gt;event loop&lt;/span&gt;, and handling the crazy edge cases.&#10;With an agent system you end up building a very complicated state machine, and if you do not watch it, any combination of states is possible. Almost all of them are edge cases.&lt;/p&gt;&#10;&lt;p&gt;A library gives you the easy 10% and leaves you the hard 90%.&#10;And you do that 90% inside somebody else&amp;rsquo;s code, where you cannot fix the root cause.&lt;/p&gt;&#10;&lt;p&gt;MindRoom is now about 360,000 lines of Python, plus about 750,000 lines of tests.&lt;sup id="fnref:7"&gt;&lt;a href="#fn:7" class="footnote-ref" role="doc-noteref"&gt;7&lt;/a&gt;&lt;/sup&gt;&#10;A good part of that is code I wrote instead of depending on a library:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Memory and knowledge:&lt;/strong&gt; about 15,600 lines across 50 files.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Conversation history and &lt;span class="hover-tip" data-tooltip="Summarizing older turns of a conversation so a long thread still fits in the model&amp;#39;s context window." tabindex="0"&gt;compaction&lt;/span&gt;:&lt;/strong&gt; about 8,400 lines.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;An OpenAI-compatible API:&lt;/strong&gt; about 2,300 lines, so MindRoom agents show up as selectable &amp;ldquo;models&amp;rdquo; in any chat frontend that speaks the OpenAI protocol.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Claude provider support:&lt;/strong&gt; about 3,000 lines.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;If I have to write the hard 90% either way, I might as well write the easy 10% too.&lt;/p&gt;&#10;&lt;h2 id="5-fork-your-own-when-the-problem-is-hard"&gt;5. Fork your own: when the problem is hard &lt;a class="anchor" href="#5-fork-your-own-when-the-problem-is-hard" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I am not going to write my own &lt;span class="hover-tip" data-tooltip="Matrix encryption means per-device keys, the Olm and Megolm ratchets, cross-signing, device verification and key backup, all of which have to interoperate with every other client." tabindex="0"&gt;end-to-end encrypted&lt;/span&gt; Matrix SDK, or my own &lt;span class="hover-tip" data-tooltip="The server that stores your rooms and syncs them with other Matrix servers, like a mail server for chat." tabindex="0"&gt;homeserver&lt;/span&gt;.&#10;Those are hard, and also far below the line.&#10;So there I fork.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;I forked the Python Matrix client.&lt;/strong&gt; MindRoom ships its own fork of matrix-nio rather than the upstream package.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;I forked the Matrix server, twice.&lt;/strong&gt; First &lt;span class="hover-tip" data-tooltip="The original Matrix homeserver, written in Python by the Matrix.org team." tabindex="0"&gt;Synapse&lt;/span&gt;. When I ran into scaling problems I moved to &lt;a href="https://github.com/matrix-construct/tuwunel" target="_blank" rel="noopener"&gt;Tuwunel&lt;/a&gt; and forked that.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;I forked the web client.&lt;/strong&gt; &lt;a href="https://github.com/cinnyapp/cinny" target="_blank" rel="noopener"&gt;Cinny&lt;/a&gt; is the Matrix web client MindRoom uses.&lt;/p&gt;&#10;&lt;p&gt;None of these are AI projects. AI is just where I felt it most.&lt;/p&gt;&#10;&lt;p&gt;Whether a fork stays a fork depends on the maintainers.&#10;Tuwunel is extremely receptive to my patches, so I upstream anything that is general enough, and over the last six months I am its fourth most active contributor by commits.&lt;sup id="fnref:8"&gt;&lt;a href="#fn:8" class="footnote-ref" role="doc-noteref"&gt;8&lt;/a&gt;&lt;/sup&gt;&#10;Cinny has not accepted any of my pull requests, so I forked it and stopped trying to upstream.&#10;Same with &lt;span class="hover-tip" data-tooltip="An open-source, self-hosted ChatGPT-style web interface that can talk to many model providers." tabindex="0"&gt;LibreChat&lt;/span&gt;: I had patches open for over a year without a reply, so I forked.&lt;/p&gt;&#10;&lt;p&gt;Keeping a fork up to date used to be miserable.&#10;With an LLM doing the rebasing, pulling in the latest upstream is now cheap, though still a bit of a risk every time.&#10;The bigger cost is that once you fork, you are responsible for the whole codebase, including the parts you never changed.&lt;/p&gt;&#10;&lt;h3 id="agno-patches-instead-of-a-fork"&gt;Agno: patches instead of a fork &lt;a class="anchor" href="#agno-patches-instead-of-a-fork" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h3&gt;&#10;&lt;p&gt;I did not fork Agno. I &lt;span class="hover-tip" data-tooltip="Replacing a library&amp;#39;s functions or methods at runtime from your own code, without changing the installed package." tabindex="0"&gt;monkey-patched&lt;/span&gt; it.&#10;Agno is very actively developed, I have talked to the maintainer, and they know about these problems, so I hoped to upstream my fixes.&#10;I have filed 33 issues and pull requests there.&lt;sup id="fnref:9"&gt;&lt;a href="#fn:9" class="footnote-ref" role="doc-noteref"&gt;9&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;That got out of hand.&#10;MindRoom now carries &lt;strong&gt;126 documented workarounds&lt;/strong&gt; for Agno across 59 files.&#10;Most of them live in 36 dedicated compatibility modules, about 7,200 lines of code, with the rest patched inline in 23 other files.&#10;Every one uses the same comment format, so I can &lt;a href="https://github.com/search?q=repo%3Amindroom-ai%2Fmindroom&amp;#43;AGNO_COMPAT&amp;amp;type=code" target="_blank" rel="noopener"&gt;find all of them with one search&lt;/a&gt;.&#10;Each one says what is wrong upstream, links the issue or PR, and says when I can delete it.&lt;sup id="fnref:10"&gt;&lt;a href="#fn:10" class="footnote-ref" role="doc-noteref"&gt;10&lt;/a&gt;&lt;/sup&gt;&#10;A few examples:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Search failures become plausible empty results.&lt;/strong&gt; A failed knowledge search and an empty index return the same empty list, so my agents confidently told me things did not exist.&lt;sup id="fnref:11"&gt;&lt;a href="#fn:11" class="footnote-ref" role="doc-noteref"&gt;11&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Streamed tool calls get malformed or merged.&lt;/strong&gt;&lt;sup id="fnref:12"&gt;&lt;a href="#fn:12" class="footnote-ref" role="doc-noteref"&gt;12&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Reasoning is silently lost when tool calls are replayed.&lt;/strong&gt;&lt;sup id="fnref:13"&gt;&lt;a href="#fn:13" class="footnote-ref" role="doc-noteref"&gt;13&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Provider errors collapse into a generic 502&lt;/strong&gt;, which throws away the error code you need to decide whether to retry.&lt;sup id="fnref:14"&gt;&lt;a href="#fn:14" class="footnote-ref" role="doc-noteref"&gt;14&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;At this size, the patches amount to a fork.&lt;/p&gt;&#10;&lt;h2 id="6-i-am-not-dunking-on-open-source-including-mine"&gt;6. I am not dunking on open source, including mine &lt;a class="anchor" href="#6-i-am-not-dunking-on-open-source-including-mine" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Everything I named is open source I use every day, for free, built by people doing their best.&#10;I filed issues and PRs, and where maintainers merged fixes I deleted my workarounds.&lt;/p&gt;&#10;&lt;p&gt;My own software is in the same category.&#10;Most new AI tooling is not very good, and MindRoom may well be included.&#10;It is large, fast-moving, and nowhere near millions of daily downloads.&#10;By my own rule, you should think twice before depending on it.&lt;/p&gt;&#10;&lt;p&gt;I also have to admit that I have not yet built a project the roll-your-own way from the start.&#10;So comparing &amp;ldquo;what I would do next time&amp;rdquo; with what I actually did is not entirely fair.&#10;This is what I think right now, and a year from now I may have to write another post correcting this one.&lt;/p&gt;&#10;&lt;h2 id="7-my-rule-for-the-next-uv-add"&gt;7. My rule for the next &lt;code&gt;uv add&lt;/code&gt; &lt;a class="anchor" href="#7-my-rule-for-the-next-uv-add" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I weigh three questions.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;How many projects use it?&lt;/strong&gt;&#10;Daily downloads of the actual package, not stars, and not how many people use the product built on top.&#10;In the millions: depend on it.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;How much do I rely on it?&lt;/strong&gt;&#10;Agno and matrix-nio are the base of MindRoom.&#10;A library I use in a CLI to format a question is not, and if I use it in a narrow way I do not need my own version.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;How hard is it, and how receptive is upstream?&lt;/strong&gt;&#10;If the library is something AI can write in a couple of days, roll my own.&#10;If it is hard, fork it.&#10;If the maintainers accept patches, upstream what is general enough.&lt;/p&gt;&#10;&lt;p&gt;&lt;strong&gt;If I started MindRoom over today, I would write the HTTP requests to the model providers myself.&lt;/strong&gt;&#10;Not even the OpenAI or Anthropic SDKs.&#10;And even httpx is less safe than its download numbers suggest: in early 2026 its maintainer closed the issue tracker and discussions, and the latest stable release is from December 2024.&lt;sup id="fnref:15"&gt;&lt;a href="#fn:15" class="footnote-ref" role="doc-noteref"&gt;15&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;&#10;&lt;p&gt;In 2025 I wrote that the right answer is &amp;ldquo;it depends on the situation&amp;rdquo;, and I still believe that.&#10;I just had the wrong threshold.&#10;Battle-tested means millions of downloads a day.&#10;Below that: roll your own, and fork the rest.&lt;/p&gt;&#10;&lt;div class="footnotes" role="doc-endnotes"&gt;&#10;&lt;hr&gt;&#10;&lt;ol&gt;&#10;&lt;li id="fn:1"&gt;&#10;&lt;p&gt;From the GitHub API (&lt;code&gt;api.github.com/repos/agno-agi/agno&lt;/code&gt;) on 2026-10-02: 42,385 stars and 6,041 forks.&amp;#160;&lt;a href="#fnref:1" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:2"&gt;&#10;&lt;p&gt;PyPI numbers are the &lt;code&gt;last_day&lt;/code&gt; field of the &lt;a href="https://pypistats.org" target="_blank" rel="noopener"&gt;pypistats.org&lt;/a&gt; API (&lt;code&gt;/api/packages/&amp;lt;name&amp;gt;/recent&lt;/code&gt;), fetched on 2026-10-02, except uv, fetched on 2026-10-18 (3,648,827). The matrix-js-sdk number is npm&amp;rsquo;s weekly download count (about 730,000) divided by seven. Daily numbers wobble, so treat them as orders of magnitude.&amp;#160;&lt;a href="#fnref:2" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:3"&gt;&#10;&lt;p&gt;When events arrive for a thread that has not finished initializing, the SDK&amp;rsquo;s replay path never applies them, so an &lt;code&gt;m.replace&lt;/code&gt; edit (the event type Matrix uses for &amp;ldquo;this replaces message X&amp;rdquo;) is stored and never shown. I replayed a captured event trace through the SDK: unpatched, zero of seventeen edits reached the listeners; patched, all seventeen. MindRoom carries the fix as a &lt;code&gt;patch-package&lt;/code&gt; patch on the SDK&amp;rsquo;s &lt;code&gt;thread.ts&lt;/code&gt;.&amp;#160;&lt;a href="#fnref:3" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:4"&gt;&#10;&lt;p&gt;For each direct dependency I looked at three things in the MindRoom repository: whether &lt;code&gt;pyproject.toml&lt;/code&gt; pins a fork instead of the upstream package, whether &lt;code&gt;patch-package&lt;/code&gt; patches it, and whether the source has workaround markers or comments pointing at upstream issues for it. The packages above a million downloads a day had none of those.&amp;#160;&lt;a href="#fnref:4" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:5"&gt;&#10;&lt;p&gt;The PR is titled &amp;ldquo;Release GIL during metadata I/O&amp;rdquo;. On the MindRoom side, &lt;code&gt;knowledge/read_process.py&lt;/code&gt; starts the child process, and a TODO says to remove it once a Chroma release with the fix is pinned. A blocked thread holding the GIL cannot be interrupted from Python, but the kernel will always terminate a child process.&amp;#160;&lt;a href="#fnref:5" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:6"&gt;&#10;&lt;p&gt;Compared with upstream matrix-nio 0.25.2 (released 2024-10-04): &lt;code&gt;git diff --shortstat&lt;/code&gt; on the library code gives 8,872 lines added and 1,966 removed across 52 files, from 89 commits, 76 of them mine. The fork is published on PyPI as &lt;code&gt;mindroom-nio&lt;/code&gt;.&amp;#160;&lt;a href="#fnref:6" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:7"&gt;&#10;&lt;p&gt;Counted with &lt;code&gt;git ls-files&lt;/code&gt; and &lt;code&gt;wc -l&lt;/code&gt; on the MindRoom repository on 2026-10-18: 360,513 lines in 1,222 Python files under &lt;code&gt;src/&lt;/code&gt;, 750,657 lines under &lt;code&gt;tests/&lt;/code&gt;, and another 81,543 lines of TypeScript in the frontend. The per-feature numbers below come from the same count over the relevant directories.&amp;#160;&lt;a href="#fnref:7" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:8"&gt;&#10;&lt;p&gt;Non-merge commits on Tuwunel&amp;rsquo;s main branch since 2026-04-18: Jason Volk 1,332, June Strawberry 325, dasha_uwu 55, me 42.&amp;#160;&lt;a href="#fnref:8" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:9"&gt;&#10;&lt;p&gt;Issues and pull requests in &lt;a href="https://github.com/agno-agi/agno/issues?q=author%3Abasnijholt" target="_blank" rel="noopener"&gt;agno-agi/agno&lt;/a&gt; with me as the author, counted on 2026-10-02.&amp;#160;&lt;a href="#fnref:9" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:10"&gt;&#10;&lt;p&gt;Every workaround starts with a comment like &lt;code&gt;# AGNO_COMPAT: Knowledge search hides provider failures as empty results.&lt;/code&gt;, followed by &lt;code&gt;Reason:&lt;/code&gt;, &lt;code&gt;Upstream issue:&lt;/code&gt;, &lt;code&gt;Upstream PR:&lt;/code&gt;, &lt;code&gt;Remove when:&lt;/code&gt; and the tests that cover it. The counts come from &lt;code&gt;grep&lt;/code&gt; over &lt;code&gt;src/&lt;/code&gt;: 126 &lt;code&gt;AGNO_COMPAT:&lt;/code&gt; markers in 59 files, and 7,160 lines in the 36 &lt;code&gt;agno_compat_*&lt;/code&gt; modules.&amp;#160;&lt;a href="#fnref:10" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:11"&gt;&#10;&lt;p&gt;&lt;code&gt;Knowledge.search&lt;/code&gt; catches vector store and provider errors and returns &lt;code&gt;[]&lt;/code&gt;. &lt;a href="https://github.com/agno-agi/agno/issues/10150" target="_blank" rel="noopener"&gt;agno#10150&lt;/a&gt;, partially fixed by &lt;a href="https://github.com/agno-agi/agno/pull/10152" target="_blank" rel="noopener"&gt;agno#10152&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:11" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:12"&gt;&#10;&lt;p&gt;When a streamed tool-call index does not start at zero, Agno leaves empty placeholder calls. &lt;a href="https://github.com/agno-agi/agno/issues/8879" target="_blank" rel="noopener"&gt;agno#8879&lt;/a&gt;, fix proposed in &lt;a href="https://github.com/agno-agi/agno/pull/8880" target="_blank" rel="noopener"&gt;agno#8880&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:12" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:13"&gt;&#10;&lt;p&gt;With the OpenAI Responses API, Agno keeps only the last reasoning item, so it cannot be replayed with the tool calls it belonged to. &lt;a href="https://github.com/agno-agi/agno/issues/9960" target="_blank" rel="noopener"&gt;agno#9960&lt;/a&gt;; partial fixes landed in &lt;a href="https://github.com/agno-agi/agno/pull/10075" target="_blank" rel="noopener"&gt;agno#10075&lt;/a&gt; and &lt;a href="https://github.com/agno-agi/agno/pull/10395" target="_blank" rel="noopener"&gt;agno#10395&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:13" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:14"&gt;&#10;&lt;p&gt;Unclassified &lt;code&gt;ModelProviderError&lt;/code&gt; failures default to HTTP 502, so a real network blip and a permanent error look the same. &lt;a href="https://github.com/agno-agi/agno/issues/8869" target="_blank" rel="noopener"&gt;agno#8869&lt;/a&gt;, partially addressed by &lt;a href="https://github.com/agno-agi/agno/pull/8870" target="_blank" rel="noopener"&gt;agno#8870&lt;/a&gt;.&amp;#160;&lt;a href="#fnref:14" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;li id="fn:15"&gt;&#10;&lt;p&gt;The latest stable release on &lt;a href="https://pypi.org/project/httpx/#history" target="_blank" rel="noopener"&gt;PyPI&lt;/a&gt; is 0.28.1, uploaded 2024-12-06. There have been 1.0 development pre-releases since, the latest in August 2026. The closing of issues and discussions was widely discussed in the Python community in March 2026.&amp;#160;&lt;a href="#fnref:15" class="footnote-backref" role="doc-backlink"&gt;&amp;#x21a9;&amp;#xfe0e;&lt;/a&gt;&lt;/p&gt;&#10;&lt;/li&gt;&#10;&lt;/ol&gt;&#10;&lt;/div&gt;&#10;</description></item><item><title>Confessions of an asymmetric hypocrite: on Python dependencies</title><link>https://www.nijho.lt/post/dependencies/</link><pubDate>Sun, 27 Apr 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/dependencies/</guid><description>&lt;h2 id="introduction-the-dependency-dilemma-"&gt;Introduction: The Dependency Dilemma 🤔 &lt;a class="anchor" href="#introduction-the-dependency-dilemma-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;I just listened to a podcast &amp;ldquo;&lt;a href="https://www.youtube.com/watch?v=5ZyzeeYZgeM" target="_blank" rel="noopener"&gt;Build software that lasts!&lt;/a&gt;&amp;rdquo; with Bert Hubert (a fellow Dutch), who prefers to reduce the number of dependencies to an absolute minimum (also see his related blog posts [&lt;a href="https://berthub.eu/articles/posts/on-long-term-software-development/" target="_blank" rel="noopener"&gt;1&lt;/a&gt;] and [&lt;a href="https://berthub.eu/articles/posts/a-2024-plea-for-lean-software/" target="_blank" rel="noopener"&gt;2&lt;/a&gt;]).&#10;While I agree one shouldn&amp;rsquo;t use &lt;a href="https://www.npmjs.com/package/is-even" target="_blank" rel="noopener"&gt;&lt;code&gt;is-even&lt;/code&gt;&lt;/a&gt; as a dependency (&lt;a href="https://www.npmjs.com/package/is-odd" target="_blank" rel="noopener"&gt;&lt;code&gt;is-odd&lt;/code&gt;&lt;/a&gt; is a better choice 😏), I think there is a lot of value in reducing the number of dependencies but &lt;em&gt;also&lt;/em&gt; in using the right tool for the job and not continuously re-inventing the wheel.&#10;The podcast got me thinking about my own stance on dependencies and how I actively avoid introducing dependencies into my libraries while expecting (hoping 🤞) others to depend on my work.&lt;/p&gt;&#10;&lt;p&gt;Like many software developers, I&amp;rsquo;ve spent countless hours wrestling with dependencies.&#10;Dependency hell is easy to fall into, &lt;em&gt;especially&lt;/em&gt; in dynamic languages like Python.&#10;Those moments where installing or updating one package breaks another, or where a tiny library pulls in hundreds of transitive dependencies, are frustratingly common.&#10;This frustration partly motivated me to create &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;&lt;code&gt;unidep&lt;/code&gt;&lt;/a&gt;, a tool aimed at simplifying dependency management across different Python tools like &lt;code&gt;pip&lt;/code&gt; and &lt;code&gt;conda&lt;/code&gt;.&lt;/p&gt;&#10;&lt;p&gt;But working on &lt;code&gt;unidep&lt;/code&gt; and my other projects forces me to think about my own relationship with dependencies.&#10;I&amp;rsquo;ve realized I operate with what feels like an &amp;ldquo;asymmetric hypocrisy.&amp;rdquo;&#10;My standards for the libraries I &lt;em&gt;publish&lt;/em&gt; are extremely strict regarding which external dependencies I &lt;em&gt;consume&lt;/em&gt;.&#10;Yet, as an author of over 30 Python packages, I inherently &lt;em&gt;produce&lt;/em&gt; dependencies, hoping others will adopt my work.&#10;And then, when building applications or CLIs, my caution often disappears entirely.&lt;/p&gt;&#10;&lt;p&gt;This post dives into that asymmetry.&#10;It&amp;rsquo;s about the different hats I wear: the minimalist library author (as a dependency &lt;em&gt;consumer&lt;/em&gt;), the hopeful library author (as a dependency &lt;em&gt;producer&lt;/em&gt;), and the pragmatic application developer.&#10;In this post, I&amp;rsquo;ll explain why I take these different approaches and why it&amp;rsquo;s really about the situation, not a contradiction.&lt;/p&gt;&#10;&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#introduction-the-dependency-dilemma-"&gt;Introduction: The Dependency Dilemma 🤔&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#1-the-library-author-hat--as-dependency-consumer-maximum-caution"&gt;1. The Library Author Hat 🎩 (as Dependency Consumer): Maximum Caution&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-the-library-author-hat--as-dependency-producer-the-paradox"&gt;2. The Library Author Hat 🎩 (as Dependency Producer): The Paradox&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-the-application-developer--contributor-hat--practicality-is-key"&gt;3. The Application Developer / Contributor Hat 🧢: Practicality is Key&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-conclusion-not-hypocrisy-but-context-driven-strategy-"&gt;4. Conclusion: Not Hypocrisy, but Context-Driven Strategy 🙏&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;aside class="callout callout-note"&gt;&lt;svg class="ico" viewBox="0 0 256 256" aria-hidden="true"&gt;&lt;path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"/&gt;&lt;/svg&gt;&lt;div&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I&amp;rsquo;m extremely strict about adding external dependencies to my &lt;em&gt;libraries&lt;/em&gt;, preferring widely-used, low-dependency packages.&#10;Simultaneously, I &lt;em&gt;create&lt;/em&gt; libraries expecting adoption.&#10;For &lt;em&gt;applications&lt;/em&gt; (especially CLIs in isolated environments), I&amp;rsquo;m far more liberal with dependencies to build features quickly.&#10;It&amp;rsquo;s all about managing risks depending on the situation.&lt;/div&gt;&lt;/aside&gt;&#10;&lt;h2 id="1-the-library-author-hat--as-dependency-consumer-maximum-caution"&gt;1. The Library Author Hat 🎩 (as Dependency Consumer): Maximum Caution &lt;a class="anchor" href="#1-the-library-author-hat--as-dependency-consumer-maximum-caution" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When I build and publish an open-source Python library – like &lt;a href="https://github.com/python-adaptive/adaptive" target="_blank" rel="noopener"&gt;&lt;code&gt;Adaptive&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt;&lt;/a&gt;, or &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;&lt;code&gt;unidep&lt;/code&gt;&lt;/a&gt; – my primary focus shifts to the &lt;em&gt;users&lt;/em&gt; who will depend on my code.&#10;My choices directly impact their projects.&#10;This responsibility means I have to be very careful about adding external dependencies:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Minimize and Scrutinize:&lt;/strong&gt; Fewer dependencies mean fewer potential conflicts for users.&#10;I aggressively minimize the core runtime dependency list.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Favor Battle-Tested Dependencies:&lt;/strong&gt; I strongly prefer dependencies that are already widely adopted and trusted by the community.&#10;(I have been called that person that always wants the latest and greatest, the truth is that I am a sucker for shiny new things but typically only adopt them once they are already quite popular.)&#10;Think &lt;code&gt;numpy&lt;/code&gt; or &lt;code&gt;cloudpickle&lt;/code&gt;.&#10;These are less likely to cause unexpected issues or disappear overnight.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Low Transitive Dependency Count:&lt;/strong&gt; A key criterion is how many &lt;em&gt;other&lt;/em&gt; packages a potential dependency pulls in.&#10;Ideally, I choose dependencies with zero or very few further dependencies.&#10;For example, in &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt;&lt;/a&gt;, dependencies like &lt;a href="https://numpy.org/" target="_blank" rel="noopener"&gt;&lt;code&gt;numpy&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://networkx.org/" target="_blank" rel="noopener"&gt;&lt;code&gt;networkx&lt;/code&gt;&lt;/a&gt;, and &lt;a href="https://github.com/cloudpipe/cloudpickle" target="_blank" rel="noopener"&gt;&lt;code&gt;cloudpickle&lt;/code&gt;&lt;/a&gt; are good examples – they are popular and don&amp;rsquo;t rely on many other things.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Optional Dependencies for Heavy Features:&lt;/strong&gt; If a feature really needs a large or less common dependency, I try making it a &lt;em&gt;optional&lt;/em&gt; extra (&lt;code&gt;project.optional-dependencies&lt;/code&gt;).&#10;Users opt-in only if they need that specific capability.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Avoid Pinning (Mostly):&lt;/strong&gt; As mentioned before, hard version pins (&lt;code&gt;==1.2.3&lt;/code&gt;) in libraries are usually harmful.&#10;Minimum versions (&lt;code&gt;&amp;gt;=1.2&lt;/code&gt;) or safe upper bounds (&lt;code&gt;&amp;lt;2.0&lt;/code&gt;) are preferred.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;License Awareness:&lt;/strong&gt; The license of a dependency matters immensely, especially regarding compatibility and obligations (&lt;a href="https://www.nijho.lt/post/gpl/"&gt;GPL considerations&lt;/a&gt;).&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;The guiding principle is: &lt;strong&gt;Do not cause pain for your users.&lt;/strong&gt;&#10;A library should integrate smoothly into diverse environments.&lt;/p&gt;&#10;&lt;h2 id="2-the-library-author-hat--as-dependency-producer-the-paradox"&gt;2. The Library Author Hat 🎩 (as Dependency Producer): The Paradox &lt;a class="anchor" href="#2-the-library-author-hat--as-dependency-producer-the-paradox" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;Here&amp;rsquo;s where the &amp;ldquo;hypocrisy&amp;rdquo; might seem to surface.&#10;While I&amp;rsquo;m cautious about &lt;em&gt;consuming&lt;/em&gt; dependencies for my libraries, I am also the author of over 30 packages.&#10;I invest time and effort into these tools, naturally hoping they prove useful and that others &lt;em&gt;will adopt them&lt;/em&gt; – making &lt;em&gt;my&lt;/em&gt; libraries dependencies in &lt;em&gt;their&lt;/em&gt; projects.&lt;/p&gt;&#10;&lt;p&gt;Is it hypocritical to be wary of adding dependencies while simultaneously creating them for others?&#10;Perhaps slightly, but it&amp;rsquo;s also the nature of contributing to an ecosystem.&#10;I try to handle this by strictly following the rules from Section 1 for my own libraries.&#10;If I expect others to rely on my code, I feel responsible to make that dependency as&#10;low-risk and stable as possible.&#10;I want my libraries to be the kind of well-behaved, minimal-friction dependencies&#10;that I myself would be comfortable adding.&lt;/p&gt;&#10;&lt;h2 id="3-the-application-developer--contributor-hat--practicality-is-key"&gt;3. The Application Developer / Contributor Hat 🧢: Practicality is Key &lt;a class="anchor" href="#3-the-application-developer--contributor-hat--practicality-is-key" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;When the context shifts to building an application – like a command-line tool, a web service, or a personal project – my rules become &lt;em&gt;much&lt;/em&gt; less strict.&#10;The key differences are isolation (keeping things separate) and how far the effects reach.&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Isolation Reduces Risk:&lt;/strong&gt; Applications often run in controlled, isolated environments.&#10;&lt;a href="https://www.docker.com/" target="_blank" rel="noopener"&gt;&lt;code&gt;Docker&lt;/code&gt;&lt;/a&gt;, &lt;code&gt;conda&lt;/code&gt;/&lt;a href="https://mamba.readthedocs.io/en/latest/user_guide/micromamba.html" target="_blank" rel="noopener"&gt;&lt;code&gt;micromamba&lt;/code&gt;&lt;/a&gt; (&lt;a href="https://www.nijho.lt/post/python-environments/"&gt;Python Environment Jungle post&lt;/a&gt;), or simple &lt;code&gt;venv&lt;/code&gt;s contain the dependencies.&#10;A conflict within the application&amp;rsquo;s environment doesn&amp;rsquo;t break the user&amp;rsquo;s entire system or other unrelated projects.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;CLIs as a Prime Example:&lt;/strong&gt; Many tools I&amp;rsquo;ve built are CLIs (like &lt;a href="https://github.com/basnijholt/tuitorial" target="_blank" rel="noopener"&gt;&lt;code&gt;tuitorial&lt;/code&gt;&lt;/a&gt; or helpers within my &lt;a href="https://github.com/basnijholt/dotfiles" target="_blank" rel="noopener"&gt;&lt;code&gt;dotfiles&lt;/code&gt;&lt;/a&gt;).&#10;These might have tens of dependencies.&#10;Because they are installed into isolated environments (often via &lt;a href="https://docs.astral.sh/uv/" target="_blank" rel="noopener"&gt;&lt;code&gt;uv&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://pipx.pypa.io/stable/" target="_blank" rel="noopener"&gt;&lt;code&gt;pipx&lt;/code&gt;&lt;/a&gt;, or within a dedicated &lt;code&gt;conda&lt;/code&gt; env), this complexity is okay.&#10;The risk is contained.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Developer Tools Welcome:&lt;/strong&gt; Productivity boosters like &lt;a href="https://beta.ruff.rs/docs/rules/" target="_blank" rel="noopener"&gt;&lt;code&gt;ruff&lt;/code&gt;&lt;/a&gt;, &lt;a href="https://mypy.readthedocs.io/en/stable/" target="_blank" rel="noopener"&gt;&lt;code&gt;mypy&lt;/code&gt;&lt;/a&gt;, etc., are fair game as development dependencies (&lt;a href="https://www.nijho.lt/post/best-python-dev-tooling/"&gt;My Python Project Toolkit&lt;/a&gt;).&#10;They don&amp;rsquo;t impact the end-user runtime (if packaged properly), so I am much more lenient with dev tools.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Focus on Functionality:&lt;/strong&gt; The goal is often to deliver features or solve a problem efficiently.&#10;Leveraging existing libraries, even those with dependencies, is usually faster and more robust than reimplementing complex logic.&#10;It usually makes more sense to use existing libraries.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;In this context, the focus changes from &lt;em&gt;minimizing external impact&lt;/em&gt; (libraries) to &lt;em&gt;maximizing internal capability and maintainability&lt;/em&gt; within a controlled scope (applications).&lt;/p&gt;&#10;&lt;h2 id="4-conclusion-not-hypocrisy-but-context-driven-strategy-"&gt;4. Conclusion: Not Hypocrisy, but Context-Driven Strategy 🙏 &lt;a class="anchor" href="#4-conclusion-not-hypocrisy-but-context-driven-strategy-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;My approach to dependencies &lt;em&gt;is&lt;/em&gt; &amp;ldquo;it depends on the situation&amp;rdquo;.&#10;I&amp;rsquo;m stricter about dependencies for my libraries than for my applications or personal tools.&#10;I am cautious about adopting dependencies while actively producing them for others.&lt;/p&gt;&#10;&lt;p&gt;But I don&amp;rsquo;t see this as hypocrisy.&#10;It&amp;rsquo;s a practical approach based on different situations and goals:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Libraries (as Consumer):&lt;/strong&gt; Focus on keeping the software community stable and making things easy for users. Be conservative.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Libraries (as Producer):&lt;/strong&gt; Create software responsibly, following the same high standards you expect from others.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Applications:&lt;/strong&gt; Focus on adding features and developing quickly within separate environments. Be pragmatic.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Understanding the different risks and goals for each situation helps make good decisions about dependencies.&#10;It&amp;rsquo;s about choosing the right trade-offs for the specific situation, whether you&amp;rsquo;re building a foundational block for the community or a specific tool for a controlled environment.&lt;/p&gt;&#10;&lt;p&gt;How do you handle dependencies across different types of projects?&#10;Do you find yourself navigating similar questions?&#10;Share your thoughts!&lt;/p&gt;&#10;</description></item><item><title>Functions over classes? Why I prefer a simpler, functional style in Python 🐍</title><link>https://www.nijho.lt/post/functional-python/</link><pubDate>Mon, 14 Apr 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/functional-python/</guid><description>&lt;p&gt;Over years of building software, from scientific simulation tools like &lt;a href="https://github.com/python-adaptive/adaptive" target="_blank" rel="noopener"&gt;&lt;code&gt;Adaptive&lt;/code&gt;&lt;/a&gt; and &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt;&lt;/a&gt; to package management packages like &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;&lt;code&gt;unidep&lt;/code&gt;&lt;/a&gt;, I&amp;rsquo;ve found myself preferring a certain coding style: using small, focused functions more than large classes with complex internal state and deep inheritance hierarchies.&#10;This isn&amp;rsquo;t about dogma, but about a pragmatic approach that, in my experience, leads to code that is easier to understand, test, and maintain.&lt;/p&gt;&#10;&lt;p&gt;This post outlines why I lean towards this functional-inspired style in my Python projects, while still recognizing where classes are the right tool.&lt;/p&gt;&#10;&lt;details class="toc" open&gt;&lt;summary&gt;Table of contents&lt;/summary&gt;&lt;nav id="TableOfContents"&gt;&#10; &lt;ul&gt;&#10; &lt;li&gt;&lt;a href="#1-taming-complexity-avoiding-the-over-engineering-trap-"&gt;1. Taming Complexity: Avoiding the Over-Engineering Trap 🧠&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#2-the-power-of-small-focused-functions-"&gt;2. The Power of Small, Focused Functions 🎯&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#3-keeping-classes-simple--using-them-where-they-fit-"&gt;3. Keeping Classes Simple &amp;amp; Using Them Where They Fit 🛠️&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#4-the-testability-advantage-"&gt;4. The Testability Advantage ✅&lt;/a&gt;&lt;/li&gt;&#10; &lt;li&gt;&lt;a href="#5-its-about-pragmatism-not-dogma-"&gt;5. It&amp;rsquo;s About Pragmatism, Not Dogma 🙏&lt;/a&gt;&lt;/li&gt;&#10; &lt;/ul&gt;&#10;&lt;/nav&gt;&lt;/details&gt;&#10;&#10;&lt;h2 id="1-taming-complexity-avoiding-the-over-engineering-trap-"&gt;1. Taming Complexity: Avoiding the Over-Engineering Trap 🧠 &lt;a class="anchor" href="#1-taming-complexity-avoiding-the-over-engineering-trap-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;One of the biggest challenges in software development is managing complexity.&#10;As systems grow, keeping track of how different parts interact and understanding the internal state of objects can become a lot to keep track of mentally (high cognitive load).&lt;/p&gt;&#10;&lt;p&gt;I&amp;rsquo;ve often observed a pattern, especially when developers first start using object-oriented programming: the tendency to use classes &lt;em&gt;everywhere&lt;/em&gt;.&#10;This can lead to over-engineering – introducing many layers of inheritance, abstract base classes with only a single implementation, and generally creating complexity when simplicity would suffice.&#10;Thinking you&amp;rsquo;re being clever by building complex class structures can sometimes just be a form of overgeneralization that makes the code harder, not easier, to follow.&lt;/p&gt;&#10;&lt;p&gt;Classes, particularly those with many methods modifying internal attributes (&lt;code&gt;self.x&lt;/code&gt;, &lt;code&gt;self.y&lt;/code&gt;, etc.), require you to constantly hold their state in your head.&#10;When you call a method &lt;code&gt;object.do_something()&lt;/code&gt;, the outcome might depend not just on the arguments you pass, but also on the object&amp;rsquo;s history – what methods were called before, and in what order?&#10;This hidden state can make reasoning about the code&amp;rsquo;s behavior difficult and debugging tricky.&lt;/p&gt;&#10;&lt;p&gt;Functions, particularly when kept small and focused, tend to be more explicit.&#10;Ideally, a function takes inputs and produces outputs, with minimal side effects (or ideally, none - making them pure functions).&#10;Its behavior is primarily determined by its arguments.&#10;This makes the code flow easier to follow: data comes in, transformation happens, data goes out.&#10;There&amp;rsquo;s less hidden context to juggle mentally.&lt;/p&gt;&#10;&lt;h2 id="2-the-power-of-small-focused-functions-"&gt;2. The Power of Small, Focused Functions 🎯 &lt;a class="anchor" href="#2-the-power-of-small-focused-functions-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This leads directly to the preference for small functions, each doing one thing well (adhering to the Single Responsibility Principle).&#10;When functions are concise and have a clear purpose:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;&lt;strong&gt;Readability Improves:&lt;/strong&gt; It&amp;rsquo;s easier to understand what a small, well-named function does quickly.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Reusability Increases:&lt;/strong&gt; Small, focused functions are more likely to be reusable in different parts of the codebase or even in other projects.&lt;/li&gt;&#10;&lt;li&gt;&lt;strong&gt;Refactoring is Simpler:&lt;/strong&gt; Modifying or replacing a small unit of logic is less risky than altering a large, complex method within a class.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;This philosophy is something I actively apply.&#10;Looking at the codebase for &lt;a href="https://github.com/basnijholt/unidep" target="_blank" rel="noopener"&gt;&lt;code&gt;unidep&lt;/code&gt;&lt;/a&gt;, for example, you&amp;rsquo;ll find many utility functions in modules like &lt;code&gt;_dependencies_parsing.py&lt;/code&gt; or &lt;code&gt;_conda_env.py&lt;/code&gt;, each handling a specific, well-defined task.&lt;/p&gt;&#10;&lt;h2 id="3-keeping-classes-simple--using-them-where-they-fit-"&gt;3. Keeping Classes Simple &amp;amp; Using Them Where They Fit 🛠️ &lt;a class="anchor" href="#3-keeping-classes-simple--using-them-where-they-fit-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;So, where do classes fit in this picture?&lt;/p&gt;&#10;&lt;p&gt;Firstly, as &lt;strong&gt;simple data structures&lt;/strong&gt; – think &lt;code&gt;NamedTuple&lt;/code&gt;s or &lt;code&gt;dataclasses&lt;/code&gt; (preferably with &lt;code&gt;frozen=True&lt;/code&gt;).&#10;Their main role is to group related data attributes together, providing a clear structure for passing information around.&#10;They hold data, but don&amp;rsquo;t typically manage complex state transitions.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Example of a simple data class from unidep&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;dataclasses&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pathlib&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Path&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="c1"&gt;# Use frozen=True for immutability&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;which&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;conda&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;#34;pip&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="n"&gt;pin&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ... other simple attributes ...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Used to pass structured data, not to manage complex state&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;process_spec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spec&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Spec&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;Path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="c1"&gt;# ... function logic using spec data ...&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt; &lt;span class="k"&gt;pass&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Secondly, and importantly, classes are excellent for &lt;strong&gt;defining user-facing APIs&lt;/strong&gt;.&#10;For the end-user of a library, having a single object with easily discoverable methods (&lt;code&gt;object.&amp;lt;tab&amp;gt;&lt;/code&gt;) is often a great user experience.&lt;/p&gt;&#10;&lt;p&gt;A good example from my own work is the &lt;a href="https://github.com/pipefunc/pipefunc" target="_blank" rel="noopener"&gt;&lt;code&gt;pipefunc&lt;/code&gt;&lt;/a&gt; package.&#10;It provides two main classes, &lt;code&gt;Pipeline&lt;/code&gt; and &lt;code&gt;PipeFunc&lt;/code&gt;, which encapsulate the core functionality.&#10;Users interact primarily with these objects and their methods.&lt;/p&gt;&#10;&lt;div class="highlight"&gt;&lt;pre tabindex="0" class="chroma"&gt;&lt;code class="language-python" data-lang="python"&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="c1"&gt;# Simplified example from pipefunc&amp;#39;s user perspective&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="nn"&gt;pipefunc&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Pipeline&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Pipeline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;some_function&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;span class="line"&gt;&lt;span class="cl"&gt;&lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pipeline&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;#34;output_node&amp;#34;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&#10;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;However, &lt;strong&gt;inside&lt;/strong&gt; the methods of these API classes (&lt;code&gt;Pipeline.run&lt;/code&gt;, &lt;code&gt;Pipeline.add&lt;/code&gt;, etc.), the actual logic is often delegated to smaller, internal functions.&#10;The class acts as a clean interface or facade, orchestrating calls to these more focused, functional units.&#10;This gives the user a nice API while keeping the internal implementation modular and testable.&lt;/p&gt;&#10;&lt;p&gt;I also actively avoid deep inheritance hierarchies.&#10;Composition (having objects contain other objects) often provides a more flexible and explicit way to combine functionality without the tight coupling and complexity inheritance can introduce.&lt;/p&gt;&#10;&lt;h2 id="4-the-testability-advantage-"&gt;4. The Testability Advantage ✅ &lt;a class="anchor" href="#4-the-testability-advantage-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;One of the most significant practical benefits of prioritizing functions and simple classes is testability.&#10;Small, pure (or mostly pure) functions are incredibly easy to test:&lt;/p&gt;&#10;&lt;ul&gt;&#10;&lt;li&gt;Provide known inputs.&lt;/li&gt;&#10;&lt;li&gt;Assert expected outputs.&lt;/li&gt;&#10;&lt;li&gt;Minimal setup or mocking is required because there&amp;rsquo;s no complex internal state to manage.&lt;/li&gt;&#10;&lt;/ul&gt;&#10;&lt;p&gt;Even when testing the methods of the API classes (like &lt;code&gt;Pipeline&lt;/code&gt; in &lt;code&gt;pipefunc&lt;/code&gt;), the fact that they often delegate to well-tested internal functions simplifies the process.&#10;You focus on testing the orchestration and interaction logic at the class level, knowing the underlying functional units are already verified.&lt;/p&gt;&#10;&lt;p&gt;Testing classes with intricate internal state inherently involves more setup (instantiating the object, getting it into the right state before calling the method under test) and potentially more complex assertions to verify state changes.&#10;Aiming for more functional units significantly simplifies the testing process.&lt;/p&gt;&#10;&lt;h2 id="5-its-about-pragmatism-not-dogma-"&gt;5. It&amp;rsquo;s About Pragmatism, Not Dogma 🙏 &lt;a class="anchor" href="#5-its-about-pragmatism-not-dogma-" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;This isn&amp;rsquo;t a wholesale rejection of object-oriented programming.&#10;It&amp;rsquo;s about choosing the right tool for the job and prioritizing clarity, testability, and maintainability.&#10;Classes are fantastic tools, especially for building clear APIs and structuring data.&#10;Deep inheritance and complex state management, however, should be used carefully, not just because the language allows it.&lt;/p&gt;&#10;&lt;p&gt;For much of the work I do – building libraries, tools, and performing data analysis or simulations – I&amp;rsquo;ve found that prioritizing functions, keeping API classes focused, minimizing internal state, and avoiding deep inheritance leads to code that &lt;em&gt;I&lt;/em&gt; find easier to write, understand, debug, and maintain in the long run.&#10;The reduction in cognitive load and the ease of testing are strong points for how I work.&lt;/p&gt;&#10;&lt;p&gt;If you often find yourself struggling with complex state management or tangled inheritance trees, perhaps consider if a simpler, more functional internal structure, potentially fronted by a clean class-based API, might bring some clarity and simplicity to your projects too.&lt;/p&gt;&#10;</description></item><item><title>Vibe coding worked—until the 15‑hour cleanup</title><link>https://www.nijho.lt/post/vibe-coding/</link><pubDate>Fri, 21 Mar 2025 00:00:00 +0000</pubDate><guid isPermaLink="false">/post/vibe-coding/</guid><description>&lt;h2 id="i-tried-vibe-coding-for-3-hours-my-application-worked-but-then-i-spent-15-hours-fixing-it"&gt;I tried &amp;ldquo;vibe coding&amp;rdquo; for 3 hours, my application worked, but then I spent 15 hours fixing it &lt;a class="anchor" href="#i-tried-vibe-coding-for-3-hours-my-application-worked-but-then-i-spent-15-hours-fixing-it" aria-label="Link to this section"&gt;#&lt;/a&gt;&lt;/h2&gt;&#10;&lt;p&gt;That&amp;rsquo;s not an exaggeration—it&amp;rsquo;s really what happened.&lt;/p&gt;&#10;&lt;p&gt;Recently, I tried &amp;ldquo;&lt;a href="https://en.wikipedia.org/wiki/Vibe_coding" target="_blank" rel="noopener"&gt;Vibe coding&lt;/a&gt;,&amp;rdquo; a term coined by Andrej Karpathy, a prominent AI researcher, co-founder of OpenAI, and former AI lead at Tesla.&#10;In &lt;a href="https://x.com/karpathy/status/1886192184808149383" target="_blank" rel="noopener"&gt;his viral tweet&lt;/a&gt; from February 2025, Karpathy described vibe coding as a programming approach where you &amp;ldquo;fully give in to the vibes, embrace exponentials, and forget that the code even exists.&amp;rdquo;&#10;It&amp;rsquo;s essentially about letting AI handle the coding based on natural language prompts without closely reviewing the generated code.&lt;/p&gt;&#10;&lt;p&gt;Intrigued by the hype, I decided to experiment with vibe coding using &lt;a href="https://www.cursor.com/" target="_blank" rel="noopener"&gt;Cursor&lt;/a&gt; on a new Python project I called &lt;a href="https://github.com/basnijholt/dotbins" target="_blank" rel="noopener"&gt;dotbins&lt;/a&gt; (&lt;a href="../dotbins"&gt;check out my detailed write-up here&lt;/a&gt;).&amp;quot;&#10;&lt;a href="https://github.com/basnijholt/dotbins" target="_blank" rel="noopener"&gt;Dotbins&lt;/a&gt; manages CLI tool binaries within Git repositories, automatically downloading the correct binary from GitHub for the user&amp;rsquo;s platform and architecture, significantly simplifying my dotfiles and bootstrapping process.&#10;I am super excited about this project but it is not what this post is about.&lt;/p&gt;&#10;&lt;p&gt;Initially, it was fun to get something working so quickly.&#10;Within just three hours, I had a functioning prototype—without carefully examining the code.&#10;But I had unknowingly created a monster—a true &lt;a href="https://en.wikipedia.org/wiki/Spaghetti_code" target="_blank" rel="noopener"&gt;spaghetti&lt;/a&gt; monster! 🍝&lt;/p&gt;&#10;&lt;p&gt;Once I started reviewing the AI-generated code, I realized it was totally incomprehensible.&#10;The AI had implemented the most complicated and roundabout ways to achieve simple tasks.&#10;It introduced redundant checks for impossible conditions, irrelevant error handling, and execution paths that would never trigger.&#10;The code worked superficially, but beneath the surface, it was chaotic and nearly impossible to understand or maintain.&lt;/p&gt;&#10;&lt;p&gt;This realization turned three hours of fun into fifteen hours of meticulous debugging, refactoring, and cleaning up the mess.&#10;Essentially, I had to rewrite the entire project.&lt;/p&gt;&#10;&lt;p&gt;My main takeaway: AI-assisted coding, or &amp;ldquo;vibe coding,&amp;rdquo; is incredibly powerful for quick prototyping but dangerous when used without oversight.&#10;According to &lt;a href="https://www.youtube.com/watch?v=IACHfKmZMr8" target="_blank" rel="noopener"&gt;this recent Y Combinator discussion&lt;/a&gt;, many startup founders are building products almost entirely with AI-generated code, &amp;ldquo;vibing&amp;rdquo; the code, meaning they let the AI write the code and don&amp;rsquo;t review it.&#10;For writing a couple of hundred lines of code it likely works pretty well, but for anything more complex, it&amp;rsquo;s a recipe for disaster.&#10;Personally, I would never invest in, build upon, or use such products—they&amp;rsquo;re essentially constructing technical prisons for themselves.&lt;/p&gt;&#10;&lt;p&gt;AI is fantastic for inspiration, quick prototyping, writing code, and refactoring—but letting it run wild without careful human review can quickly lead to severe technical debt and hidden bugs.&lt;/p&gt;&#10;&lt;p&gt;Thanks for reading!&lt;/p&gt;&#10;</description></item></channel></rss>