<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>Michael Kennedy&#39;s Thoughts on Technology</title>
        <link>https://mkennedy.codes/posts/</link>
        <description>Essays on Python, technology, programming and more from Michael Kennedy.</description>
        <generator>Hugo -- gohugo.io</generator>
        <language>en-us</language>
        <managingEditor>michael@mkennedy.tech (Michael Kennedy)</managingEditor>
        <webMaster>michael@mkennedy.tech (Michael Kennedy)</webMaster>
        <lastBuildDate>Wed, 01 Apr 2026 08:52:18 -0700</lastBuildDate><atom:link href="https://mkennedy.codes/posts/index.xml" rel="self" type="application/rss+xml" />
        
        <item>
            <title>Cutting Python Web App Memory Over 31%</title>
            <link>https://mkennedy.codes/posts/cutting-python-web-app-memory-over-31-percent/</link>
            <pubDate>Wed, 01 Apr 2026 08:52:18 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/cutting-python-web-app-memory-over-31-percent/</guid>
            <description>&lt;p&gt;&lt;strong&gt;tl;dr;&lt;/strong&gt; I cut 3.2 GB of memory usage from our Python web apps using five techniques: async workers, import isolation, the Raw+DC database pattern, local imports for heavy libraries, and disk-based caching. Here are the exact before-and-after numbers for each optimization.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Over the past few weeks, I&amp;rsquo;ve been ruthlessly focused on reducing memory usage on my web apps, APIs, and daemons. I&amp;rsquo;ve been following the &lt;a href=&#34;https://talkpython.fm/books/python-in-production/chapter-4-one-big-server-rather-than-many-small-ones&#34;&gt;one big server pattern&lt;/a&gt; for deploying all the Talk Python web apps, APIs, background services, and supporting infrastructure.&lt;/p&gt;
&lt;p&gt;There are a ridiculous number of containers running to make everything go around here at Talk Python (23 apps, APIs, and database servers in total).&lt;/p&gt;
&lt;p&gt;Even with that many apps running, the actual server CPU load is quite low. But memory usage is creeping up. The server was running at 65% memory usage on a 16GB server. While that may be fine - &lt;a href=&#34;https://talkpython.fm/blog/posts/we-have-moved-to-hetzner/&#34;&gt;the server&amp;rsquo;s not that expensive&lt;/a&gt; - I decided to take some time and see if there were some code level optimizations available.&lt;/p&gt;
&lt;p&gt;What I learned was interesting and much of it was a surprise to me. So, I thought I&amp;rsquo;d share it here with you. I was able to drop the memory usage by 3.2GB  basically for free just by changing some settings, changing how I import packages in Python, and proper use of offloading some caching to disk.&lt;/p&gt;
&lt;h2 id=&#34;how-much-memory-were-the-python-apps-using-before-optimization&#34;&gt;How much memory were the Python apps using before optimization?&lt;/h2&gt;
&lt;p&gt;For this blog post, I&amp;rsquo;m going to focus on just two applications. However, I applied this to most of the apps that we own the source code for (as opposed to Umami, etc). Take these as concrete examples more than the entire use case.&lt;/p&gt;
&lt;p&gt;Here are the initial stats we&amp;rsquo;ll be improving on along the way.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Application&lt;/th&gt;
          &lt;th&gt;Starting Memory&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://training.talkpython.fm/courses/all&#34;&gt;Talk Python Training&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;1,280 MB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://training.talkpython.fm/search/all/memory-optimization&#34;&gt;Training Search Indexer Daemon&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;708 MB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;1,988 MB&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;how-async-workers-and-quart-cut-python-web-app-memory-in-half&#34;&gt;How async workers and Quart cut Python web app memory in half&lt;/h2&gt;
&lt;p&gt;I knew that starting with a core architectural change in how we run our apps and access our database would have huge implications. You see, we&amp;rsquo;re running our web apps as a web garden, one orchestrator, multiple worker processes via &lt;a href=&#34;https://github.com/emmett-framework/granian&#34;&gt;the lovely Granian&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve wanted to migrate our remaining web applications to some fully asynchronous application framework. See &lt;a href=&#34;https://talkpython.fm/blog/posts/talk-python-rewritten-in-quart-async-flask/&#34;&gt;Talk Python rewritten in Quart (async Flask)&lt;/a&gt; for a detailed discussion on this topic. If we have a truly async-capable application server (Granian) and a truly async web framework (Quart), then we can change our deployment style to one worker running fully asynchronous code. Much less blocking code means a single worker is more responsive now. Thus we can work with a single worker instance.&lt;/p&gt;
&lt;p&gt;This one change alone would cut the memory usage nearly in half. To facilitate this, we needed two actions:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Action 1: Rewrite Talk Python Training in Quart&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The first thing I had to do was rewrite Talk Python Training, the app I was mostly focused on at the time, in Quart. This was a lot of work. You might not know it from the outside, but Talk Python Training is a significant application.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;talk-python-training.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;178,000 lines of code! Rewriting this from the older framework, Pyramid, to async Flask (aka Quart), was a lot of work, but I pulled it off last week.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Action 2: Rewrite data access to raw + dc design pattern&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Data access was based on MongoEngine, a barely maintained older database ODM for talking to MongoDB, which does not support async code and never will support async code. Even though we have Quart as a runtime option, we hardly can do anything async without the data access layer.&lt;/p&gt;
&lt;p&gt;So I spent some time removing MongoEngine and implementing the Raw + DC design pattern. That saved us a ton of memory, facilitated writing async queries, and almost doubled our requests per second.&lt;/p&gt;
&lt;p&gt;I actually wrote this up in isolation here with some nice graphs: &lt;a href=&#34;https://mkennedy.codes/posts/raw-dc-a-retrospective/&#34;&gt;Raw+DC Database Pattern: A Retrospective&lt;/a&gt;. Switching from a formalized ODM to raw database queries along with data classes with slots &lt;strong&gt;saved us 100 MB per worker process&lt;/strong&gt;, or in this case, 200 MB of working memory. Given that it also sped up the app significantly, that&amp;rsquo;s a serious win.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Change&lt;/th&gt;
          &lt;th&gt;Memory Saved&lt;/th&gt;
          &lt;th&gt;Bonus&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Rewrite to Quart (async Flask)&lt;/td&gt;
          &lt;td&gt;Enabled single-worker mode&lt;/td&gt;
          &lt;td&gt;Async capable&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Raw + DC database pattern&lt;/td&gt;
          &lt;td&gt;200 MB (100 MB per worker)&lt;/td&gt;
          &lt;td&gt;Almost 2x requests/sec&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;how-switching-to-a-single-async-granian-worker-saved-542-mb&#34;&gt;How switching to a single async Granian worker saved 542 MB&lt;/h2&gt;
&lt;p&gt;Now that our web app runs asynchronously and our database queries fully support it, we could trim our web garden down to a single, fully asynchronous worker process using Granian. When every request is run in a blocking mode, one worker not ideal. But now the requests all interleave using Python concurrency.&lt;/p&gt;
&lt;p&gt;This brought things down to a whopping 536 MB in total (&lt;strong&gt;a savings of 542 MB&lt;/strong&gt;!) I could have stopped there, and things would have been excellent compared to where we were before, but I wanted to see what else was a possibility.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Metric&lt;/th&gt;
          &lt;th&gt;Value&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Before&lt;/strong&gt; (multi-worker)&lt;/td&gt;
          &lt;td&gt;1,280 MB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;After&lt;/strong&gt; (single async worker and raw+dc)&lt;/td&gt;
          &lt;td&gt;536 MB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Savings&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;542 MB&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;how-isolating-python-imports-in-a-subprocess-cut-memory-from-708-mb-to-22-mb&#34;&gt;How isolating Python imports in a subprocess cut memory from 708 MB to 22 MB&lt;/h2&gt;
&lt;p&gt;The next biggest problem was that the Talk Python Training search indexer. It reads literally everything from the many gigabyte database backing Talk Python Training, indexes it, and stores it into a custom data structure that we use for our ultra-fast search. It was running at 708 MB in its own container.&lt;/p&gt;
&lt;p&gt;Surely, this could be more efficient.&lt;/p&gt;
&lt;p&gt;And boy, was it. There were two main takeaways here. I noticed first that even if no indexing ran, just at startup, this process was using almost 200 megabytes of memory. Why? Import chains.&lt;/p&gt;
&lt;p&gt;The short version is it was importing almost all of the files of Talk Python Training and their in third-party dependencies because that was just the easiest way to write the code and because of PEP 8. When the app starts, it imports a few utilities from Talk Python Training. That, in turn, pulls in the entire mega application plus all of the dependencies that the application itself is using, bloating the memory way, way up.&lt;/p&gt;
&lt;p&gt;All this little daemon needs to do is every few hours re-index the site. It sits there, does nothing in particular related to our app, loops around, waits for exit commands from Docker, and if enough time has elapsed, then it runs the search process with our code.&lt;/p&gt;
&lt;p&gt;We could move all of that search indexing code into a subprocess. And only that subprocess&amp;rsquo;s code actually imports anything of significance. When the search index has to run, that process kicks off for maybe 30 seconds, builds the index, uses a bunch of memory, but once the indexing is done, it shuts down and even the imports are unloaded.&lt;/p&gt;
&lt;p&gt;What was the change? Amazing. &lt;strong&gt;The search indexer went from 708 MB to just 22 MB&lt;/strong&gt;! All we had to do was isolate imports into its own separate file and then run that separately using a Python subprocess. That&amp;rsquo;s it, 32x less memory used.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Metric&lt;/th&gt;
          &lt;th&gt;Value&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Before&lt;/strong&gt; (monolithic process)&lt;/td&gt;
          &lt;td&gt;708 MB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;After&lt;/strong&gt; (subprocess isolation)&lt;/td&gt;
          &lt;td&gt;22 MB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Reduction&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;32x&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;how-much-memory-do-python-imports-like-boto3-pandas-and-matplotlib-use&#34;&gt;How much memory do Python imports like boto3, pandas, and matplotlib use?&lt;/h2&gt;
&lt;p&gt;When we write simple code such as &lt;code&gt;import boto3&lt;/code&gt; it looks like no big deal. You&amp;rsquo;re just telling Python you need to use this library. But as I hinted at above, what it actually does is load up that library in total, and any static data or singleton-style data is created, as well as transitive dependencies for that library.&lt;/p&gt;
&lt;p&gt;Unbeknownst to me, boto3 takes a ton of memory.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Import Statement&lt;/th&gt;
          &lt;th&gt;Memory Cost (3.14)&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;import boto3&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;25 MB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;import matplotlib&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;17 MB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;import pandas&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;44 MB&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Yet for our application, these are very rarely used. Maybe we need to upload a file to blob storage using boto3, or use matplotlib and pandas to generate some report that we rarely run.&lt;/p&gt;
&lt;p&gt;By moving these to be local imports, we are able to save a ton of memory. What do I mean by that? Simply don&amp;rsquo;t follow PEP 8 here - instead of putting these at the top of your file, put them inside of the functions that use them, and they will only be imported if those functions are called.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;generate_usage_report&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;matplotlib&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;kn&#34;&gt;import&lt;/span&gt; &lt;span class=&#34;nn&#34;&gt;pandas&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;  &lt;span class=&#34;c1&#34;&gt;# Write code with these libs...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Now eventually, this generate_usage_report function probably will get called, but that&amp;rsquo;s where you go back to DevOps. We can simply set a time-to-live on the worker process. Granian will gracefully shut down the worker process and start a new one every six hours or once a day or whatever you choose.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PEP 810 – Explicit lazy imports&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This makes me very excited for Python 3.15. That&amp;rsquo;s where &lt;a href=&#34;https://peps.python.org/pep-0810/&#34;&gt;the lazy imports feature&lt;/a&gt; will land. That &lt;em&gt;should&lt;/em&gt; make this behavior entirely automatic without the need to jump through hoops.&lt;/p&gt;
&lt;h2 id=&#34;how-moving-python-caches-to-diskcache-reduced-memory-usage&#34;&gt;How moving Python caches to diskcache reduced memory usage&lt;/h2&gt;
&lt;p&gt;Finally I addressed our caches. This was probably the smallest of the improvements, but still relevant. We had quite a few things that were small to medium-sized caches being kept in memory. For example, the site takes a fragment of markdown which is repeatedly used, and instead of regenerating it every time, we would stash the generated markdown and just return that from cache.&lt;/p&gt;
&lt;p&gt;We moved most of this caching to diskcache. If you want to hear me and Vincent nerd out on how powerful this little library is, listen to the Talk Python episode &lt;a href=&#34;https://talkpython.fm/episodes/show/534/diskcache-your-secret-python-perf-weapon&#34;&gt;diskcache: Your secret Python perf weapon&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;total-memory-savings-from-1988-mb-to-472-mb&#34;&gt;Total memory savings: from 1,988 MB to 472 MB&lt;/h2&gt;
&lt;p&gt;&lt;img src=&#34;memory-graph.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;So where are things today after applying these optimizations?&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Application&lt;/th&gt;
          &lt;th&gt;Before&lt;/th&gt;
          &lt;th&gt;After&lt;/th&gt;
          &lt;th&gt;Savings&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://training.talkpython.fm/courses/all&#34;&gt;Talk Python Training&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;1,280 MB&lt;/td&gt;
          &lt;td&gt;450 MB&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;1.8x&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;https://training.talkpython.fm/search/all/memory-optimization&#34;&gt;Training Search Indexer Daemon&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;708 MB&lt;/td&gt;
          &lt;td&gt;22 MB&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;32x&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;1,988 MB&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;472 MB&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;3.2x&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Applying these techniques and more to all of our web apps &lt;strong&gt;reduced our server load by 3.2 GB of memory&lt;/strong&gt;. Memory is often the most expensive and scarce resource in production servers. This is a huge win for us.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Raw&#43;DC Database Pattern: A Retrospective</title>
            <link>https://mkennedy.codes/posts/raw-dc-a-retrospective/</link>
            <pubDate>Mon, 30 Mar 2026 08:31:17 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/raw-dc-a-retrospective/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR;&lt;/strong&gt; After migrating three production Python web apps from MongoEngine to the Raw+DC database pattern, I measured nearly 2x the requests per second, 18% less memory, and gained native async support. Raw+DC delivered real-world performance gains, not just synthetic benchmarks.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;About a month ago, I wrote about a new design pattern I&amp;rsquo;m seeing gain traction in the software space: &lt;a href=&#34;https://mkennedy.codes/posts/raw-dc-the-orm-pattern-of-2026/&#34;&gt;Raw+DC: The ORM pattern of 2026&lt;/a&gt;. This article generated a lot of interest and a lot of debate. The short version: instead of using an ORM or ODM, you write raw database queries paired with Python dataclasses for type safety. This gives AI coding assistants a much larger training base to work from, reduces dependency risk, and delivers comparable or better performance.&lt;/p&gt;
&lt;h2 id=&#34;putting-rawdc-into-practice&#34;&gt;Putting Raw+DC into practice&lt;/h2&gt;
&lt;p&gt;Now that some time has passed and I&amp;rsquo;ve thought about it more, I&amp;rsquo;ve had a chance to migrate three of my most important web apps to Raw+DC: &lt;a href=&#34;https://talkpython.fm/?ref=mkennedycodes&#34;&gt;Talk Python the podcast&lt;/a&gt;, &lt;a href=&#34;https://training.talkpython.fm/courses/all?ref=mkennedycodes&#34;&gt;Talk Python Courses&lt;/a&gt;, and &lt;a href=&#34;https://pythonbytes.fm/?ref=mkennedycodes&#34;&gt;Python Bytes&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;So how did it go? From a pure functionality perspective, it went great. There were maybe one to three problems per web app. This might not sound great, and I didn&amp;rsquo;t love it, but given this is thousands and thousands of lines of code per app, that&amp;rsquo;s a small percentage of issues, given how many things went right.&lt;/p&gt;
&lt;p&gt;More importantly, I was able to remove a dependency on two faltering database libraries. &lt;a href=&#34;https://pypi.org/project/mongoengine/&#34;&gt;Mongoengine&lt;/a&gt;, the one that I&amp;rsquo;m going to pull numbers from for Talk Python Training below, has not had a meaningful release in years. It was one of the two core blockers that prevented me from using async programming patterns on the website entirely.&lt;/p&gt;
&lt;h2 id=&#34;how-much-faster-is-rawdc-than-mongoengine&#34;&gt;How much faster is Raw+DC than MongoEngine?&lt;/h2&gt;
&lt;p&gt;I said I imagined that we would save in memory and CPU costs, but did it actually pan out in a practical application? After all, we saw that Robyn, the web framework, is 25 times faster than Flask. However, &lt;a href=&#34;https://mkennedy.codes/posts/replacing-flask-with-robyn-wasnt-worth-it/&#34;&gt;in practice, it was almost a dead even heat&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m thrilled to report that yes, &lt;strong&gt;the web app is much faster using Raw+DC&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Below is an apples-to-apples comparison for Talk Python Training using MongoEngine and the Raw+DC pattern.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Metric&lt;/th&gt;
          &lt;th&gt;MongoEngine (ODM)&lt;/th&gt;
          &lt;th&gt;Raw+DC&lt;/th&gt;
          &lt;th&gt;Improvement&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Requests/sec&lt;/td&gt;
          &lt;td&gt;baseline&lt;/td&gt;
          &lt;td&gt;~1.75x&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;1.75x faster&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Response time&lt;/td&gt;
          &lt;td&gt;baseline&lt;/td&gt;
          &lt;td&gt;~50% less&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;~50% faster&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Memory usage&lt;/td&gt;
          &lt;td&gt;baseline&lt;/td&gt;
          &lt;td&gt;200 MB less&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;18% less&lt;/strong&gt;&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/raw-dc-a-retrospective/raw-dc-vs-mongoengine-graph.webp&#34; alt=&#34;Raw+DC vs ODM/ORM requests per second graph&#34;&gt;&lt;/p&gt;
&lt;p&gt;The memory story is really great as well. After letting the web app run for over 24 hours for each mode, we saw a 200 MB memory usage decrease using Raw+DC.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/raw-dc-a-retrospective/rawdc-vs-odm.webp&#34; alt=&#34;Raw+DC vs ODM/ORM memory usage&#34;&gt;&lt;/p&gt;
&lt;p&gt;That amount of memory might still look high to you. This Raw+DC transformation actually facilitates future work that will cut it in half again, down to about 500 MB for the full app, up and running in production at equilibrium.&lt;/p&gt;
&lt;h2 id=&#34;is-rawdc-worth-migrating-to&#34;&gt;Is Raw+DC worth migrating to?&lt;/h2&gt;
&lt;p&gt;To me, this seems 100% worth it. I&amp;rsquo;ve gained four important things with Raw+DC.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;1.75x the requests per second&lt;/strong&gt; on the exact same hardware and codebase (sans data layer swap)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;18% less memory usage&lt;/strong&gt; with much more savings on the horizon&lt;/li&gt;
&lt;li&gt;New data layer natively &lt;strong&gt;supports async/await&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Removal of problematic&lt;/strong&gt;, core data access library&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All of these benefits and none of that even touches on whether or not this new programming model is better for AI (it is).&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Fire and Forget at Textual</title>
            <link>https://mkennedy.codes/posts/fire-and-forget-at-textual/</link>
            <pubDate>Sun, 29 Mar 2026 09:37:44 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/fire-and-forget-at-textual/</guid>
            <description>&lt;p&gt;If you read my &lt;a href=&#34;https://mkennedy.codes/posts/fire-and-forget-or-never-with-python-s-asyncio/&#34;&gt;Fire and Forget (or Never)&lt;/a&gt; about Python and asynchronous programming, you could think it&amp;rsquo;s a super odd edge case. But a reader/listener, Richard, pointed me at Will McGugan&amp;rsquo;s article &lt;a href=&#34;https://textual.textualize.io/blog/2023/02/11/the-heisenbug-lurking-in-your-async-code/&#34;&gt;The Heisenbug lurking in your async code&lt;/a&gt;. This is basically the same article, but in Will-style.&lt;/p&gt;
&lt;p&gt;Will does say &amp;ldquo;This behavior is well documented, as you can see from this excerpt.&amp;rdquo; True, but the documentation got this emphasis and warning in Python 3.12 whereas the feature &lt;code&gt;create_task&lt;/code&gt; was added in Python 3.6/3.5 timeframe. So it&amp;rsquo;s not just a matter of did we read the docs carefully. It&amp;rsquo;s a matter of did we reread the docs carefully, years later?&lt;/p&gt;
&lt;p&gt;Luckily Will added some nice concrete numbers I didn&amp;rsquo;t have:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/search?q=%22asyncio.create_task%28%22&amp;amp;type=code&#34;&gt;https://github.com/search?q=%22asyncio.create_task%28%22&amp;amp;type=code&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This appears in over 0.5M separate code files on GitHub&lt;/strong&gt;. To be clear, not every search result for &lt;code&gt;create_task&lt;/code&gt; uses the fire-and-forget pattern, but just on the first page of results there are 5 instances.&lt;/p&gt;
&lt;p&gt;If the design pattern to fix this is to:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Create a global set&lt;/li&gt;
&lt;li&gt;When a task is added to the event loop, add it to the set&lt;/li&gt;
&lt;li&gt;Remove it from the set when it&amp;rsquo;s done&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Wouldn&amp;rsquo;t it have been better for the Python team to add this to the event loop internally once and solve this problem for everyone globally across the entire Python ecosystem?&lt;/p&gt;
&lt;p&gt;It doesn&amp;rsquo;t look like that&amp;rsquo;s going to happen. So make sure you double check your code for &lt;code&gt;create_task&lt;/code&gt;. And don&amp;rsquo;t let the Heisenbugs bite.&lt;/p&gt;
&lt;p&gt;And yes, &lt;strong&gt;I know about task groups&lt;/strong&gt;. Several people told me that we could use task groups to hang on to the task. Yes, that&amp;rsquo;s true. But task groups are incongruent with the fire-and-forget design pattern. Why? Because you create the group in a context manager and then you wait for all the tasks in the group to be finished. That doesn&amp;rsquo;t allow you to fire off a task and then continue working. So task groups may or may not have fixed Will&amp;rsquo;s problem, but they don&amp;rsquo;t solve the one &lt;a href=&#34;https://mkennedy.codes/posts/fire-and-forget-or-never-with-python-s-asyncio/&#34;&gt;I was originally talking about&lt;/a&gt;.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Replacing Flask with Robyn wasn&#39;t worth it</title>
            <link>https://mkennedy.codes/posts/replacing-flask-with-robyn-wasnt-worth-it/</link>
            <pubDate>Mon, 23 Mar 2026 09:31:19 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/replacing-flask-with-robyn-wasnt-worth-it/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/replacing-flask-with-robyn-wasnt-worth-it/rusting.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR;&lt;/strong&gt; I converted &lt;a href=&#34;https://pythonbytes.fm/?ref=mkennedycodes&#34;&gt;Python Bytes&lt;/a&gt; from Quart/Flask to the Rust-backed &lt;a href=&#34;https://robyn.tech/&#34;&gt;Robyn&lt;/a&gt; framework and benchmarked it with &lt;a href=&#34;https://locust.io/&#34;&gt;Locust&lt;/a&gt;. &lt;strong&gt;There was no meaningful speed or memory improvement&lt;/strong&gt; - and Robyn actually used &lt;em&gt;more&lt;/em&gt; memory. Framework maturity, ecosystem depth, and app server flexibility still matter more than raw benchmark numbers.&lt;/p&gt;
&lt;p&gt;Last week I played with the idea of replacing &lt;a href=&#34;https://quart.palletsprojects.com/en/latest/&#34;&gt;Quart&lt;/a&gt; (async Flask ) with &lt;a href=&#34;https://robyn.tech/&#34;&gt;Robyn&lt;/a&gt; for our bigger web apps. Robyn is built almost entirely in Rust, and in the benchmarks, it looks dramatically better. Not just a little bit faster, but 25 times faster. However, if you&amp;rsquo;ve been around the block for a while, you know that benchmarks and how things work for &lt;em&gt;your app&lt;/em&gt; and &lt;em&gt;your situation&lt;/em&gt; are not always the same thing.&lt;/p&gt;
&lt;p&gt;So I picked the simplest complex app that I run, &lt;a href=&#34;https://pythonbytes.fm/?ref=mkennedycodes&#34;&gt;Python Bytes&lt;/a&gt;, and converted it entirely to run on the Robyn framework. This took a few hours of careful work and experimenting, and I even had to create &lt;a href=&#34;https://mkennedy.codes/posts/use-chameleon-templates-in-the-robyn-web-framework/&#34;&gt;a Python package&lt;/a&gt; to allow Robyn to run the Chameleon template language.&lt;/p&gt;
&lt;p&gt;When I was done, it was time to fire up &lt;a href=&#34;https://locust.io/&#34;&gt;Locust&lt;/a&gt; and see if there was any dramatic performance improvements. I certainly wasn&amp;rsquo;t expecting 25x, but 2x? 1.5x? That would have been really impressive.&lt;/p&gt;
&lt;h2 id=&#34;did-robyn-improve-speed-or-memory-over-flask&#34;&gt;Did Robyn improve speed or memory over Flask?&lt;/h2&gt;
&lt;p&gt;The results were in and &lt;strong&gt;the answer was just about no difference in RPS&lt;/strong&gt; or latency. It turns out that almost all the computational time is in the logic of our app, which of course doesn&amp;rsquo;t change and I never intended to change it.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Requests per second:&lt;/strong&gt; No meaningful difference between Robyn and Quart/Granian&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Latency:&lt;/strong&gt; Essentially identical under load&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Memory:&lt;/strong&gt; Robyn actually used &lt;em&gt;more&lt;/em&gt; memory, not less&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Another area I was hoping to optimize is memory. Our web apps use a lot of memory for what they are. They&amp;rsquo;re certainly not trivial. But running a couple of copies of the app in a web garden was using way more than I expected that they should. And I thought moving closer to Rust might have positive influences for memory too.&lt;/p&gt;
&lt;p&gt;It turns out &lt;strong&gt;the Robyn fork actually used more memory, not less&lt;/strong&gt;, than the current setup. After all, our web apps run on &lt;a href=&#34;https://github.com/emmett-framework/granian&#34;&gt;Granian&lt;/a&gt;, which is mostly Rust right up to the Flask framework itself already.&lt;/p&gt;
&lt;h2 id=&#34;why-flasks-maturity-still-beats-robyns-speed&#34;&gt;Why Flask&amp;rsquo;s maturity still beats Robyn&amp;rsquo;s speed&lt;/h2&gt;
&lt;p&gt;So our fun little spike to explore the Robyn framework is going to remain just that. I&amp;rsquo;m sticking with Flask. I&amp;rsquo;ve talked about this before, but maturity in a library or framework is a big plus. The ecosystem for Flask/Quart is much bigger and more polished than for the smaller Robyn framework.&lt;/p&gt;
&lt;p&gt;More than that, the app server runtime for Robyn is much less polished than some of the pluggable app servers out there. Think Granian, Gunicorn, uvicorn, etc. For example, Robyn does not support web garden process recycling. In many servers you can say after five hours or 10,000 requests or something like that, just slowly take the request out of a process, spin up a new one and shut down the old one just to keep things fresh. This helps if you&amp;rsquo;re using some library that holds on to too many caches or some other weird memory thing.&lt;/p&gt;
&lt;h2 id=&#34;was-the-robyn-experiment-a-waste-of-time&#34;&gt;Was the Robyn experiment a waste of time?&lt;/h2&gt;
&lt;p&gt;Even though I spent maybe close to six hours working on this exploration and decided not to use it, I still found it super valuable. I created the fun &lt;a href=&#34;https://mkennedy.codes/posts/use-chameleon-templates-in-the-robyn-web-framework/&#34;&gt;Chameleon Robyn package&lt;/a&gt; to help people using Robyn have a greater choice of template languages. I got to see my apps from multiple perspectives. I built out some tooling for Claude that I&amp;rsquo;m going to write about later that is generally really awesome. And I ended up saving significant memory for some of my biggest web apps by just spending more time thinking about how I&amp;rsquo;m running them currently in Granian and Flask.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Use Chameleon templates in the Robyn web framework</title>
            <link>https://mkennedy.codes/posts/use-chameleon-templates-in-the-robyn-web-framework/</link>
            <pubDate>Thu, 19 Mar 2026 16:18:41 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/use-chameleon-templates-in-the-robyn-web-framework/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR; &lt;a href=&#34;https://pypi.org/project/chameleon_robyn/&#34;&gt;Chameleon-robyn&lt;/a&gt;&lt;/strong&gt; is a new Python package I created that brings &lt;a href=&#34;https://chameleon.readthedocs.io/en/latest/&#34;&gt;Chameleon template&lt;/a&gt; support to the &lt;a href=&#34;https://robyn.tech&#34;&gt;Robyn web framework&lt;/a&gt;. If you prefer Chameleon&amp;rsquo;s structured, HTML-first approach over Jinja and want to try Robyn&amp;rsquo;s Rust-powered performance, this package bridges the two.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;People who have known me for a while know that I&amp;rsquo;m very much &lt;strong&gt;not&lt;/strong&gt; a fan of the &lt;a href=&#34;https://pypi.org/project/Jinja2/&#34;&gt;Jinja templating language&lt;/a&gt;. &lt;strong&gt;Neither&lt;/strong&gt; am I a fan of the &lt;a href=&#34;https://docs.djangoproject.com/en/6.0/ref/templates/language/&#34;&gt;Django templating language&lt;/a&gt;, since it&amp;rsquo;s very similar. I dislike the fact that you&amp;rsquo;re mostly programming with interlaced HTML rather than having mostly HTML that is very restricted in what it allows in terms of coding. While nowhere near perfect, I prefer &lt;a href=&#34;https://chameleon.readthedocs.io/en/latest/&#34;&gt;Chameleon&lt;/a&gt; because it requires you to write well-structured code. Sadly, I think Jinja won exactly because it allows you to write whatever Python code in your HTML you want. For most frameworks, Jinja is the only templating language they support.&lt;/p&gt;
&lt;h2 id=&#34;why-migrate-chameleon-templates-to-a-new-framework&#34;&gt;Why migrate Chameleon templates to a new framework?&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;d love to try out some new frameworks, but I have so much existing Chameleon code that any sort of migration will never include converting to Jinja, if I have a say in it. Not because of my dislike for it, but  because it&amp;rsquo;s incredibly error prone, and it would mean changing my entire web design, not just my code.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the code breakdown for just &lt;a href=&#34;https://training.talkpython.fm/&#34;&gt;Talk Python Training&lt;/a&gt;.&lt;/p&gt;
&lt;img src=&#34;talk-python-training.png&#34; style=&#34;max-width: 500px&#34; /&gt;
&lt;p&gt;That &lt;strong&gt;design category&lt;/strong&gt; is 14,650 lines of HTML and 11,104 lines of CSS! If I can get Chameleon running on a framework, it will 100% reuse every line of that to perfection. If I cannot, I&amp;rsquo;m rewriting them all. No thanks.&lt;/p&gt;
&lt;h2 id=&#34;how-does-robyn-use-rust-to-speed-up-python-web-apps&#34;&gt;How does Robyn use Rust to speed up Python web apps?&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been thinking a lot about what if our web frameworks actually ran in Rust? Right now I&amp;rsquo;m running Quart (async Flask) on top of Granian. So Rust is the base of my web server and processing. But there is a lot of infrastructure provided by Flask and Werkzueg leading up to my code actually running that is all based on Python.&lt;/p&gt;
&lt;p&gt;Would it be a lot faster? Maybe. My exploring this idea was inspired by &lt;a href=&#34;https://turboapi.trilok.ai/&#34;&gt;TurboAPI&lt;/a&gt;. TurboAPI did exactly this as I was thinking about, but with Zig and for FastAPI. While I am &lt;strong&gt;not&lt;/strong&gt; recommending people leave FastAPI, their headline &amp;ldquo;FastAPI-compatible. Zig HTTP core. 22x faster,&amp;rdquo; does catch one&amp;rsquo;s attention.&lt;/p&gt;
&lt;p&gt;Eventually I found my way to &lt;a href=&#34;https://robyn.tech&#34;&gt;Robyn&lt;/a&gt;. &lt;a href=&#34;https://github.com/sparckles/Robyn&#34;&gt;Robyn&lt;/a&gt; merges Python&amp;rsquo;s async capabilities with a Rust runtime for reliable, scalable web solutions. Here are a few key highlights:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Runtime&lt;/strong&gt;: Rust-based request handling for high throughput&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;API style&lt;/strong&gt;: Flask-like Python API, making migration straightforward&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Async&lt;/strong&gt;: Built-in async support out of the box&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There&amp;rsquo;s this quite interesting performance graph from &lt;a href=&#34;https://github.com/sparckles/Robyn&#34;&gt;Robyn&amp;rsquo;s benchmarks&lt;/a&gt;. Of course, take it with all the caveats that benchmarks come with.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/use-chameleon-templates-in-the-robyn-web-framework/perf-robyn.webp&#34; alt=&#34;Benchmark comparing Robyn, FastAPI, Flask, and Django on request throughput&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;how-to-use-chameleon-templates-with-robyn&#34;&gt;How to use Chameleon templates with Robyn&lt;/h2&gt;
&lt;p&gt;I want to try this framework on real projects that I&amp;rsquo;m running in production to see how they size up. However, given all of my web UI is written in Chameleon, there&amp;rsquo;s absolutely no way I&amp;rsquo;m converting to Jinja. I can hear everyone now, &amp;ldquo;So just use it for something simple and new, Michael.&amp;rdquo; For me that defeats the point. Thus, my obsession with getting Chameleon to work.&lt;/p&gt;
&lt;p&gt;I created the integration for Chameleon for Flask with my &lt;a href=&#34;https://github.com/mikeckennedy/chameleon-flask&#34;&gt;chameleon-flask package&lt;/a&gt;. Could I do the same thing for Robyn?&lt;/p&gt;
&lt;p&gt;It turns out that I can! Without further ado, &lt;strong&gt;introducing chameleon-robyn&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href=&#34;https://github.com/mikeckennedy/chameleon-robyn&#34;&gt;github.com/mikeckennedy/chameleon-robyn&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PyPI&lt;/strong&gt;: &lt;a href=&#34;https://pypi.org/project/chameleon_robyn/&#34;&gt;pypi.org/project/chameleon_robyn&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It&amp;rsquo;s super early days and I&amp;rsquo;m just starting to use this package for my prototype. I&amp;rsquo;m sure as I put it into production in a real app, I&amp;rsquo;ll see if it&amp;rsquo;s feature complete or not.&lt;/p&gt;
&lt;p&gt;For now, it&amp;rsquo;s out there on GitHub and on PyPI. If Chameleon + Robyn sounds like an interesting combo to you as well, give this a try. PRs are welcome.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Fire and forget (or never) with Python’s asyncio</title>
            <link>https://mkennedy.codes/posts/fire-and-forget-or-never-with-python-s-asyncio/</link>
            <pubDate>Wed, 18 Mar 2026 20:22:27 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/fire-and-forget-or-never-with-python-s-asyncio/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/fire-and-forget-or-never-with-python-s-asyncio/fire-forget.webp&#34; alt=&#34;Python asyncio fire and forget task pattern&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR;&lt;/strong&gt; Python&amp;rsquo;s &lt;code&gt;asyncio.create_task()&lt;/code&gt; can silently garbage collect your fire-and-forget tasks starting in Python 3.12 - they may never run. The fix: store task references in a &lt;code&gt;set&lt;/code&gt; and register a &lt;code&gt;done_callback&lt;/code&gt; to clean them up.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Do you use Python&amp;rsquo;s async/await in programming? Often you have some async task that needs to run, but you don&amp;rsquo;t care to monitor it, know when it&amp;rsquo;s done, or even if it errors.&lt;/p&gt;
&lt;p&gt;Let&amp;rsquo;s imagine you have an async function that logs to a remote service. You want its execution out of the main-line execution. Maybe it looks like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;async&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;log_account_created&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;username&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;str&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Someone new to Python&amp;rsquo;s odd version of async would think they could write code like this (hint, &lt;strong&gt;they&lt;/strong&gt; &lt;strong&gt;cannot&lt;/strong&gt;):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;async&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;register_user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;get_form_data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;user&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;user_service&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;create_user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# Log in the background (actually no, but we try)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;log_account_created&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# BUG!&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;redirect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;/account/welcome&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;&lt;strong&gt;You cannot just run an async function, you fool!&lt;/strong&gt; Why? I don&amp;rsquo;t know. It&amp;rsquo;s a massively needless complication of modern Python. You either have to &lt;strong&gt;await it&lt;/strong&gt; (which would block the main execution foiling our fire and forget intention) or you have to &lt;strong&gt;start it as a task separately&lt;/strong&gt;. Here&amp;rsquo;s the working version (at least in Python 3.11 it works, Python 3.12+? Sometimes):&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;async&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;register_user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;get_form_data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;user&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;user_service&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;create_user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# Log in the background?&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# Runs on the asyncio loop, fixed, maybe&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;asyncio&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;create_task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;log_account_created&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;redirect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;/account/welcome&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;why-asynciocreate_task-loses-tasks-in-python-312&#34;&gt;Why asyncio.create_task loses tasks in Python 3.12+&lt;/h2&gt;
&lt;p&gt;Actually that fixed version has a tremendously subtle race condition that was introduced in Python 3.12 (seriously). In Python 3.11 or before, the async loop holds the new task and will just run it at some point soon.&lt;/p&gt;
&lt;p&gt;Here is the first line in the docs for &lt;code&gt;create_task&lt;/code&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Wrap the &lt;em&gt;coro&lt;/em&gt; &lt;a href=&#34;https://docs.python.org/3/library/asyncio-task.html#coroutine&#34;&gt;coroutine&lt;/a&gt; into a &lt;a href=&#34;https://docs.python.org/3/library/asyncio-task.html#asyncio.Task&#34;&gt;&lt;code&gt;Task&lt;/code&gt;&lt;/a&gt; and &lt;strong&gt;schedule its execution&lt;/strong&gt;. Return the Task object.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It schedules its execution. But in Python 3.12, it might forget about it!&lt;/p&gt;
&lt;p&gt;I call functions like &lt;code&gt;log_account_created&lt;/code&gt; &lt;em&gt;fire and forget&lt;/em&gt; async functions. You don&amp;rsquo;t care to wait for it or even check its outcome. What are you going to do if logging fails anyway? Log it more? But check this out, &lt;a href=&#34;https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task&#34;&gt;straight from Python 3.14&amp;rsquo;s documentation&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Important&lt;/strong&gt;: Save a reference to the result of [ &lt;code&gt;asyncio.create_task&lt;/code&gt; ], to avoid a task disappearing mid-execution. The event loop only keeps weak references to tasks. A task that isn’t referenced elsewhere may get garbage collected at any time, even before it’s done. For reliable “fire-and-forget” background tasks, gather them in a collection&lt;/em&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;background_tasks&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;set&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;i&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;range&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;10&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;task&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;asyncio&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;create_task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;some_coro&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;param&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;i&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# Add task to the set. This creates a strong reference.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;background_tasks&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;add&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# To prevent keeping references to finished tasks forever,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# make each task remove its own reference from the set after&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# completion:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;task&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;add_done_callback&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;background_tasks&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;discard&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Wait, what? &lt;strong&gt;If I start a Python async task it might be GC&amp;rsquo;ed before it even starts&lt;/strong&gt;? Wow, just wow.&lt;/p&gt;
&lt;p&gt;The fix? Well, you just create a &lt;code&gt;set&lt;/code&gt; to track them (keep a strong reference in GC-parlance). In our example, it looks like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;background_tasks&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;set&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;async&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;register_user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;data&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;get_form_data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;user&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;user_service&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;create_user&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;data&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# Log in the background&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;task&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;asyncio&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;create_task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;log_account_created&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;user&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;name&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;))&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;background_tasks&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;add&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;task&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;                       &lt;span class=&#34;c1&#34;&gt;# prevent GC&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;task&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;add_done_callback&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;background_tasks&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;discard&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# cleanup when done&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;redirect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;/account/welcome&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;one-obvious-way-to-do-it&#34;&gt;One obvious way to do it&lt;/h2&gt;
&lt;p&gt;This &lt;code&gt;set&lt;/code&gt; hack is entirely non-obvious that this is required. After all, the &lt;a href=&#34;https://www.youtube.com/watch?v=i6G6dmVJy74&#34;&gt;Zen of Python&lt;/a&gt; states:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;There should be one &amp;ndash; and preferably only one &amp;ndash; obvious way to do it.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;But &lt;em&gt;Zen&lt;/em&gt; doesn&amp;rsquo;t always apply, does it? I&amp;rsquo;m sure there is &lt;em&gt;a reason&lt;/em&gt; for this change, though I&amp;rsquo;m not sure it was worth it.&lt;/p&gt;
&lt;p&gt;If it were up to me, Python would come with one omnipresent event loop running on a background thread. Just calling an async function would schedule and run it there. The &lt;code&gt;await&lt;/code&gt; keyword would be a control flow only construct, not the thing that actually does the execution.&lt;/p&gt;
&lt;p&gt;But it doesn&amp;rsquo;t work that way and so we get oddities here and there I guess.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;: See my follow up post &lt;a href=&#34;https://mkennedy.codes/posts/fire-and-forget-at-textual/&#34;&gt;Fire and Forget at Textual&lt;/a&gt; for more details.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Will AI Kill Open Source?</title>
            <link>https://mkennedy.codes/posts/will-ai-kill-open-source/</link>
            <pubDate>Thu, 12 Mar 2026 11:39:47 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/will-ai-kill-open-source/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR; No - AI won&amp;rsquo;t kill open source, but it will reshape it.&lt;/strong&gt; Small, single-purpose packages (micro open source) are likely to languish as AI agents write trivial utility code on the fly. But major frameworks, databases, and runtimes like Django, Postgres, and Python itself aren&amp;rsquo;t going anywhere - AI agents actually &lt;em&gt;prefer&lt;/em&gt; reaching for established building blocks over reinventing them.  The key is staying in the architect&amp;rsquo;s seat.&lt;/p&gt;
&lt;p&gt;AI will replace the trivial, leave the foundational, and force us to rethink everything in between:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Micro open source (utility packages):&lt;/strong&gt; Likely to decline &amp;ndash; AI writes trivial code faster than importing it&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Mid-level libraries:&lt;/strong&gt; Case-by-case &amp;ndash; depends on complexity and maintenance burden&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Major frameworks and infrastructure:&lt;/strong&gt; Safe &amp;ndash; AI agents prefer &lt;code&gt;uv pip install&lt;/code&gt; over reinventing Django&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I sat down with &lt;a href=&#34;https://github.com/pauleveritt&#34;&gt;Paul Everitt&lt;/a&gt; to debate this question, and it turns out the answer is way more nuanced than a simple yes or no.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=VVk-GMbAcew&#34;&gt;Watch the full conversation on YouTube →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id=&#34;why-would-ai-rebuild-what-frameworks-already-provide&#34;&gt;Why would AI rebuild what frameworks already provide?&lt;/h2&gt;
&lt;p&gt;Paul kicked things off with a great framing. Think of building an app like a 100-meter soccer field. A framework like Flask or Django gets you 95 meters down the field. You and your AI agent only need to handle the last 5 meters &amp;ndash; the part that&amp;rsquo;s unique to your app.&lt;/p&gt;
&lt;p&gt;Why would an agent rebuild those 95 meters from scratch when it can just &lt;code&gt;uv pip install&lt;/code&gt; the framework and focus on the hard part? Software is a liability, not an asset, and owning all of that code means owning all of those future bugs.&lt;/p&gt;
&lt;p&gt;But there&amp;rsquo;s a counterargument: if you only need 10% of a framework, you&amp;rsquo;re still dragging in the other 90% &amp;ndash; attack surface, security issues, maintenance burden. Maybe you&amp;rsquo;re better off owning a small thing than renting a large one?&lt;/p&gt;
&lt;h2 id=&#34;will-ai-replace-small-open-source-packages&#34;&gt;Will AI replace small open source packages?&lt;/h2&gt;
&lt;p&gt;I think the real casualty here is &lt;em&gt;micro&lt;/em&gt; open source &amp;ndash; those tiny packages that wrap a single function or a handful of utility classes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Evidence? Tailwind usage:&lt;/strong&gt; It&amp;rsquo;s up &lt;a href=&#34;https://trends.builtwith.com/framework/Tailwind-CSS&#34;&gt;600% in the last 18 months&lt;/a&gt;, largely because AI loves reaching for it. But the revenue story for Tailwind is heading in the opposite direction. AI can easily write the 47 utility classes you actually need instead of pulling in the whole framework.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What should go? Micro-packages:&lt;/strong&gt; There&amp;rsquo;s the &lt;a href=&#34;https://arstechnica.com/information-technology/2016/03/rage-quit-coder-unpublished-17-lines-of-javascript-and-broke-the-internet/&#34;&gt;left-pad cautionary tale&lt;/a&gt;. A single trivial function as a standalone package took down huge swaths of the JavaScript ecosystem when its maintainer pulled it. AI &lt;em&gt;should&lt;/em&gt; absolutely be writing those two functions for us instead of importing a package for them.&lt;/p&gt;
&lt;h2 id=&#34;can-ai-replace-major-frameworks-like-django-or-postgres&#34;&gt;Can AI replace major frameworks like Django or Postgres?&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s what I don&amp;rsquo;t see happening: an AI saying &amp;ldquo;let me rebuild Postgres for you&amp;rdquo; or &amp;ldquo;give me an hour, I&amp;rsquo;ll recreate Django from scratch.&amp;rdquo; Even if it &lt;em&gt;could&lt;/em&gt;, why would it? The agent&amp;rsquo;s goal is to solve your problem well and quickly. &lt;code&gt;uv pip install django&lt;/code&gt; is faster and more reliable than conjuring up a bespoke web framework.&lt;/p&gt;
&lt;p&gt;At the macro level, frameworks, databases, runtimes, open source is safe.&lt;/p&gt;
&lt;h2 id=&#34;will-ai-coding-costs-make-open-source-irrelevant&#34;&gt;Will AI coding costs make open source irrelevant?&lt;/h2&gt;
&lt;p&gt;Paul raised an important point: what happens when AI pricing subsidies end and costs go up 5x? My take is that hardware costs are dropping even faster.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://blogs.nvidia.com/blog/data-blackwell-ultra-performance-lower-cost-agentic-ai/&#34;&gt;NVIDIA&amp;rsquo;s latest inference hardware is roughly 10x cheaper per token than two years ago&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;NVIDIA GB200 NVL72 with extreme hardware and software codesign delivers more than 10x more tokens per watt, &lt;strong&gt;resulting in one-tenth the cost per token&lt;/strong&gt;.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And the &lt;a href=&#34;https://www.apple.com/newsroom/2026/03/apple-debuts-m5-pro-and-m5-max-to-supercharge-the-most-demanding-pro-workflows/&#34;&gt;Apple Silicon trajectory&lt;/a&gt; means serious local model capability is coming to everyone&amp;rsquo;s laptop. The bubble isn&amp;rsquo;t as extreme as people imagine.&lt;/p&gt;
&lt;h2 id=&#34;how-should-developers-work-with-ai-agents-on-open-source-projects&#34;&gt;How should developers work with AI agents on open source projects?&lt;/h2&gt;
&lt;p&gt;We also dug into the &amp;ldquo;just send it&amp;rdquo; overnight agent workflow &amp;ndash; and neither of us is a fan. Working in small, reviewable chunks is the way. Think &lt;a href=&#34;https://mkennedy.codes/posts/its-not-vibe-coding-agentic-engineering/&#34;&gt;spec-driven development&lt;/a&gt;, not &amp;ldquo;agents devour this, I&amp;rsquo;ll see you in the morning.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Our job was never to type characters. It&amp;rsquo;s to ship quality software. If you apply engineering discipline &amp;ndash; specs, tests, architecture decisions &amp;ndash; then AI-assisted code is absolutely something you can put your name on. Paul shared his crisis of confidence the first time he hit Enter on &lt;code&gt;twine upload&lt;/code&gt; for an AI-assisted package. I think a lot of developers can relate to that moment. But the question comes down to: did you ship something well-built that serves a purpose? If yes, the tooling you used to get there matters a lot less than you think.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the workflow that actually works:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Choose your frameworks&lt;/strong&gt; and specify your stack up front&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Work in small, reviewable chunks&lt;/strong&gt; &amp;ndash; not overnight agent runs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Use spec-driven development&lt;/strong&gt; with tests and architecture decisions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Review all AI-generated output&lt;/strong&gt; before shipping&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;will-ai-kill-open-source-the-verdict&#34;&gt;Will AI kill open source? The verdict&lt;/h2&gt;
&lt;p&gt;Micro open source is probably toast. The big building blocks aren&amp;rsquo;t going anywhere. But the key is to stay in the architect&amp;rsquo;s seat &amp;ndash; choose your frameworks, specify your stack, review the output. Be the architect handing specs to the contractor, and don&amp;rsquo;t give that role away to the AI.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a lot more in the full conversation including anti-AI vigilante groups shaming people for publishing agent-assisted packages, the open source gift economy, and why none of us really know where this is all heading yet.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=VVk-GMbAcew&#34; target=&#34;blank&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/will-ai-kill-open-source/poster-kill-open-source.jpg&#34; style=&#34;max-width: 600px; border-radius: 10px; border: 1px solid #ddd;&#34; alt=&#34;Michael Kennedy and Paul Everitt discussing whether AI will kill open source&#34; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=VVk-GMbAcew&#34;&gt;Watch the full video with Paul Everitt →&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>What hyper-personal software looks like</title>
            <link>https://mkennedy.codes/posts/what-hyper-personal-software-looks-like/</link>
            <pubDate>Fri, 06 Mar 2026 11:32:30 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/what-hyper-personal-software-looks-like/</guid>
            <description>&lt;p&gt;Have you heard that the age of hyper-personal software is upon us?&lt;/p&gt;
&lt;p&gt;Typically what people mean is that agentic AI allows the creation of simple and small software built by individuals, often not super technical individuals, to solve a personal problem. As a result, we will see this explosion of software and the death of SaaS.&lt;/p&gt;
&lt;p&gt;Naysayers point to the lack of many new software projects being launched as proof that agentic AI is all hype.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s not hype. It&amp;rsquo;s just that much of this software is hidden. Maybe we&amp;rsquo;ll call it &lt;strong&gt;dark matter software&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&#34;hyper-personal-software-an-example&#34;&gt;Hyper-personal software, an example&lt;/h2&gt;
&lt;p&gt;I personally have many of these dark matter software projects. So I thought I would share an incredibly straightforward one to inspire you as well as make the concepts concrete.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve stopped using Google search a long time ago. I&amp;rsquo;m not a big fan of how it works, the tracking, and so much more. I bounced around from different search engines, even used the paid Kagi one for a while.&lt;/p&gt;
&lt;p&gt;These days I&amp;rsquo;m using Start Page at &lt;a href=&#34;https://www.startpage.com&#34;&gt;https://www.startpage.com&lt;/a&gt;. It&amp;rsquo;s great, very fast, has good results, and cleans up much of the extra junk that Google adds to its results.&lt;/p&gt;
&lt;p&gt;However, recently they started adding advertisements, sponsored links to their results. Now, if these were chill, small, little sponsored links, I would have absolutely had no complaint with them. After all, I do want to support Start Page. But the amount of ads that were included is pretty egregious.&lt;/p&gt;
&lt;p&gt;Here are the results for searching for an AI course on my 5k monitor:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/what-hyper-personal-software-looks-like/polluted.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Check this out. Even on a huge monitor, &lt;strong&gt;not a single organic result is visible&lt;/strong&gt;. This is expected from Google, but not here (not for me anyway).&lt;/p&gt;
&lt;h2 id=&#34;claude-i-need-a-browser-extension&#34;&gt;Claude, I need a browser extension&lt;/h2&gt;
&lt;p&gt;I am very hesitant to install third-party browser extensions. A few hundred are outright malicious. And most frightening of all, sometimes the good ones get hijacked, phished, or outright purchased by less well-meaning folks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;But what I need to solve this problem is a browser extension.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;I could create one from scratch. I could go through all the tutorials and so on and actually create one. It wouldn&amp;rsquo;t take that much time. Maybe half a day? But this problem is not that bad. I can page down once or hit the space bar after a search and deal with it. I don&amp;rsquo;t want to waste half a day which likely turns into 2 days. ;)&lt;/p&gt;
&lt;p&gt;This is where agentic coding comes in. Instead of continuing to be frustrated yet again after doing a search, I fired up Claude Code inside of my editor and asked for a web browser extension that would remove just the ads from Start Page. I gave it the exact example HTML that is used. I described what I want, I hit enter, and I walked away.&lt;/p&gt;
&lt;p&gt;I came back 15 minutes later and I had a local browser extension. Of course, instead of publishing it to the Chrome Web Store, I just loaded it locally.&lt;/p&gt;
&lt;img src=&#34;https://cdn.mkennedy.codes/posts/what-hyper-personal-software-looks-like/extension-installed.png&#34; style=&#34;max-width: 450px&#34; /&gt;
&lt;p&gt;I was &lt;strong&gt;so&lt;/strong&gt; ready to run my next search. I gave it a try and there were literally no results on the page. It didn&amp;rsquo;t work. But after one or two more exchanges with Claude to narrow down the selectors, I have a perfectly crisp start page result set.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/what-hyper-personal-software-looks-like/clean.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Look at that &lt;strong&gt;crystal clear search page&lt;/strong&gt;. My hyper-personal browser extension solved my problem perfectly.&lt;/p&gt;
&lt;h2 id=&#34;startpage-clean-is-just-for-me&#34;&gt;Startpage-clean is just for me&lt;/h2&gt;
&lt;p&gt;It&amp;rsquo;s a small project as you can see from &lt;a href=&#34;https://mkennedy.codes/tools/tallyman/&#34;&gt;Tallyman&lt;/a&gt;.&lt;/p&gt;
&lt;img src=&#34;https://cdn.mkennedy.codes/posts/what-hyper-personal-software-looks-like/startpage-clean-source.png&#34; style=&#34;max-width: 600px&#34; /&gt;
&lt;p&gt;Even so, I have no intention of sharing it with the world. I don&amp;rsquo;t want to create or maintain yet another ad blocker. If something like Ad Block Plus or whatever wants to take this on they can.&lt;/p&gt;
&lt;p&gt;I had a problem that software would solve. I spent 10 active minutes and I had that software created exactly as I wanted it. &lt;strong&gt;That&amp;rsquo;s hyper-personal software&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&#34;so-where-is-this-explosion-of-software&#34;&gt;So where is this explosion of software?&lt;/h2&gt;
&lt;p&gt;Now, when you hear people claim that agentic AI is a productivity bust, think back to this example. It&amp;rsquo;s the perfect use-case on an absolutely zero stakes project that tangibly changes my day-to-day work, and yet no one will ever see it. You wouldn&amp;rsquo;t even know it existed if I didn&amp;rsquo;t take the time to use it as an example.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s a lot of dark matter software out there, and its genesis is just beginning.&lt;/p&gt;
&lt;h2 id=&#34;more-to-come&#34;&gt;More to come&lt;/h2&gt;
&lt;p&gt;Beyond dark matter software, I think there will legitimately be an explosion of new software because of AI. I have a lot more to say on this with concrete examples as well. So be sure to subscribe to the blog if you&amp;rsquo;re not already.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Raw&#43;DC: The ORM pattern of 2026?</title>
            <link>https://mkennedy.codes/posts/raw-dc-the-orm-pattern-of-2026/</link>
            <pubDate>Mon, 09 Feb 2026 19:29:08 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/raw-dc-the-orm-pattern-of-2026/</guid>
            <description>&lt;p&gt;Recently &lt;strong&gt;I have started going Raw+DC on my databases&lt;/strong&gt;. I think I love it. Let me explain.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR;&lt;/strong&gt; After 25+ years championing ORMs, I&amp;rsquo;ve switched to raw database queries paired with Python dataclasses. I&amp;rsquo;m calling it the &lt;strong&gt;Raw+DC pattern&lt;/strong&gt;. The result: better AI coding assistance, fewer aging dependencies, comparable or better performance, and type safety where it counts.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;&lt;/th&gt;
          &lt;th&gt;ORM/ODM&lt;/th&gt;
          &lt;th&gt;Raw+DC Pattern&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Type safety&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Built-in via entity classes&lt;/td&gt;
          &lt;td&gt;Dataclasses at the data access boundary&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;AI coding support&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Limited by library popularity&lt;/td&gt;
          &lt;td&gt;Excellent - vanilla query syntax is 1,000x more common&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Dependency risk&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;High: ORMs can go unmaintained&lt;/td&gt;
          &lt;td&gt;Low: only the DB driver&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Performance&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Overhead from serialization/validation&lt;/td&gt;
          &lt;td&gt;Near-raw speed&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Query complexity&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Abstracted by ORM&lt;/td&gt;
          &lt;td&gt;Native queries&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;why-developers-use-orms-and-odms&#34;&gt;Why developers use ORMs and ODMs&lt;/h2&gt;
&lt;p&gt;For almost my entire programming career &lt;strong&gt;I have been a massive proponent of the concept of an ORM (relational) and ODM (document)&lt;/strong&gt; for talking to a database. ORMs provide several key benefits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Type-safe entity classes&lt;/strong&gt; mapped directly to database tables&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;SQL injection prevention&lt;/strong&gt; - no more &lt;a href=&#34;https://xkcd.com/327/&#34;&gt;little bobby tables&lt;/a&gt; wrecking your day&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Complex query abstraction&lt;/strong&gt; - eager joins expressed in Python, not SQL&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;IDE autocompletion&lt;/strong&gt; on every entity field&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;People would tell me that I&amp;rsquo;d get better performance if I just did raw db queries that returned tuples or documents. I didn&amp;rsquo;t care. I still don&amp;rsquo;t.&lt;/p&gt;
&lt;p&gt;But ORMs come with trade-offs (going forward assume ORM and/or ODMs ;) ). You have the entity classes sure. But the ORM usually has some dependencies that can be a hassle. They can be finicky in ways that go beyond what you care about.&lt;/p&gt;
&lt;h2 id=&#34;how-raw-database-queries-work-pymongo-example&#34;&gt;How raw database queries work (pymongo example)&lt;/h2&gt;
&lt;p&gt;There is something lovely about the pure simplicity of a direct query in the native query language of the database. Here&amp;rsquo;s a &lt;a href=&#34;https://www.mongodb.com/docs/languages/python/pymongo-driver/current/&#34;&gt;pymongo&lt;/a&gt; example:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;db&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;get_db&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;ctx&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;dict&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;db&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;orders&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;find_one&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;order_number&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;order_number&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;customer_email&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s1&#34;&gt;&amp;#39;_id&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;0&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;ol&gt;
&lt;li&gt;Go to the table &lt;code&gt;orders&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Find the one with the order number &lt;code&gt;order_number&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Return only the &lt;code&gt;customer_email&lt;/code&gt; (not even the primary key &lt;code&gt;id&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;Done.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This query is simple, but they can get increasingly complex. ORMs seriously help for complex queries (think multiple eager joins!) Maybe you don&amp;rsquo;t want to write those complex queries. I sure don&amp;rsquo;t and didn&amp;rsquo;t see the benefit of doing so except in a very narrow high-traffic use-cases.&lt;/p&gt;
&lt;p&gt;Another benefit of ORMs is that your IDEs and type checkers can tell you exactly where an ORM class is being used since every query is expressed in terms of one or more of these classes.&lt;/p&gt;
&lt;h2 id=&#34;why-ai-coding-assistants-work-better-with-raw-queries&#34;&gt;Why AI coding assistants work better with raw queries&lt;/h2&gt;
&lt;p&gt;You see, I have this friend named &lt;a href=&#34;https://code.claude.com/docs/en/overview&#34;&gt;Claude&lt;/a&gt;. He&amp;rsquo;s really good at programming. Gets distracted, but if kept on track he can nail a three-way eager join in SQL.&lt;/p&gt;
&lt;p&gt;Seriously now. After a significant amount of experience programming ORM-backed projects with Claude Opus/Sonnet, it dawned on me that I&amp;rsquo;m writing fewer of the queries by hand and just telling Claude what to do. And we have to keep in mind one of the key rules of working with agentic coding:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;My universal 7th rule of agentic coding&lt;/strong&gt;: AI coding works best for extremely popular languages, tools, and platforms. Given a specialized tool or framework vs. a general &amp;ldquo;vanilla&amp;rdquo; one, choose vanilla if everything else is equal.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Claude knows &lt;a href=&#34;https://beanie-odm.dev&#34;&gt;the Beanie ODM&lt;/a&gt; great (which I use for some of my apps). But do you know what it knows better? Pure, native MongoDB query syntax. Vanilla.&lt;/p&gt;
&lt;p&gt;Look at these stats:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Beanie&lt;/strong&gt;: 1.4M downloads/month &lt;a href=&#34;https://pypistats.org/packages/beanie&#34;&gt;PyPI Stats&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PyMongo&lt;/strong&gt;: 74.2M downloads/month &lt;a href=&#34;https://pypistats.org/packages/pymongo&#34;&gt;PyPI Stats&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;PyMongo is 53x times more popular. The syntax it needs is actually identical across the Node/Deno/Bun ecosystem, the PHP ecosystem, and many others. This makes the examples of pymongo (aka native MongoDB) queries likely 1,000x more common. Agentic AI will likely do much better with this query foundation.&lt;/p&gt;
&lt;h2 id=&#34;how-to-keep-type-safety-without-an-orm&#34;&gt;How to keep type safety without an ORM&lt;/h2&gt;
&lt;p&gt;Remember my adoration of the type safety and the IDE support? Coding with classes returned from the DB queries is &lt;strong&gt;unquestionably better&lt;/strong&gt; in most cases. You specify the return type (db class), get a return value named &lt;code&gt;result&lt;/code&gt;, type &lt;code&gt;result.&lt;/code&gt; and boom, a list of fields appears before your eyes. This also works for powering &lt;a href=&#34;https://docs.astral.sh/ty&#34;&gt;ty&lt;/a&gt;, &lt;a href=&#34;https://pyrefly.org&#34;&gt;pyrefly&lt;/a&gt;, and all the other type checkers.&lt;/p&gt;
&lt;p&gt;My realization is that we don&amp;rsquo;t actually need the types baked entirely into the queries. I just want to talk in Python classes beyond the data access layer.&lt;/p&gt;
&lt;p&gt;Enter &lt;a href=&#34;https://docs.python.org/3/library/dataclasses.html&#34;&gt;dataclasses&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If we just make the data access layer convert results to data classes when it&amp;rsquo;s to our benefit and keep them as single values (scalars) for basic queries, we get almost all the benefits of a full ORM without many of the drawbacks. This is the core of the &lt;strong&gt;Raw+DC pattern&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example from MongoDB/pymongo. Notice the embedded data classes &lt;code&gt;Address&lt;/code&gt;, &lt;code&gt;Payment&lt;/code&gt;, &lt;code&gt;StatusEntry&lt;/code&gt;, and &lt;code&gt;LineItem&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nd&#34;&gt;@dataclass&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;slots&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;True&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;class&lt;/span&gt; &lt;span class=&#34;nc&#34;&gt;Order&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;nb&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;ObjectId&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;order_number&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;str&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;customer_email&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;str&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;status&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;str&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;total_cents&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;item_count&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;int&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;created_at&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;datetime&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;updated_at&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;datetime&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;shipping_address&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Address&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;payment&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Payment&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;line_items&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;list&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;LineItem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;n&#34;&gt;status_history&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;list&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;StatusEntry&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;We just write a to/from dictionary method to handle the top-level classes and we have a much lighter weight data access layer.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;order_from_doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;nb&#34;&gt;dict&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Order&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Order&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;nb&#34;&gt;id&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;_id&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;order_number&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;order_number&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;customer_email&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;customer_email&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;status&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;status&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;total_cents&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;total_cents&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;item_count&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;item_count&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;created_at&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;created_at&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;updated_at&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;updated_at&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;shipping_address&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Address&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;**&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;shipping_address&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;payment&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;Payment&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;**&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;payment&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]),&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;line_items&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;LineItem&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;**&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;li&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;li&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;line_items&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;status_history&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;StatusEntry&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;**&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sh&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;for&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;sh&lt;/span&gt; &lt;span class=&#34;ow&#34;&gt;in&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;doc&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;status_history&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;I can already hear the critics: &amp;ldquo;You&amp;rsquo;ve just built a worse ORM by hand!&amp;rdquo; Fair point. But this thin conversion layer is exactly the kind of boilerplate that AI coding assistants excel at generating and maintaining. When your dataclass changes, Claude can update &lt;code&gt;order_from_doc&lt;/code&gt; in seconds. It&amp;rsquo;s fully transparent (no framework magic to debug), has zero dependencies beyond the standard library, and you own every line of it.&lt;/p&gt;
&lt;p&gt;Now, before you &lt;strong&gt;close your browser in disgust&lt;/strong&gt;, let me share two bits of data:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Why ORMs go unmaintained (and raw queries don’t)&lt;/li&gt;
&lt;li&gt;Performance benchmarks&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&#34;why-orms-go-unmaintained-and-raw-queries-dont&#34;&gt;Why ORMs go unmaintained (and raw queries don&amp;rsquo;t)&lt;/h2&gt;
&lt;p&gt;This one makes me sad. But I&amp;rsquo;ve been doing Python and working with amazing packages from PyPI long enough to see it over and over.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Some of these ORMs just start to fade into obscurity due to neglect&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;My beloved &lt;a href=&#34;https://github.com/BeanieODM/beanie&#34;&gt;Beanie ODM&lt;/a&gt; has only had a few releases in 2025. &lt;strong&gt;Latest release:&lt;/strong&gt; 4 months ago. &lt;strong&gt;Open issues:&lt;/strong&gt; 91. &lt;strong&gt;Pending PRs:&lt;/strong&gt; 18 (one of which is for a bug I&amp;rsquo;ve been chasing for years now).&lt;/p&gt;
&lt;p&gt;Before Beanie, I was using &lt;a href=&#34;https://github.com/MongoEngine/mongoengine&#34;&gt;mongoengine&lt;/a&gt;. Some of my major apps still run on mongoengine. But it hasn&amp;rsquo;t seen a release in years. It never got updated to async. Sad face.&lt;/p&gt;
&lt;p&gt;Working closer to the DB means you only really go into unmaintained mode if the DB itself does. For example, pymongo had &lt;a href=&#34;https://github.com/mongodb/mongo-python-driver/releases&#34;&gt;a release just a few weeks ago&lt;/a&gt;. Plus, we can always move the raw queries to another library like motor if needed.&lt;/p&gt;
&lt;h2 id=&#34;raw-queries-vs-orm-performance-dataclasses--pymongo-benchmarks&#34;&gt;Raw queries vs ORM performance: dataclasses + pymongo benchmarks&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;The Raw+DC pattern is as fast as pure raw queries and significantly faster than ORMs for large datasets.&lt;/strong&gt; No one would question whether just running dictionaries in/out in raw queries would be faster than doing full entity class serialization and validation on top of that same layer.&lt;/p&gt;
&lt;p&gt;But what actually is the cost of my recommended frankenstein raw queries + data classes? Is it actually worse than the ORMs?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Spoiler&lt;/strong&gt;: low and not at all.&lt;/p&gt;
&lt;p&gt;Check out the graph (click to zoom). Data classes + raw = purple, orange = Beanie, green = mongoengine. The - - - -  blue line is pure pymongo.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.mkennedy.codes/posts/raw-dc-the-orm-pattern-of-2026/odm-beanie-mongoengine-raw-pymono-comparison.png&#34; target=&#34;_blank&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/raw-dc-the-orm-pattern-of-2026/odm-beanie-mongoengine-raw-pymono-comparison.png&#34; alt=&#34;Performance comparison chart: dataclasses + raw pymongo vs Beanie vs mongoengine&#34; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You can see any time you&amp;rsquo;re working with large amounts of data, &lt;strong&gt;Raw+DC&lt;/strong&gt; is actually much faster. Beanie represents well all things considered. MongoEngine is seriously showing its age.&lt;/p&gt;
&lt;p&gt;Performance comparisons are always fraught with pitfalls. But you get a sense of things from the picture nonetheless. You can explore all of this code and run it for yourself. Check out the GitHub repo at:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;ORM vs Raw MongoDB Benchmarks&lt;/strong&gt;: &lt;a href=&#34;https://github.com/mikeckennedy/orm-vs-raw-mongo&#34;&gt;github.com/mikeckennedy/orm-vs-raw-mongo&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll need MongoDB running. Just run this docker command then you can run the demos:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker pull mongo:latest &lt;span class=&#34;o&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; docker run -d --rm -p 0.0.0.0:27017:27017 --name mongosvr mongo:latest
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Then run this in the main repo:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;uv sync
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;uv run main.py run
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;should-you-switch-from-an-orm-to-raw-queries&#34;&gt;Should you switch from an ORM to raw queries?&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s my final case for the &lt;strong&gt;Raw+DC pattern&lt;/strong&gt;, letting Claude (or you) write raw queries, adapted to light-weight dataclasses at the data access layer.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Your query library and code is much less likely to go out of support&lt;/li&gt;
&lt;li&gt;It&amp;rsquo;s very fast&lt;/li&gt;
&lt;li&gt;You still get typed support anywhere you are writing code&lt;/li&gt;
&lt;li&gt;You have fewer (often many fewer) dependencies&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;So &lt;strong&gt;are you going to go Raw+DC&lt;/strong&gt;? Give it a try and see what you think.&lt;/p&gt;
&lt;p&gt;Just be careful to always use safe query practices when using user input. That means parameterized queries for SQL and always convert user input to strings (not dictionaries) in MongoDB.&lt;/p&gt;
&lt;h2 id=&#34;frequently-asked-questions-about-ditching-your-orm&#34;&gt;Frequently asked questions about ditching your ORM&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Is it safe to use raw database queries instead of an ORM?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Yes, as long as you follow safe query practices. For SQL, always use parameterized queries, never string concatenation. For MongoDB, always convert user input to strings (not dictionaries) to prevent query injection. The ORM&amp;rsquo;s safety comes from parameterization, and you can do that yourself.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Are raw database queries faster than ORMs in Python?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Yes. Raw queries with dataclasses (the Raw+DC pattern) perform nearly identically to pure raw dictionary queries and significantly outperform ORMs like Beanie and mongoengine on large datasets. The dataclass conversion overhead is minimal compared to full ORM entity serialization and validation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How do you get type safety without an ORM?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Use Python dataclasses (or Pydantic models) as your entity types. Write a thin conversion layer, &lt;code&gt;to_dict()&lt;/code&gt; and &lt;code&gt;from_doc()&lt;/code&gt; methods, at the data access boundary. Beyond that boundary, all your application code works with fully typed classes, giving you IDE autocompletion and type checker support.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Do AI coding assistants work better with raw queries or ORMs?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;AI assistants like Claude work dramatically better with raw/native query syntax. PyMongo has 53x the downloads of Beanie, and native MongoDB query syntax is shared across Node.js, PHP, and other ecosystems, making training examples 1,000 of times more common. More training data means more accurate, reliable code generation.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Your Terminal Tabs Are Fragile. I Built Something Better.</title>
            <link>https://mkennedy.codes/posts/your-terminal-tabs-are-fragile-i-built-something-better/</link>
            <pubDate>Thu, 05 Feb 2026 18:54:46 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/your-terminal-tabs-are-fragile-i-built-something-better/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;command-book-social.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I built Command Book, a native macOS app that gives your long-running terminal commands a permanent home. Free to download at &lt;a href=&#34;https://commandbookapp.com&#34;&gt;commandbookapp.com&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;terminal-pain-points&#34;&gt;Terminal pain points&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;ve been a terminal power user for over 20 years. And I&amp;rsquo;m done using terminal tabs as a process manager. Here&amp;rsquo;s why.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a familiar tale. You sit down to work in the morning and you have to get a host of apps up and running before you can start coding. Maybe your terminal looks a bit like this.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;-------------------------&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;Terminal&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;-----------------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;python&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;python&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;]&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;python&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;python&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;docker&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;tail&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;zsh&lt;/span&gt;     &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;-----------------------------------------------------------------&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;  &lt;span class=&#34;err&#34;&gt;$&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;python&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;src&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;/&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;app&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;py&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;--&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;reload&lt;/span&gt;                                 &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;  &lt;span class=&#34;n&#34;&gt;Flask&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;app&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;starting&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;up&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;...&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;listening&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;on&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;http&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;//&lt;/span&gt;&lt;span class=&#34;mf&#34;&gt;127.0.0.1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;5000&lt;/span&gt;  &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;  &lt;span class=&#34;o&#34;&gt;...&lt;/span&gt;                                                          &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It&amp;rsquo;s pretty common! And until recently, that&amp;rsquo;s how I started my day.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;First  tab is running a background daemon for processing long running requests.&lt;/li&gt;
&lt;li&gt;Second is my flask app in dev mode (restart on changes)&lt;/li&gt;
&lt;li&gt;Third is another python script watching the design files for changes&lt;/li&gt;
&lt;li&gt;Fourth is docker running the database&lt;/li&gt;
&lt;li&gt;Fifth, tailing the production log from changes I just posted to keep an eye on them&lt;/li&gt;
&lt;li&gt;Sixth, an inactive shell that I can run various ad-hoc commands on&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Each of these commands requires that I remember which working directory to start in (the daemon runs somewhere else than the flask app for example). Getting them up and running is a bit tedious. &lt;strong&gt;Certainly doable, but tedious&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;re working through the day, and you realize some part of your app isn&amp;rsquo;t working. &lt;strong&gt;It crashed&lt;/strong&gt;. Why? You had &lt;code&gt;--reload&lt;/code&gt; set and it dutifully reloaded on file changes. But the code was half-ready. You or Claude wrote the import statement, &lt;code&gt;import new_feature&lt;/code&gt;, but you haven&amp;rsquo;t yet created that module. Or maybe it reloaded while you were mid-function having written &lt;code&gt;def scan_files(&lt;/code&gt; and you get an unmatched brace. Reload fails and you&amp;rsquo;re hunting through terminal tabs for which part stopped working and needs to be restarted.&lt;/p&gt;
&lt;h2 id=&#34;what-i-actually-needed&#34;&gt;What I actually needed&lt;/h2&gt;
&lt;p&gt;Terminals are great for interactive work, exploration, quick commands. But they are terrible as a &lt;em&gt;process manager&lt;/em&gt;. What I wanted was a command/process manager for long running commands currently living in my terminal.&lt;/p&gt;
&lt;p&gt;I wanted them to be reproducible, instant, reliable and auto restarting if code changes, not just reloading unless the code gets out of sync.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I wanted a terminal &lt;em&gt;companion&lt;/em&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&#34;introducing-command-book&#34;&gt;Introducing Command Book&lt;/h2&gt;
&lt;p&gt;I didn&amp;rsquo;t find this app. So I built it. After 10 years of running Talk Python, Talk Python Training, and a handful of other production apps, I knew exactly what I needed, and what was missing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Introducing &lt;a href=&#34;https://commandbookapp.com&#34;&gt;Command Book&lt;/a&gt;&lt;/strong&gt;: A native macOS app built with SwiftUI &amp;ndash; no Electron, no Chromium, just a fast, lightweight experience that feels at home on your Mac.&lt;/p&gt;
&lt;p&gt;Command Book is for people like me: &lt;strong&gt;Developers juggling a web server&lt;/strong&gt;, a database, a couple of background workers, and a log tail every day. &lt;strong&gt;Data scientists who kick off long training runs&lt;/strong&gt; and need to know when they crash. Anyone who&amp;rsquo;s tired of Electron apps chewing through 500 MB of RAM to do something a 21 MB native app handles just fine.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;command-book-hero.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;how-it-works&#34;&gt;How it works&lt;/h2&gt;
&lt;p&gt;&amp;mdash;&amp;mdash;&amp;mdash; &lt;strong&gt;Save a command once, run it forever&lt;/strong&gt; &amp;mdash;&amp;mdash;&amp;mdash;&lt;/p&gt;
&lt;p&gt;Create a command, specify a working directory, env vars, pre-commands like &lt;code&gt;git pull&lt;/code&gt;, and the main command to run. For example, to work on talkpython.fm I have this command:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Command&lt;/strong&gt;: &lt;code&gt;python talk-python/web_app.py --reload&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Working directory&lt;/strong&gt;: &lt;code&gt;~/github/talk-python-web/&lt;/code&gt;&lt;br&gt;
&lt;strong&gt;Pre-command&lt;/strong&gt;: &lt;code&gt;git pull&lt;/code&gt;&lt;br&gt;&lt;/p&gt;
&lt;p&gt;&amp;mdash;&amp;mdash;&amp;mdash; &lt;strong&gt;Auto-restart on crash&lt;/strong&gt; &amp;mdash;&amp;mdash;&amp;mdash;&lt;/p&gt;
&lt;p&gt;Command Book can go into Honey Badger mode to keep you productive. You can check a box to always restart the command when it crashes. This is perfect for that &lt;code&gt;dev-server --reload&lt;/code&gt; command, except when something goes wrong like an unmatched brace then reload becomes crashes. Now Command Book will restart it until the code is fixed.&lt;/p&gt;
&lt;p&gt;It is configurable with a delay so it doesn&amp;rsquo;t go too Honey Badger.&lt;/p&gt;
&lt;p&gt;&amp;mdash;&amp;mdash;&amp;mdash; &lt;strong&gt;Keyboard-first with ⌘K&lt;/strong&gt; &amp;mdash;&amp;mdash;&amp;mdash;&lt;/p&gt;
&lt;p&gt;Command Book is a GUI, but one built for developers. So it&amp;rsquo;s very keyboard focused (it also comes with a CLI interface). Many actions are keyboard first. This is most evident with our command palette. Pressing ⌘K, you can search, run, and create saved or ad-hoc commands.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;command-palette.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&amp;mdash;&amp;mdash;&amp;mdash; &lt;strong&gt;URL detection&lt;/strong&gt; &amp;mdash;&amp;mdash;&amp;mdash;&lt;/p&gt;
&lt;p&gt;Another papercut Command Book looks to solve is getting back to your dev server. Did you click the link when the server started but absent-mindedly close the browser an hour later? Now the terminal output has scrolled back 1,000 lines and you have to hunt for the URL to get back?&lt;/p&gt;
&lt;p&gt;This is a common problem with Flask, Django, Node, Jupyter Notebooks, and many more.&lt;/p&gt;
&lt;p&gt;So when a command runs, &lt;strong&gt;Command Book grabs the first few urls and keeps them accessible at the top of the command output&lt;/strong&gt;. That way, you can always get back to the running app regardless of how much output has streamed by.&lt;/p&gt;
&lt;p&gt;&amp;mdash;&amp;mdash;&amp;mdash; &lt;strong&gt;CLI integration&lt;/strong&gt; &amp;mdash;&amp;mdash;&amp;mdash;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;command-book-cli.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Command Book&amp;rsquo;s GUI lets you configure commands with precision: working directories, pre-commands, environment variables, auto-restart behavior, and more. The CLI brings all of that to your terminal with zero extra setup.&lt;/p&gt;
&lt;p&gt;Instead of remembering the full incantation, you just run &lt;code&gt;commandbook run talk-python-dev&lt;/code&gt; and it does exactly what the GUI would do - same directory, same env vars, same everything.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;$ commandbook run talk-python-dev
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;download-command-book-for-free&#34;&gt;Download Command Book for free&lt;/h2&gt;
&lt;p&gt;Command Book comes with a free personal license as well as a paid, pro edition. You should definitely be able to see if the app is useful for you with the personal edition. To get started, just &lt;strong&gt;download Command Book for free&lt;/strong&gt; at:&lt;/p&gt;
&lt;p&gt;      &lt;strong&gt;&lt;a href=&#34;https://commandbookapp.com/download&#34;&gt;commandbookapp.com/download&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If you do decide to upgrade, it&amp;rsquo;s just $14.99 one-time. No subscription. No account. No tracking.&lt;/p&gt;
&lt;p&gt;There&amp;rsquo;s no VC runway behind this app, no enterprise upsell waiting in the wings, and no tracker phoning home. Just a tool that gets better because users support it directly. Zero enshittification. If Command Book saves you from rebuilding your terminal setup even once, it&amp;rsquo;s paid for itself.&lt;/p&gt;
&lt;h2 id=&#34;feedback-and-roadmap&#34;&gt;Feedback and roadmap&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;d really appreciate it if you gave it a look, shared it with your team, and sent me feedback. Feel free to &lt;a href=&#34;mailto:mikeckennedy@gmail.com&#34;&gt;email me with feedback&lt;/a&gt;. Consider &lt;a href=&#34;https://commandbookapp.com/newsletter&#34;&gt;signing up&lt;/a&gt; for announcements (the newsletter) too.&lt;/p&gt;
&lt;p&gt;The app is still getting final touches. So community input can truly drive the direction of what comes next. For example, a Windows version is under consideration and more features planned.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>It&#39;s not vibe coding: Agentic engineering</title>
            <link>https://mkennedy.codes/posts/its-not-vibe-coding-agentic-engineering/</link>
            <pubDate>Thu, 05 Feb 2026 16:21:01 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/its-not-vibe-coding-agentic-engineering/</guid>
            <description>&lt;p&gt;I&amp;rsquo;ve been bewildered by the wide range of experiences that software developers observe when working with AI. This has led many of us to outright reject AI for coding.&lt;/p&gt;
&lt;p&gt;There are reasons for this variance.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Skeptics want to lightly dip their foot in the water to see what it&amp;rsquo;s about&lt;/strong&gt;. This manifests as using the cheapest or free models and &amp;ldquo;just send it&amp;rdquo; styles of programming. Then they lament that what comes out is not what they would have built nor is it what they wanted.&lt;/p&gt;
&lt;p&gt;Meanwhile, professional developers adopting agentic AI for coding operate differently. They see spending $100+/mo on AI tools as a great bargain. They &lt;strong&gt;spend hours planning and refining and decomposing a problem&lt;/strong&gt; before they send a single request to their fancy AIs. Code carefully matching their request is often the outcome.&lt;/p&gt;
&lt;p&gt;Is it a surprise they are getting different results? I&amp;rsquo;ve seen both styles and it&amp;rsquo;s no surprise to me.&lt;/p&gt;
&lt;p&gt;These two experiences often get lumped under the same term: Vibe Coding.&lt;/p&gt;
&lt;p&gt;They are not the same.&lt;/p&gt;
&lt;p&gt;Today I ran across an essay by Addy Osmani, &amp;ldquo;&lt;a href=&#34;https://addyosmani.com/blog/agentic-engineering/&#34;&gt;Agentic Engineering&lt;/a&gt;.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;Addy really nails this difference and spells out a term that I&amp;rsquo;m keen to adopt: &lt;strong&gt;Agentic Engineering&lt;/strong&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Vibe coding means &lt;strong&gt;going with the vibes&lt;/strong&gt; and &lt;strong&gt;not reviewing the code&lt;/strong&gt;. That’s the defining characteristic. You prompt, you accept, you run it, you see if it works. If it doesn’t, you paste the error back and try again. You keep prompting. The human is a prompt DJ, not an engineer.&lt;/p&gt;
&lt;p&gt;&amp;hellip;&lt;/p&gt;
&lt;p&gt;Here’s the thing: a lot of experienced engineers are now getting massive productivity gains from AI - 2x, 5x, sometimes more - while maintaining code quality. But the way they work looks nothing like vibe coding. They’re writing specs before prompting. They’re reviewing every diff. They’re running test suites. They’re treating the AI like a fast but unreliable junior developer who needs constant oversight. I’ve personally liked “AI-assisted engineering” and have talked about how this describes that end of the spectrum where the human remains in the loop.&lt;/p&gt;
&lt;p&gt;&amp;hellip;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;It draws a clean line.&lt;/strong&gt; Vibe coding = YOLO. Agentic engineering = AI does the implementation, human owns the architecture, quality, and correctness. The terminology itself enforces the distinction.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Give the &lt;a href=&#34;https://addyosmani.com/blog/agentic-engineering/&#34;&gt;full article a read&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;That last quote is important. Many devs dread AI because they don&amp;rsquo;t want to be a full time reviewer for marginal code. But if &amp;ldquo;AI does the implementation, human owns the architecture, quality, and correctness&amp;rdquo;, &lt;strong&gt;that sounds like senior developers&amp;rsquo; job descriptions&lt;/strong&gt; to me.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s something to think about.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Blocking AI crawlers might be a bad idea</title>
            <link>https://mkennedy.codes/posts/why-hiding-from-ai-crawlers-is-a-bad-idea/</link>
            <pubDate>Wed, 21 Jan 2026 12:04:53 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/why-hiding-from-ai-crawlers-is-a-bad-idea/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/why-hiding-from-ai-crawlers-is-a-bad-idea/blocked-door-hero.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; You can&amp;rsquo;t stop people from using AI or put it back in the box. Blocking AI crawlers feels satisfying but just makes you invisible to users who rely on AI recommendations. This post covers the tradeoffs content creators face in an AI world and why embracing AI integrations beats hiding from them.&lt;/p&gt;
&lt;p&gt;Over at &lt;a href=&#34;https://talkpython.fm&#34;&gt;Talk Python To Me&lt;/a&gt;, I added a couple of deep AI integrations. You can read about them here at &lt;a target=&#34;_blank&#34; href=&#34;https://talkpython.fm/blog/posts/announcing-talk-python-ai-integrations/&#34;&gt;talkpython.fm/blog/posts/announcing-talk-python-ai-integrations/&lt;/a&gt;. A couple of folks in the community asked &lt;strong&gt;what I thought about how embracing AI consumption of our content would affect us&lt;/strong&gt;. Or, how in the light of &lt;a target=&#34;_blank&#34; href=&#34;https://www.reddit.com/r/theprimeagen/comments/1q7awhk/tailwind_is_in_deep_trouble/&#34;&gt;the tailwind situation&lt;/a&gt;, it might even undermine us.&lt;/p&gt;
&lt;h2 id=&#34;wait-what-happened-to-tailwind&#34;&gt;Wait, what happened to Tailwind?&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re not familiar with the Tailwind situation, the TLDR is that their usage has gone up 6x over the last year while the revenue has fallen to one-fifth of what it was a year prior. Basically, AI is using and recommending Tailwind like crazy, which counterintuitively has destroyed their web traffic, and hence reduced people seeing and upgrading to their pro offers.&lt;/p&gt;
&lt;h2 id=&#34;fear-of-ai-ingestion&#34;&gt;Fear of AI ingestion&lt;/h2&gt;
&lt;p&gt;I know many content creators (writers, podcasters, and so on), are pretty frustrated with AI. I totally get it. We all work very hard to create content that gets ingested by AI. Then people ask questions of the AI. AI uses our content to answer the question, usually without referencing our work.&lt;/p&gt;
&lt;p&gt;This has led a lot of people to think that maybe they should block AI from even reading what they&amp;rsquo;re doing. Here are some examples:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&#34;https://www.reddit.com/r/TechSEO/comments/1nj82s7/how_can_we_stop_ai_to_read_our_website_information/&#34;&gt;Reddit: How can we stop AI to read our website information&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=grCBm0Gq0Zw&#34;&gt;Not On My Watch! 🚫 Block AI From Training On Your Website&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;strong&gt;I get the frustration and the ick factor&lt;/strong&gt;. But I fear that blocking AI is going to end up in a similar situation as if you decided to block Google in 1998. For sure, those suckers will not be able to train on your content. But as users rely more and more on AI for recommendations, you will vanish from awareness in much the same way as if you had vanished from Google search results.&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;re welcome to block AI crawlers, and most of them will respect it. But that won&amp;rsquo;t make AI go away, nor will it make users stop using AI.&lt;/p&gt;
&lt;h2 id=&#34;if-youre-going-to-be-in-ai-results&#34;&gt;If you&amp;rsquo;re going to be in AI results&amp;hellip;&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re going to be in AI results anyway,  you should want the very best experience for current and potential users.&lt;/p&gt;
&lt;p&gt;First, you do want to be recommended. After all, that&amp;rsquo;s how I sold this to you, right? The new Google.  It&amp;rsquo;s a bit of the Wild West still, but there are tools that you can use to check how you appear in AI results. Here&amp;rsquo;s an example from &lt;a href=&#34;https://productrank.ai/topic/python-podcast&#34;&gt;ProductRank.ai&lt;/a&gt; for Python Podcasts.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/why-hiding-from-ai-crawlers-is-a-bad-idea/productrank-ai-podcasts.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Talk Python to Me is the number one recommended podcast&lt;/strong&gt; when users ask ChatGPT, Claude, or Perplexity for a Python podcast. And my other podcast, Python Bytes, is number three. Honestly, I&amp;rsquo;m thrilled at this result. If anyone is talking to these AIs and says, &amp;ldquo;Hey, I would love a podcast to listen to. Do you have recommendations?&amp;rdquo; Here you go.&lt;/p&gt;
&lt;p&gt;What would the experience be if I got mad at AI and blocked it? I simply would be invisible to all of these users wanting to learn about Python podcasts. Whatever effect AI is going to have on Talk Python in the future, disappearing from its recommendations is not going to make it better.&lt;/p&gt;
&lt;h2 id=&#34;what-will-make-it-better&#34;&gt;What will make it better?&lt;/h2&gt;
&lt;p&gt;How can we improve our situation of, say, decreased traffic or fewer customers? And I&amp;rsquo;m not even saying that that has actually happened for Talk Python, the podcast. It&amp;rsquo;s still going strong, just speaking generally.&lt;/p&gt;
&lt;p&gt;Obviously, getting recommended more is key, so see above. Offering a better experience to your users when they ask questions about topics your content covers or even specifically about your content in particular.&lt;/p&gt;
&lt;p&gt;That is why I added the AI integrations to Talk Python. Not so that AI could undermine us even more, but so that our users would get more value from years and years of content we&amp;rsquo;ve already created. Counting the transcripts, deep dives, and episode pages, &lt;strong&gt;we have well over 7 million words of content at &lt;a href=&#34;https://talkpython.fm&#34;&gt;talkpython.fm&lt;/a&gt;.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;If users want to ask AI about that content, I want AI to give them as good and accurate of a response as possible. Here are some super frustrating experiences that you sometimes get from AI.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Outdated data&lt;/strong&gt;: My training only goes back to the summer of 2025, so here are the most recent results according to that.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Imaginary data&lt;/strong&gt;: My mistake. You&amp;rsquo;re absolutely right! President Obama did appear on Talk Python back in 2008! Here&amp;rsquo;s a link to that interview &amp;hellip;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Out of date data, and especially, hallucinated data is why so many people are creeped out by AI and want to use AI less. The AI integrations that I created significantly reduced this by providing real-time information to AI, as well as tools to verify what it thinks exists, actually does, and link back to it.&lt;/p&gt;
&lt;h2 id=&#34;how-it-looks-with-ai-integrations&#34;&gt;How it looks with AI integrations&lt;/h2&gt;
&lt;p&gt;Check out this response from Claude when I asked &lt;strong&gt;what the latest 5 episodes of Talk Python To Me are&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/why-hiding-from-ai-crawlers-is-a-bad-idea/latest-episodes-mcp.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;This is real-time information. If I publish a new episode, or heck, even change the title of an older one, and ask this question again in a separate chat conversation 10 seconds later I will get up-to-date information.&lt;/p&gt;
&lt;p&gt;There are many tools that the AIs can use that we&amp;rsquo;ve provided so that our content is more accurate, more up-to-date, and more useful. This can only mean that AI will recommend our content more and link back to it more accurately.&lt;/p&gt;
&lt;p&gt;Here is another example which I posed in a totally fresh chat: &lt;strong&gt;Can you recommend a Python course on agentic AI and cursor?&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/why-hiding-from-ai-crawlers-is-a-bad-idea/recommend-course.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Notice, I did not ask Claude to recommend a Talk Python course on Agentic AI. I simply asked it for any Python-based Agentic AI and Cursor course. Now, I have installed the &lt;a href=&#34;https://talkpython.fm/ai-integration&#34;&gt;Talk Python MCP server&lt;/a&gt;, so that probably influenced Claude, but still this is very powerful.&lt;/p&gt;
&lt;h2 id=&#34;blocking-ai-crawlers-might-be-a-bad-idea&#34;&gt;Blocking AI crawlers might be a bad idea&lt;/h2&gt;
&lt;p&gt;This is exactly why I think blocking AI crawlers is probably a bad idea. Yes, it&amp;rsquo;ll make you feel great if you&amp;rsquo;re pissed at AI: &lt;u&gt;&amp;ldquo;You&amp;rsquo;ll show them!&amp;quot;&lt;/u&gt; But it likely will not further your cause in sharing ideas, gaining awareness, and much more.&lt;/p&gt;
&lt;p&gt;So for all of you who have asked me why I&amp;rsquo;m willing to make Talk Python more accessible to AI in the immediate shadow of Tailwind and Stack Overflow suffering greatly from AI, this is why.&lt;/p&gt;
&lt;p&gt;Thanks for checking out my content, whether you got here through an RSS reader, web search, or maybe even an AI recommendation. ;)&lt;/p&gt;
&lt;p&gt;Cheers, Michael.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Always activate the venv (a shell script)</title>
            <link>https://mkennedy.codes/posts/always-activate-the-venv-a-shell-script/</link>
            <pubDate>Wed, 07 Jan 2026 10:15:37 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/always-activate-the-venv-a-shell-script/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Add &lt;a href=&#34;https://gist.github.com/mikeckennedy/010a96dc6a406242d5b49d12e5d51c22&#34;&gt;this Zsh script&lt;/a&gt; to your &lt;code&gt;.zshrc&lt;/code&gt; to automatically activate/deactivate &lt;code&gt;venv&lt;/code&gt; or &lt;code&gt;.venv&lt;/code&gt; as you navigate directories. It includes a whitelist feature for security.&lt;/p&gt;
&lt;p&gt;Using a virtual environment is a well-known and important best practice for working on Python projects that use third-party dependencies, i.e. pretty much every Python project out there.&lt;/p&gt;
&lt;p&gt;But it&amp;rsquo;s a hassle to make sure you always have the right one activated, that you have one activated, and checking if there&amp;rsquo;s even one present. Maybe you haven&amp;rsquo;t created one for this project yet, yet where you&amp;rsquo;ve checked it out from source control.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This post shares a simple shell script that will automatically find and activate your Python virtual environment.&lt;/strong&gt; (* See security warning at the end.)&lt;/p&gt;
&lt;p&gt;As you navigate through your source code, it seeks out virtual enviornemnts (named &lt;code&gt;venv&lt;/code&gt; or &lt;code&gt;.venv&lt;/code&gt;) and turns them on and off as in enter/leave that tree of your file system.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/always-activate-the-venv-a-shell-script/auto-venv-example.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;why-not-just-direnv&#34;&gt;Why not just direnv?&lt;/h2&gt;
&lt;p&gt;Because direnv + Python seems to be a mess and it&amp;rsquo;s a sledgehammer when all you need is a &lt;a href=&#34;https://i5.walmartimages.com/asr/6b07d81d-12f1-49c0-ad75-8793619029c1.32f1b2242490754f517cc9ba855cf86f.jpeg?odnHeight=768&amp;amp;odnWidth=768&amp;amp;odnBg=FFFFFF&#34;&gt;thumbtack&lt;/a&gt;. Check out the still open issue &lt;a href=&#34;https://github.com/direnv/direnv/issues/1264&#34;&gt;Activate python venv by default? #1264&lt;/a&gt; in the direnv project to get a sense.&lt;/p&gt;
&lt;h2 id=&#34;installing-the-script&#34;&gt;Installing the script&lt;/h2&gt;
&lt;p&gt;Just save the script below to &lt;code&gt;venv-auto-activate.sh&lt;/code&gt; and then add include it in your &lt;code&gt;.zshrc&lt;/code&gt; file with &lt;code&gt;source &amp;quot;venv-auto-activate.sh&amp;quot;&lt;/code&gt;. Note that this only works on ZSH (not Bash) due to the use of the &lt;code&gt;chpwd&lt;/code&gt; hook. Luckily ZSH is the default shell on macOS. If you use another shell, maybe this post will inspire you to create something similar.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the shell script:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-plaintext&#34; data-lang=&#34;plaintext&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;# Virtual Environment Auto-Activation
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;# ===================================
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;# Automatically activates/deactivates Python virtual environments
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;# when changing directories
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;# Please read &amp;#34;A small security warning&amp;#34; on the post before continuing.
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;# Auto-activate virtual environment for any project with a venv directory
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;function chpwd() {
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    # Function to find venv directory in current path or parent directories
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    # Prefers &amp;#39;venv&amp;#39; over &amp;#39;.venv&amp;#39; if both exist
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    local find_venv() {
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        local dir=&amp;#34;$PWD&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        while [[ &amp;#34;$dir&amp;#34; != &amp;#34;/&amp;#34; ]]; do
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            if [[ -d &amp;#34;$dir/venv&amp;#34; &amp;amp;&amp;amp; -f &amp;#34;$dir/venv/bin/activate&amp;#34; ]]; then
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                echo &amp;#34;$dir/venv&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                return 0
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            elif [[ -d &amp;#34;$dir/.venv&amp;#34; &amp;amp;&amp;amp; -f &amp;#34;$dir/.venv/bin/activate&amp;#34; ]]; then
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                echo &amp;#34;$dir/.venv&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                return 0
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            fi
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            dir=&amp;#34;$(dirname &amp;#34;$dir&amp;#34;)&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        done
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        return 1
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    }
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    local venv_path
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    venv_path=$(find_venv)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    if [[ -n &amp;#34;$venv_path&amp;#34; ]]; then
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        # Normalize paths for comparison (handles symlinks and path differences)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        # Use zsh :A modifier to resolve paths without triggering chpwd recursively
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        local normalized_venv_path=&amp;#34;${venv_path:A}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        local normalized_current_venv=&amp;#34;&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        if [[ -n &amp;#34;${VIRTUAL_ENV:-}&amp;#34; ]]; then
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            normalized_current_venv=&amp;#34;${VIRTUAL_ENV:A}&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        fi
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        # We found a venv, check if it&amp;#39;s already active
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        if [[ &amp;#34;$normalized_current_venv&amp;#34; != &amp;#34;$normalized_venv_path&amp;#34; ]]; then
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            # Deactivate current venv if different
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            if [[ -n &amp;#34;${VIRTUAL_ENV:-}&amp;#34; ]] &amp;amp;&amp;amp; type deactivate &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                deactivate
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            fi
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            # Activate the found venv
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            source &amp;#34;$venv_path/bin/activate&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            local project_name=$(basename &amp;#34;$(dirname &amp;#34;$venv_path&amp;#34;)&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            echo &amp;#34;🐍 Activated virtual environment \033[95m$project_name\033[0m.&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        fi
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    else
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        # No venv found, deactivate if we have one active
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        if [[ -n &amp;#34;${VIRTUAL_ENV:-}&amp;#34; ]] &amp;amp;&amp;amp; type deactivate &amp;gt;/dev/null 2&amp;gt;&amp;amp;1; then
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            local project_name=$(basename &amp;#34;$(dirname &amp;#34;${VIRTUAL_ENV}&amp;#34;)&amp;#34;)
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            deactivate
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            echo &amp;#34;🔒 Deactivated virtual environment \033[95m$project_name\033[0m.&amp;#34;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        elif [[ -n &amp;#34;${VIRTUAL_ENV:-}&amp;#34; ]]; then
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            # VIRTUAL_ENV is set but deactivate function is not available
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            # This can happen when opening a new shell with VIRTUAL_ENV from previous session
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            unset VIRTUAL_ENV
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        fi
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    fi
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;}
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;# Run the chpwd function when the shell starts
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;chpwd
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;a-small-security-warning&#34;&gt;A small security warning&lt;/h2&gt;
&lt;p&gt;While the script does not do anything shady, it does run code on your behalf. Normally, you&amp;rsquo;re using the terminal to go in and out of your projects with your virtual environments. In theory, someone could trick you into checking out or opening a directory structure that has a script named &lt;code&gt;venv/bin/activate&lt;/code&gt; which runs via this script when you navigate into that tree structure.&lt;/p&gt;
&lt;p&gt;If you interact with a lot of projects that you do not control or trust, maybe you want to have this feature as something you can turn on and off with a shell command command. Maybe update the script to give you a confirmation: Are you sure you want to activate SCRIPT_PATH? Something like that. As usual, it&amp;rsquo;s your call. Use at your own risk.&lt;/p&gt;
&lt;h2 id=&#34;a-security-fix&#34;&gt;A security fix&lt;/h2&gt;
&lt;p&gt;After posting this article, a reader sent in a great idea:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;What I did for the security concern was implement a whitelist. If it finds a venv but it’s not in the whitelist it informs me. Then I have a whitelist-venv command that will add that repo to the whitelist. &amp;ndash; Scott H.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Amazing idea Scott. I think it balances safety with easy-of-use. I updated the script with a partner Python utlity to validate and manage this whitelist. Check it out at this Github Gist:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://gist.github.com/mikeckennedy/010a96dc6a406242d5b49d12e5d51c22&#34;&gt;Security-aware always activate the venv for Python&lt;/a&gt;&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Python Numbers Every Programmer Should Know</title>
            <link>https://mkennedy.codes/posts/python-numbers-every-programmer-should-know/</link>
            <pubDate>Wed, 31 Dec 2025 11:49:00 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/python-numbers-every-programmer-should-know/</guid>
            <description>&lt;p&gt;&lt;strong&gt;There are numbers every Python programmer should know&lt;/strong&gt;. For example, how fast or slow is it to add an item to a list in Python? What about opening a file? Is that less than a millisecond? Is there something that makes that slower than you might have guessed? If you have a performance sensitive algorithm, which data structure should you use? How much memory does a floating point number use? What about a single character or the empty string? How fast is FastAPI compared to Django?&lt;/p&gt;
&lt;p&gt;I wanted to take a moment and write down performance numbers specifically focused on Python developers. Below you will find an extensive table of such values. They are grouped by category. And I provided a couple of graphs for the more significant analysis below the table.&lt;/p&gt;
&lt;p&gt;Acknowledgements: Inspired by &lt;a href=&#34;https://gist.github.com/jboner/2841832&#34;&gt;Latency Numbers Every Programmer Should Know&lt;/a&gt; and similar resources.&lt;/p&gt;
&lt;h3 id=&#34;source-code-for-the-benchmarks&#34;&gt;Source code for the benchmarks&lt;/h3&gt;
&lt;p&gt;This article is posted without any code. I encourage you to dig into the benchmarks. The code is available on GitHub at:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/mikeckennedy/python-numbers-everyone-should-know&#34;&gt;https://github.com/mikeckennedy/python-numbers-everyone-should-know&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;-system-information&#34;&gt;📊 System Information&lt;/h3&gt;
&lt;p&gt;The benchmarks were run on the sytem described in this table. While yours may be faster or slower, the most important thing to consider is relative comparisons.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Property&lt;/th&gt;
          &lt;th&gt;Value&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Python Version&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;CPython 3.14.2&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Hardware&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;Mac Mini M4 Pro&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Platform&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;macOS Tahoe (26.2)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Processor&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;ARM&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;CPU Cores&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;14 physical / 14 logical&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;RAM&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;24 GB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;strong&gt;Timestamp&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;2025-12-30&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;tldr-python-numbers&#34;&gt;TL;DR; Python Numbers&lt;/h2&gt;
&lt;p&gt;This first version is a quick &amp;ldquo;pyramid&amp;rdquo; of growing time/size for common Python ops. There is &lt;strong&gt;much more&lt;/strong&gt; detail below.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Python Operation Latency Numbers (the pyramid)&lt;/strong&gt;&lt;/p&gt;
&lt;pre style=&#34;font-size: .75em;&#34;&gt;
Attribute read (obj.x)                              14   ns
Dict key lookup                                     22   ns              1.5x attr
Function call (empty)                               22   ns
List append                                         29   ns              2x attr
f-string formatting                                 65   ns              3x function
Exception raised + caught                          140   ns             10x attr
orjson.dumps() complex object                      310   ns        0.3 μs
json.loads() simple object                         714   ns        0.7 μs   2x orjson
sum() 1,000 integers                             1,900   ns        1.9 μs   3x json
SQLite SELECT by primary key                     3,600   ns        3.6 μs   5x json
Iterate 1,000-item list                          7,900   ns        7.9 μs   2x SQLite read
Open and close file                              9,100   ns        9.1 μs   2x SQLite read
asyncio run_until_complete (empty)              28,000   ns         28 μs   3x file open
Write 1KB file                                  35,000   ns         35 μs   4x file open
MongoDB find_one() by _id                      121,000   ns        121 μs   3x write 1KB
SQLite INSERT (with commit)                    192,000   ns        192 μs   5x write 1KB
Write 1MB file                                 207,000   ns        207 μs   6x write 1KB
import json                                  2,900,000   ns      2,900 μs   3 ms   15x write 1MB
import asyncio                              17,700,000   ns     17,700 μs  18 ms    6x import json
import fastapi                             104,000,000   ns    104,000 μs 104 ms    6x import asyncio
&lt;/pre&gt;
&lt;p&gt;&lt;strong&gt;Python Memory Numbers (the pyramid)&lt;/strong&gt;&lt;/p&gt;
&lt;pre style=&#34;font-size: .75em;&#34;&gt;
Float                                               24   bytes
Small int (cached -5 to 256)                        28   bytes
Empty string                                        41   bytes
Empty list                                          56   bytes          2x int
Empty dict                                          64   bytes          2x int
Empty set                                          216   bytes          8x int
__slots__ class (5 attrs)                          212   bytes          8x int
Regular class (5 attrs)                            694   bytes         25x int
List of 1,000 ints                              36,856   bytes         36 KB
Dict of 1,000 items                             92,924   bytes         91 KB         
List of 1,000 __slots__ instances              220,856   bytes        216 KB
List of 1,000 regular instances                309,066   bytes        302 KB         1.4x slots list
Empty Python process                        16,000,000   bytes         16 MB
&lt;/pre&gt;
&lt;h2 id=&#34;python-numbers-you-should-know-detailed-version&#34;&gt;Python numbers you should know (detailed version)&lt;/h2&gt;
&lt;p&gt;Here is a deeper table comparing many more details.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Category&lt;/th&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
          &lt;th&gt;Memory&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;#memory-costs&#34;&gt;&lt;strong&gt;💾 Memory&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Empty Python process&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;15.77 MB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Empty string&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;41 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;100-char string&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;141 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Small int (-5 to 256)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;28 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Large int&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;28 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Float&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;24 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Empty list&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;56 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;List with 1,000 ints&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;36.0 KB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;List with 1,000 floats&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;32.1 KB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Empty dict&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;64 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Dict with 1,000 items&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;90.7 KB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Empty set&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;216 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Set with 1,000 items&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;59.6 KB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Regular class instance (5 attrs)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;694 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;__slots__&lt;/code&gt; class instance (5 attrs)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;212 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;List of 1,000 regular class instances&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;301.8 KB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;List of 1,000 &lt;code&gt;__slots__&lt;/code&gt; class instances&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;215.7 KB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;dataclass instance&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;694 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;namedtuple instance&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;228 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;#basic-operations&#34;&gt;&lt;strong&gt;⚙️ Basic Ops&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Add two integers&lt;/td&gt;
          &lt;td&gt;19.0 ns (52.7M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Add two floats&lt;/td&gt;
          &lt;td&gt;18.4 ns (54.4M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;String concatenation (small)&lt;/td&gt;
          &lt;td&gt;39.1 ns (25.6M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;f-string formatting&lt;/td&gt;
          &lt;td&gt;64.9 ns (15.4M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;.format()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;103 ns (9.7M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;%&lt;/code&gt; formatting&lt;/td&gt;
          &lt;td&gt;89.8 ns (11.1M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;List append&lt;/td&gt;
          &lt;td&gt;28.7 ns (34.8M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;List comprehension (1,000 items)&lt;/td&gt;
          &lt;td&gt;9.45 μs (105.8k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Equivalent for-loop (1,000 items)&lt;/td&gt;
          &lt;td&gt;11.9 μs (83.9k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;#collection-access-and-iteration&#34;&gt;&lt;strong&gt;📦 Collections&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Dict lookup by key&lt;/td&gt;
          &lt;td&gt;21.9 ns (45.7M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Set membership check&lt;/td&gt;
          &lt;td&gt;19.0 ns (52.7M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;List index access&lt;/td&gt;
          &lt;td&gt;17.6 ns (56.8M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;List membership check (1,000 items)&lt;/td&gt;
          &lt;td&gt;3.85 μs (259.6k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;len()&lt;/code&gt; on list&lt;/td&gt;
          &lt;td&gt;18.8 ns (53.3M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Iterate 1,000-item list&lt;/td&gt;
          &lt;td&gt;7.87 μs (127.0k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Iterate 1,000-item dict&lt;/td&gt;
          &lt;td&gt;8.74 μs (114.5k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;sum()&lt;/code&gt; of 1,000 ints&lt;/td&gt;
          &lt;td&gt;1.87 μs (534.8k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;#class-and-object-attributes&#34;&gt;&lt;strong&gt;🏷️ Attributes&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Read from regular class&lt;/td&gt;
          &lt;td&gt;14.1 ns (70.9M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Write to regular class&lt;/td&gt;
          &lt;td&gt;15.7 ns (63.6M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Read from &lt;code&gt;__slots__&lt;/code&gt; class&lt;/td&gt;
          &lt;td&gt;14.1 ns (70.7M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Write to &lt;code&gt;__slots__&lt;/code&gt; class&lt;/td&gt;
          &lt;td&gt;16.4 ns (60.8M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Read from &lt;code&gt;@property&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;19.0 ns (52.8M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;getattr()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;13.8 ns (72.7M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;hasattr()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;23.8 ns (41.9M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;#json-and-serialization&#34;&gt;&lt;strong&gt;📄 JSON&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;json.dumps()&lt;/code&gt; (simple)&lt;/td&gt;
          &lt;td&gt;708 ns (1.4M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;json.loads()&lt;/code&gt; (simple)&lt;/td&gt;
          &lt;td&gt;714 ns (1.4M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;json.dumps()&lt;/code&gt; (complex)&lt;/td&gt;
          &lt;td&gt;2.65 μs (376.8k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;json.loads()&lt;/code&gt; (complex)&lt;/td&gt;
          &lt;td&gt;2.22 μs (449.9k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;orjson.dumps()&lt;/code&gt; (complex)&lt;/td&gt;
          &lt;td&gt;310 ns (3.2M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;orjson.loads()&lt;/code&gt; (complex)&lt;/td&gt;
          &lt;td&gt;839 ns (1.2M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;ujson.dumps()&lt;/code&gt; (complex)&lt;/td&gt;
          &lt;td&gt;1.64 μs (611.2k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;msgspec&lt;/code&gt; encode (complex)&lt;/td&gt;
          &lt;td&gt;445 ns (2.2M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Pydantic &lt;code&gt;model_dump_json()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;1.54 μs (647.8k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Pydantic &lt;code&gt;model_validate_json()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;2.99 μs (334.7k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;#web-frameworks&#34;&gt;&lt;strong&gt;🌐 Web Frameworks&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Flask (return JSON)&lt;/td&gt;
          &lt;td&gt;16.5 μs (60.7k req/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Django (return JSON)&lt;/td&gt;
          &lt;td&gt;18.1 μs (55.4k req/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;FastAPI (return JSON)&lt;/td&gt;
          &lt;td&gt;8.63 μs (115.9k req/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Starlette (return JSON)&lt;/td&gt;
          &lt;td&gt;8.01 μs (124.8k req/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Litestar (return JSON)&lt;/td&gt;
          &lt;td&gt;8.19 μs (122.1k req/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;#file-io&#34;&gt;&lt;strong&gt;📁 File I/O&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Open and close file&lt;/td&gt;
          &lt;td&gt;9.05 μs (110.5k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Read 1KB file&lt;/td&gt;
          &lt;td&gt;10.0 μs (99.5k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Write 1KB file&lt;/td&gt;
          &lt;td&gt;35.1 μs (28.5k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Write 1MB file&lt;/td&gt;
          &lt;td&gt;207 μs (4.8k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;pickle.dumps()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;1.30 μs (769.6k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;pickle.loads()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;1.44 μs (695.2k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;#database-and-persistence&#34;&gt;&lt;strong&gt;🗄️ Database&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;SQLite insert (JSON blob)&lt;/td&gt;
          &lt;td&gt;192 μs (5.2k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;SQLite select by PK&lt;/td&gt;
          &lt;td&gt;3.57 μs (280.3k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;SQLite update one field&lt;/td&gt;
          &lt;td&gt;5.22 μs (191.7k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;diskcache set&lt;/td&gt;
          &lt;td&gt;23.9 μs (41.8k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;diskcache get&lt;/td&gt;
          &lt;td&gt;4.25 μs (235.5k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;MongoDB insert_one&lt;/td&gt;
          &lt;td&gt;119 μs (8.4k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;MongoDB find_one by _id&lt;/td&gt;
          &lt;td&gt;121 μs (8.2k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;MongoDB find_one by nested field&lt;/td&gt;
          &lt;td&gt;124 μs (8.1k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;#function-and-call-overhead&#34;&gt;&lt;strong&gt;📞 Functions&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Empty function call&lt;/td&gt;
          &lt;td&gt;22.4 ns (44.6M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Function with 5 args&lt;/td&gt;
          &lt;td&gt;24.0 ns (41.7M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Method call&lt;/td&gt;
          &lt;td&gt;23.3 ns (42.9M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;Lambda call&lt;/td&gt;
          &lt;td&gt;19.7 ns (50.9M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;try/except (no exception)&lt;/td&gt;
          &lt;td&gt;21.5 ns (46.5M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;try/except (exception raised)&lt;/td&gt;
          &lt;td&gt;139 ns (7.2M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;isinstance()&lt;/code&gt; check&lt;/td&gt;
          &lt;td&gt;18.3 ns (54.7M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;a href=&#34;#async-overhead&#34;&gt;&lt;strong&gt;⏱️ Async&lt;/strong&gt;&lt;/a&gt;&lt;/td&gt;
          &lt;td&gt;Create coroutine object&lt;/td&gt;
          &lt;td&gt;47.0 ns (21.3M ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;run_until_complete(empty)&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;27.6 μs (36.2k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;asyncio.sleep(0)&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;39.4 μs (25.4k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;gather()&lt;/code&gt; 10 coroutines&lt;/td&gt;
          &lt;td&gt;55.0 μs (18.2k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;create_task()&lt;/code&gt; + await&lt;/td&gt;
          &lt;td&gt;52.8 μs (18.9k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;/td&gt;
          &lt;td&gt;&lt;code&gt;async with&lt;/code&gt; (context manager)&lt;/td&gt;
          &lt;td&gt;29.5 μs (33.9k ops/sec)&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;memory-costs&#34;&gt;Memory Costs&lt;/h2&gt;
&lt;p&gt;Understanding how much memory different Python objects consume.&lt;/p&gt;
&lt;h3 id=&#34;an-empty-python-process-uses-1577-mb&#34;&gt;An empty Python process uses 15.77 MB&lt;/h3&gt;
&lt;hr&gt;
&lt;h3 id=&#34;strings&#34;&gt;Strings&lt;/h3&gt;
&lt;p&gt;The rule of thumb for ASCII strings is the core string object takes 41 bytes, with each additional character adding 1 byte. Note: Python uses different internal representations based on content—strings with Latin-1 characters use 1 byte/char, those with most Unicode use 2 bytes/char, and strings with emoji or rare characters use 4 bytes/char.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;String&lt;/th&gt;
          &lt;th&gt;Size&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Empty string &lt;code&gt;&amp;quot;&amp;quot;&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;41 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;1-char string &lt;code&gt;&amp;quot;a&amp;quot;&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;42 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;100-char string&lt;/td&gt;
          &lt;td&gt;141 bytes&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-numbers-every-programmer-should-know/string-memory-usage-by-size.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&#34;numbers&#34;&gt;Numbers&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Numbers are surprisingly large in Python&lt;/strong&gt;. They have to derive from CPython&amp;rsquo;s &lt;code&gt;PyObject&lt;/code&gt; and are subject to reference counting for garabage collection, they exceed our typical mental model many of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2 bytes = short int&lt;/li&gt;
&lt;li&gt;4 bytes = long int&lt;/li&gt;
&lt;li&gt;etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Type&lt;/th&gt;
          &lt;th&gt;Size&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Small int (-5 to 256, cached)&lt;/td&gt;
          &lt;td&gt;28 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Large int (1000)&lt;/td&gt;
          &lt;td&gt;28 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Very large int (10**100)&lt;/td&gt;
          &lt;td&gt;72 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Float&lt;/td&gt;
          &lt;td&gt;24 bytes&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-numbers-every-programmer-should-know/individual-integer-and-float-memory-usage.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&#34;collections&#34;&gt;Collections&lt;/h3&gt;
&lt;p&gt;Collections are amazing in Python. Dynamically growing lists. Ultra high-perf dictionaries and sets. Here is the empty and &amp;ldquo;full&amp;rdquo; overhead of each.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Collection&lt;/th&gt;
          &lt;th&gt;Empty&lt;/th&gt;
          &lt;th&gt;1,000 items&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;List (ints)&lt;/td&gt;
          &lt;td&gt;56 bytes&lt;/td&gt;
          &lt;td&gt;36.0 KB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;List (floats)&lt;/td&gt;
          &lt;td&gt;56 bytes&lt;/td&gt;
          &lt;td&gt;32.1 KB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Dict&lt;/td&gt;
          &lt;td&gt;64 bytes&lt;/td&gt;
          &lt;td&gt;90.7 KB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Set&lt;/td&gt;
          &lt;td&gt;216 bytes&lt;/td&gt;
          &lt;td&gt;59.6 KB&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-numbers-every-programmer-should-know/empty-collection-memory-overhead.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&#34;classes-and-instances&#34;&gt;Classes and Instances&lt;/h3&gt;
&lt;p&gt;Slots are an interesting addition to Python classes. They remove the entire concept of a &lt;code&gt;__dict__&lt;/code&gt; for tracking fields and other values. Even for a single instance, slots classes are &lt;strong&gt;significantly smaller&lt;/strong&gt; (212 bytes vs 694 bytes for 5 attributes). If you are holding a large number of them in memory for a list or cache, the memory savings of a slots class becomes meaningful - about 30% less memory usage. Luckily for most use-cases, just adding a slots entry saves memory with minimal effort.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Type&lt;/th&gt;
          &lt;th&gt;Empty&lt;/th&gt;
          &lt;th&gt;5 attributes&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Regular class&lt;/td&gt;
          &lt;td&gt;344 bytes&lt;/td&gt;
          &lt;td&gt;694 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;__slots__&lt;/code&gt; class&lt;/td&gt;
          &lt;td&gt;32 bytes&lt;/td&gt;
          &lt;td&gt;212 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;dataclass&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;694 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;@dataclass(slots=True)&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;212 bytes&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;namedtuple&lt;/td&gt;
          &lt;td&gt;—&lt;/td&gt;
          &lt;td&gt;228 bytes&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;Aggregate Memory Usage (1,000 instances):&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Type&lt;/th&gt;
          &lt;th&gt;Total Memory&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;List of 1,000 regular class instances&lt;/td&gt;
          &lt;td&gt;301.8 KB&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;List of 1,000 &lt;code&gt;__slots__&lt;/code&gt; class instances&lt;/td&gt;
          &lt;td&gt;215.7 KB&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-numbers-every-programmer-should-know/memory-for-1-000-class-instances.png?v=3&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;basic-operations&#34;&gt;Basic Operations&lt;/h2&gt;
&lt;p&gt;The cost of fundamental Python operations: Way slower than C/C++/C# but still quite fast. I added &lt;a href=&#34;https://github.com/mikeckennedy/python-numbers-everyone-should-know/blob/main/code/basic_ops/vs_dotnet/RESULTS.md&#34;&gt;a brief comparison to C#&lt;/a&gt; to the source repo.&lt;/p&gt;
&lt;h3 id=&#34;arithmetic&#34;&gt;Arithmetic&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Add two integers&lt;/td&gt;
          &lt;td&gt;19.0 ns (52.7M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Add two floats&lt;/td&gt;
          &lt;td&gt;18.4 ns (54.4M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Multiply two integers&lt;/td&gt;
          &lt;td&gt;19.4 ns (51.6M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-numbers-every-programmer-should-know/arithmetic-operation-speed.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&#34;string-operations&#34;&gt;String Operations&lt;/h3&gt;
&lt;p&gt;String operations in Python are fast as well. Among template-based formatting styles, f-strings are the fastest. Simple concatenation (&lt;code&gt;+&lt;/code&gt;) is faster still for combining a couple strings, but f-strings scale better and are more readable. Even the slowest formatting style is still measured in just nanoseconds.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Concatenation (&lt;code&gt;+&lt;/code&gt;)&lt;/td&gt;
          &lt;td&gt;39.1 ns (25.6M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;f-string&lt;/td&gt;
          &lt;td&gt;64.9 ns (15.4M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;.format()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;103 ns (9.7M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;%&lt;/code&gt; formatting&lt;/td&gt;
          &lt;td&gt;89.8 ns (11.1M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-numbers-every-programmer-should-know/string-formatting-speed-comparison.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&#34;list-operations&#34;&gt;List Operations&lt;/h3&gt;
&lt;p&gt;List operations are very fast in Python. Adding a single item &lt;em&gt;usually&lt;/em&gt; requires 28ns. Said another way, you can do 35M appends per second. This is unless the list has to expand using something like a doubling algorithm. You can see this in the ops/sec for 1,000 items.&lt;/p&gt;
&lt;p&gt;Surprisingly, &lt;strong&gt;list comprehensions are 26% faster than the equivalent for loops&lt;/strong&gt; with append statements.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;list.append()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;28.7 ns (34.8M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;List comprehension (1,000 items)&lt;/td&gt;
          &lt;td&gt;9.45 μs (105.8k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Equivalent for-loop (1,000 items)&lt;/td&gt;
          &lt;td&gt;11.9 μs (83.9k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-numbers-every-programmer-should-know/list-comprehension-vs-for-loop.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;collection-access-and-iteration&#34;&gt;Collection Access and Iteration&lt;/h2&gt;
&lt;p&gt;How fast can you get data out of Python&amp;rsquo;s built-in collections? Here is a dramatic example of how much faster the correct data structure is. &lt;code&gt;item in set&lt;/code&gt; or &lt;code&gt;item in dict&lt;/code&gt; is &lt;strong&gt;200x faster&lt;/strong&gt; than &lt;code&gt;item in list&lt;/code&gt; for just 1,000 items! This difference comes from algorithmic complexity: sets and dicts use O(1) hash lookups, while lists require O(n) linear scans—and this gap grows with collection size.&lt;/p&gt;
&lt;p&gt;The graph below is non-linear in the x-axis.&lt;/p&gt;
&lt;h3 id=&#34;access-by-keyindex&#34;&gt;Access by Key/Index&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Dict lookup by key&lt;/td&gt;
          &lt;td&gt;21.9 ns (45.7M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Set membership (&lt;code&gt;in&lt;/code&gt;)&lt;/td&gt;
          &lt;td&gt;19.0 ns (52.7M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;List index access&lt;/td&gt;
          &lt;td&gt;17.6 ns (56.8M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;List membership (&lt;code&gt;in&lt;/code&gt;, 1,000 items)&lt;/td&gt;
          &lt;td&gt;3.85 μs (259.6k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-numbers-every-programmer-should-know/collection-access-speed.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&#34;length&#34;&gt;Length&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;len()&lt;/code&gt; is very fast. Maybe we don&amp;rsquo;t have to optimize it out of the test condition on a while loop looping 100 times after all.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Collection&lt;/th&gt;
          &lt;th&gt;&lt;code&gt;len()&lt;/code&gt; time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;List (1,000 items)&lt;/td&gt;
          &lt;td&gt;18.8 ns (53.3M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Dict (1,000 items)&lt;/td&gt;
          &lt;td&gt;17.6 ns (56.9M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Set (1,000 items)&lt;/td&gt;
          &lt;td&gt;18.0 ns (55.5M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&#34;iteration&#34;&gt;Iteration&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Iterate 1,000-item list&lt;/td&gt;
          &lt;td&gt;7.87 μs (127.0k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Iterate 1,000-item dict (keys)&lt;/td&gt;
          &lt;td&gt;8.74 μs (114.5k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;sum()&lt;/code&gt; of 1,000 integers&lt;/td&gt;
          &lt;td&gt;1.87 μs (534.8k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;class-and-object-attributes&#34;&gt;Class and Object Attributes&lt;/h2&gt;
&lt;p&gt;The cost of reading and writing attributes, and how &lt;code&gt;__slots__&lt;/code&gt; changes things. &lt;strong&gt;Slots saves ~30% memory on large collections, with virtually identical attribute access speed&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id=&#34;attribute-access&#34;&gt;Attribute Access&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Regular Class&lt;/th&gt;
          &lt;th&gt;&lt;code&gt;__slots__&lt;/code&gt; Class&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Read attribute&lt;/td&gt;
          &lt;td&gt;14.1 ns (70.9M ops/sec)&lt;/td&gt;
          &lt;td&gt;14.1 ns (70.7M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Write attribute&lt;/td&gt;
          &lt;td&gt;15.7 ns (63.6M ops/sec)&lt;/td&gt;
          &lt;td&gt;16.4 ns (60.8M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-numbers-every-programmer-should-know/attribute-access-regular-vs-slots-classes.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&#34;other-attribute-operations&#34;&gt;Other Attribute Operations&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Read &lt;code&gt;@property&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;19.0 ns (52.8M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;getattr(obj, &#39;attr&#39;)&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;13.8 ns (72.7M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;hasattr(obj, &#39;attr&#39;)&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;23.8 ns (41.9M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;json-and-serialization&#34;&gt;JSON and Serialization&lt;/h2&gt;
&lt;p&gt;Comparing standard library JSON with optimized alternatives. &lt;code&gt;orjson&lt;/code&gt; &lt;strong&gt;handles more data types and is over 8x faster than standard lib&lt;/strong&gt; &lt;code&gt;json&lt;/code&gt; for complex objects. Impressive!&lt;/p&gt;
&lt;h3 id=&#34;serialization-dumps&#34;&gt;Serialization (dumps)&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Library&lt;/th&gt;
          &lt;th&gt;Simple Object&lt;/th&gt;
          &lt;th&gt;Complex Object&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;json&lt;/code&gt; (stdlib)&lt;/td&gt;
          &lt;td&gt;708 ns (1.4M ops/sec)&lt;/td&gt;
          &lt;td&gt;2.65 μs (376.8k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;orjson&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;60.9 ns (16.4M ops/sec)&lt;/td&gt;
          &lt;td&gt;310 ns (3.2M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;ujson&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;264 ns (3.8M ops/sec)&lt;/td&gt;
          &lt;td&gt;1.64 μs (611.2k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;msgspec&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;92.3 ns (10.8M ops/sec)&lt;/td&gt;
          &lt;td&gt;445 ns (2.2M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-numbers-every-programmer-should-know/json-serialization-speed-complex-object.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&#34;deserialization-loads&#34;&gt;Deserialization (loads)&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Library&lt;/th&gt;
          &lt;th&gt;Simple Object&lt;/th&gt;
          &lt;th&gt;Complex Object&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;json&lt;/code&gt; (stdlib)&lt;/td&gt;
          &lt;td&gt;714 ns (1.4M ops/sec)&lt;/td&gt;
          &lt;td&gt;2.22 μs (449.9k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;orjson&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;106 ns (9.4M ops/sec)&lt;/td&gt;
          &lt;td&gt;839 ns (1.2M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;ujson&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;268 ns (3.7M ops/sec)&lt;/td&gt;
          &lt;td&gt;1.46 μs (682.8k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;msgspec&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;101 ns (9.9M ops/sec)&lt;/td&gt;
          &lt;td&gt;850 ns (1.2M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&#34;pydantic&#34;&gt;Pydantic&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;model_dump_json()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;1.54 μs (647.8k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;model_validate_json()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;2.99 μs (334.7k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;model_dump()&lt;/code&gt; (to dict)&lt;/td&gt;
          &lt;td&gt;1.71 μs (585.2k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;model_validate()&lt;/code&gt; (from dict)&lt;/td&gt;
          &lt;td&gt;2.30 μs (435.5k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;web-frameworks&#34;&gt;Web Frameworks&lt;/h2&gt;
&lt;p&gt;Returning a simple JSON response. Benchmarked with &lt;code&gt;wrk&lt;/code&gt; against localhost running 4 works in Granian. Each framework returns the same JSON payload from a minimal endpoint. No database access or that sort of thing. This is just how much overhead/perf do we get from each framework itself. The code we write that runs within those view methods is largely the same.&lt;/p&gt;
&lt;h3 id=&#34;results&#34;&gt;Results&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Framework&lt;/th&gt;
          &lt;th&gt;Requests/sec&lt;/th&gt;
          &lt;th&gt;Latency (p99)&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Flask&lt;/td&gt;
          &lt;td&gt;16.5 μs (60.7k req/sec)&lt;/td&gt;
          &lt;td&gt;20.85 ms (48.0 ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Django&lt;/td&gt;
          &lt;td&gt;18.1 μs (55.4k req/sec)&lt;/td&gt;
          &lt;td&gt;170.3 ms (5.9 ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;FastAPI&lt;/td&gt;
          &lt;td&gt;8.63 μs (115.9k req/sec)&lt;/td&gt;
          &lt;td&gt;1.530 ms (653.6 ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Starlette&lt;/td&gt;
          &lt;td&gt;8.01 μs (124.8k req/sec)&lt;/td&gt;
          &lt;td&gt;930 μs (1.1k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Litestar&lt;/td&gt;
          &lt;td&gt;8.19 μs (122.1k req/sec)&lt;/td&gt;
          &lt;td&gt;1.010 ms (990.1 ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-numbers-every-programmer-should-know/web-framework-throughput.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;file-io&#34;&gt;File I/O&lt;/h2&gt;
&lt;p&gt;Reading and writing files of various sizes. Note that the graph is non-linear in y-axis.&lt;/p&gt;
&lt;h3 id=&#34;basic-operations-1&#34;&gt;Basic Operations&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Open and close (no read)&lt;/td&gt;
          &lt;td&gt;9.05 μs (110.5k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Read 1KB file&lt;/td&gt;
          &lt;td&gt;10.0 μs (99.5k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Read 1MB file&lt;/td&gt;
          &lt;td&gt;33.6 μs (29.8k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Write 1KB file&lt;/td&gt;
          &lt;td&gt;35.1 μs (28.5k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Write 1MB file&lt;/td&gt;
          &lt;td&gt;207 μs (4.8k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-numbers-every-programmer-should-know/file-io-performance.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3 id=&#34;pickle-vs-json-serialization&#34;&gt;Pickle vs JSON (Serialization)&lt;/h3&gt;
&lt;p&gt;For more serialization options including &lt;code&gt;orjson&lt;/code&gt;, &lt;code&gt;msgspec&lt;/code&gt;, and &lt;code&gt;pydantic&lt;/code&gt;, see &lt;a href=&#34;#json-and-serialization&#34;&gt;JSON and Serialization&lt;/a&gt; above.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;pickle.dumps()&lt;/code&gt; (complex obj)&lt;/td&gt;
          &lt;td&gt;1.30 μs (769.6k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;pickle.loads()&lt;/code&gt; (complex obj)&lt;/td&gt;
          &lt;td&gt;1.44 μs (695.2k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;json.dumps()&lt;/code&gt; (complex obj)&lt;/td&gt;
          &lt;td&gt;2.72 μs (367.1k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;json.loads()&lt;/code&gt; (complex obj)&lt;/td&gt;
          &lt;td&gt;2.35 μs (425.9k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;database-and-persistence&#34;&gt;Database and Persistence&lt;/h2&gt;
&lt;p&gt;Comparing SQLite, diskcache, and MongoDB using the same complex object.&lt;/p&gt;
&lt;h3 id=&#34;test-object&#34;&gt;Test Object&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;user_data&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;12345&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;username&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;alice_dev&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;email&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;alice@example.com&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;profile&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;bio&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Software engineer who loves Python&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;location&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Portland, OR&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;website&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;https://alice.dev&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;joined&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;2020-03-15T08:30:00Z&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;posts&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;First Post&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;python&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;tutorial&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;views&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;1520&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Second Post&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;rust&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;wasm&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;views&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;843&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;id&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;3&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;title&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Third Post&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;tags&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;python&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;async&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;],&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;views&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2341&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;},&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;settings&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;theme&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;dark&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;notifications&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;kc&#34;&gt;True&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;s2&#34;&gt;&amp;#34;email_frequency&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;weekly&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h3 id=&#34;sqlite-json-blob-approach&#34;&gt;SQLite (JSON blob approach)&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Insert one object&lt;/td&gt;
          &lt;td&gt;192 μs (5.2k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Select by primary key&lt;/td&gt;
          &lt;td&gt;3.57 μs (280.3k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Update one field&lt;/td&gt;
          &lt;td&gt;5.22 μs (191.7k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Delete&lt;/td&gt;
          &lt;td&gt;191 μs (5.2k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Select with &lt;code&gt;json_extract()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;4.27 μs (234.2k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&#34;diskcache&#34;&gt;diskcache&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;cache.set(key, obj)&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;23.9 μs (41.8k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;cache.get(key)&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;4.25 μs (235.5k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;cache.delete(key)&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;51.9 μs (19.3k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Check key exists&lt;/td&gt;
          &lt;td&gt;1.91 μs (523.2k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&#34;mongodb&#34;&gt;MongoDB&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;insert_one()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;119 μs (8.4k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;find_one()&lt;/code&gt; by &lt;code&gt;_id&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;121 μs (8.2k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;find_one()&lt;/code&gt; by nested field&lt;/td&gt;
          &lt;td&gt;124 μs (8.1k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;update_one()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;115 μs (8.7k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;delete_one()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;30.4 ns (32.9M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&#34;comparison-table&#34;&gt;Comparison Table&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;SQLite&lt;/th&gt;
          &lt;th&gt;diskcache&lt;/th&gt;
          &lt;th&gt;MongoDB&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Write one object&lt;/td&gt;
          &lt;td&gt;192 μs (5.2k ops/sec)&lt;/td&gt;
          &lt;td&gt;23.9 μs (41.8k ops/sec)&lt;/td&gt;
          &lt;td&gt;119 μs (8.4k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Read by key/id&lt;/td&gt;
          &lt;td&gt;3.57 μs (280.3k ops/sec)&lt;/td&gt;
          &lt;td&gt;4.25 μs (235.5k ops/sec)&lt;/td&gt;
          &lt;td&gt;121 μs (8.2k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Read by nested field&lt;/td&gt;
          &lt;td&gt;4.27 μs (234.2k ops/sec)&lt;/td&gt;
          &lt;td&gt;N/A&lt;/td&gt;
          &lt;td&gt;124 μs (8.1k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Update one field&lt;/td&gt;
          &lt;td&gt;5.22 μs (191.7k ops/sec)&lt;/td&gt;
          &lt;td&gt;23.9 μs (41.8k ops/sec)&lt;/td&gt;
          &lt;td&gt;115 μs (8.7k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Delete&lt;/td&gt;
          &lt;td&gt;191 μs (5.2k ops/sec)&lt;/td&gt;
          &lt;td&gt;51.9 μs (19.3k ops/sec)&lt;/td&gt;
          &lt;td&gt;30.4 ns (32.9M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Note: MongoDB is a victim of network access version in-process access.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-numbers-every-programmer-should-know/database-performance-sqlite-vs-diskcache-vs-mongodb.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;function-and-call-overhead&#34;&gt;Function and Call Overhead&lt;/h2&gt;
&lt;p&gt;The hidden cost of function calls, exceptions, and async.&lt;/p&gt;
&lt;h3 id=&#34;function-calls&#34;&gt;Function Calls&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Empty function call&lt;/td&gt;
          &lt;td&gt;22.4 ns (44.6M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Function with 5 arguments&lt;/td&gt;
          &lt;td&gt;24.0 ns (41.7M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Method call on object&lt;/td&gt;
          &lt;td&gt;23.3 ns (42.9M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Lambda call&lt;/td&gt;
          &lt;td&gt;19.7 ns (50.9M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Built-in function (&lt;code&gt;len()&lt;/code&gt;)&lt;/td&gt;
          &lt;td&gt;17.1 ns (58.4M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&#34;exceptions&#34;&gt;Exceptions&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;try/except&lt;/code&gt; (no exception raised)&lt;/td&gt;
          &lt;td&gt;21.5 ns (46.5M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;try/except&lt;/code&gt; (exception raised)&lt;/td&gt;
          &lt;td&gt;139 ns (7.2M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&#34;type-checking&#34;&gt;Type Checking&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;isinstance()&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;18.3 ns (54.7M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;type() == type&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;21.8 ns (46.0M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;async-overhead&#34;&gt;Async Overhead&lt;/h2&gt;
&lt;p&gt;The cost of async machinery.&lt;/p&gt;
&lt;h3 id=&#34;coroutine-creation&#34;&gt;Coroutine Creation&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Create coroutine object (no await)&lt;/td&gt;
          &lt;td&gt;47.0 ns (21.3M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Create coroutine (with return value)&lt;/td&gt;
          &lt;td&gt;45.3 ns (22.1M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&#34;running-coroutines&#34;&gt;Running Coroutines&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;run_until_complete(empty)&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;27.6 μs (36.2k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;run_until_complete(return value)&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;26.6 μs (37.5k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Run nested await&lt;/td&gt;
          &lt;td&gt;28.9 μs (34.6k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Run 3 sequential awaits&lt;/td&gt;
          &lt;td&gt;27.9 μs (35.8k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&#34;asynciosleep&#34;&gt;asyncio.sleep()&lt;/h3&gt;
&lt;p&gt;Note: &lt;code&gt;asyncio.sleep(0)&lt;/code&gt; is a special case in Python&amp;rsquo;s event loop—it yields control but schedules an immediate callback, making it faster than typical sleeps but not representative of general event loop overhead.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;asyncio.sleep(0)&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;39.4 μs (25.4k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Coroutine with &lt;code&gt;sleep(0)&lt;/code&gt;&lt;/td&gt;
          &lt;td&gt;41.8 μs (23.9k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&#34;asynciogather&#34;&gt;asyncio.gather()&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;gather()&lt;/code&gt; 5 coroutines&lt;/td&gt;
          &lt;td&gt;49.7 μs (20.1k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;gather()&lt;/code&gt; 10 coroutines&lt;/td&gt;
          &lt;td&gt;55.0 μs (18.2k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;gather()&lt;/code&gt; 100 coroutines&lt;/td&gt;
          &lt;td&gt;155 μs (6.5k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&#34;task-creation&#34;&gt;Task Creation&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;create_task()&lt;/code&gt; + await&lt;/td&gt;
          &lt;td&gt;52.8 μs (18.9k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Create 10 tasks + gather&lt;/td&gt;
          &lt;td&gt;85.5 μs (11.7k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&#34;async-context-managers--iteration&#34;&gt;Async Context Managers &amp;amp; Iteration&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;async with&lt;/code&gt; (context manager)&lt;/td&gt;
          &lt;td&gt;29.5 μs (33.9k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;async for&lt;/code&gt; (5 items)&lt;/td&gt;
          &lt;td&gt;30.0 μs (33.3k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;&lt;code&gt;async for&lt;/code&gt; (100 items)&lt;/td&gt;
          &lt;td&gt;36.4 μs (27.5k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h3 id=&#34;sync-vs-async-comparison&#34;&gt;Sync vs Async Comparison&lt;/h3&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;Operation&lt;/th&gt;
          &lt;th&gt;Time&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;Sync function call&lt;/td&gt;
          &lt;td&gt;20.3 ns (49.2M ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;Async equivalent (&lt;code&gt;run_until_complete&lt;/code&gt;)&lt;/td&gt;
          &lt;td&gt;28.2 μs (35.5k ops/sec)&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;hr&gt;
&lt;h2 id=&#34;methodology&#34;&gt;Methodology&lt;/h2&gt;
&lt;h3 id=&#34;benchmarking-approach&#34;&gt;Benchmarking Approach&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;All benchmarks run multiple times and with warmup not timed&lt;/li&gt;
&lt;li&gt;Timing uses &lt;code&gt;timeit&lt;/code&gt; or &lt;code&gt;perf_counter_ns&lt;/code&gt; as appropriate&lt;/li&gt;
&lt;li&gt;Memory measured with &lt;code&gt;sys.getsizeof()&lt;/code&gt; and &lt;code&gt;tracemalloc&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Results are median of N runs&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;environment&#34;&gt;Environment&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;OS:&lt;/strong&gt; macOS 26.2&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Python:&lt;/strong&gt; 3.14.2 (CPython)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;CPU:&lt;/strong&gt; ARM - 14 cores (14 logical)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;RAM:&lt;/strong&gt; 24.0 GB&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;code-repository&#34;&gt;Code Repository&lt;/h3&gt;
&lt;p&gt;All benchmark code available at: &lt;a href=&#34;https://github.com/mikeckennedy/python-numbers-everyone-should-know&#34;&gt;https://github.com/mikeckennedy/python-numbers-everyone-should-know&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2 id=&#34;key-takeaways&#34;&gt;Key Takeaways&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Memory overhead&lt;/strong&gt;: Python objects have significant memory overhead - even an empty list is 56 bytes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Dict/set speed&lt;/strong&gt;: Dictionary and set lookups are extremely fast (O(1) average case) compared to list membership checks (O(n))&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;JSON performance&lt;/strong&gt;: Alternative JSON libraries like &lt;code&gt;orjson&lt;/code&gt; and &lt;code&gt;msgspec&lt;/code&gt; are 3-8x faster than stdlib &lt;code&gt;json&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Async overhead&lt;/strong&gt;: Creating and awaiting coroutines has measurable overhead - only use async when you need concurrency&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;__slots__&lt;/code&gt; tradeoff&lt;/strong&gt;: &lt;code&gt;__slots__&lt;/code&gt; saves memory (~30% for collections of instances) with virtually no performance impact&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;Last updated: 2026-01-01&lt;/em&gt;&lt;/p&gt;
&lt;style&gt;
/* Custom table styling for this post only */
table {
    font-size: 13px;
    border-collapse: collapse;
    width: 100%;
    max-width: 690px;
    /* margin: 1em auto; */
    background-color: #ddd;
    box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
    border-radius: 6px;
    overflow: hidden;
}

table thead {
    background-color: #444;
    color: white;
}

table th {
    padding: 8px 12px;
    text-align: left;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: 0.5px;
    font-size: 11px;
}

table td {
    padding: 6px 12px;
    border-bottom: 1px solid #e5e7eb;
    line-height: 1.4;
}

table tbody tr {
    transition: all 0.2s ease-in-out;
    background-color: #fff;
}

table tbody tr:hover {
    background-color: #e3f2fd;
    transform: scale(1.01);
    box-shadow: 0 2px 12px rgba(33, 150, 243, 0.15);
    cursor: default;
}

table tbody tr:last-child td {
    border-bottom: none;
}

/* Code in tables */
table code {
    background-color: #f3f4f6;
    padding: 2px 4px;
    border-radius: 3px;
    font-size: 12px;
}
&lt;/style&gt;
</description>
        </item>
        
        
        
        <item>
            <title>DevOps Python Supply Chain Security</title>
            <link>https://mkennedy.codes/posts/devops-python-supply-chain-security/</link>
            <pubDate>Fri, 26 Dec 2025 15:53:11 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/devops-python-supply-chain-security/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/devops-python-supply-chain-security/pip-audit-in-docker.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Run pip-audit in an isolated Docker container before installing updated dependencies on your dev machine. Build a reusable &lt;code&gt;pipauditdocker&lt;/code&gt; image and alias &lt;code&gt;pip-audit-proj&lt;/code&gt; to test requirements.txt in isolation before they touch your local environment.&lt;/p&gt;
&lt;p&gt;In my last article, &amp;ldquo;&lt;a href=&#34;https://mkennedy.codes/posts/python-supply-chain-security-made-easy/&#34;&gt;Python Supply Chain Security Made Easy&lt;/a&gt;&amp;rdquo; I talked about how to automate &lt;a href=&#34;https://pypi.org/project/pip-audit/&#34;&gt;pip-audit&lt;/a&gt; so you don&amp;rsquo;t accidentally ship malicious Python packages to production. While there was defense in depth with uv&amp;rsquo;s delayed installs, there wasn&amp;rsquo;t much safety beyond that for developers themselves on their machines.&lt;/p&gt;
&lt;p&gt;This follow up fixes that so even dev machines stay safe.&lt;/p&gt;
&lt;h2 id=&#34;defending-your-dev-machine&#34;&gt;Defending your dev machine&lt;/h2&gt;
&lt;p&gt;My recommendation is instead of installing directly into a local virtual environment and then running pip-audit, create a dedicated Docker image meant for testing dependencies with pip-audit in isolation.&lt;/p&gt;
&lt;p&gt;Our workflow can go like this.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First&lt;/strong&gt;, we update your local dependencies file:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;uv pip compile requirements.piptools --output-file requirements.txt --exclude-newer &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt; week
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will update the requirements.txt file, or tweak the command to update your uv.lock file, but it don&amp;rsquo;t install anything.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second&lt;/strong&gt;, run a command that uses this new requirements file inside of a temporary docker container to install the requirements and run pip-audit on them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Third&lt;/strong&gt;, only if that pip-audit test succeeds, install the updated requirements into your local venv.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;uv pip install -r requirements.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;the-pip-audit-docker-image&#34;&gt;The pip-audit docker image&lt;/h2&gt;
&lt;p&gt;What do we use for our Docker testing image? There are of course a million ways to do this. Here&amp;rsquo;s one optimized for building Python packages that deeply leverages uv&amp;rsquo;s and pip-audit&amp;rsquo;s caching to make subsequent runs much, much faster.&lt;/p&gt;
&lt;p&gt;Create a Dockerfile with this content:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Image for installing python packages with uv and testing with pip-audit&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Saved as Dockerfile&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;ubuntu:latest&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; apt-get update&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; apt-get upgrade -y&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; apt-get autoremove -y&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; apt-get -y install curl&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Dependencies for building Python packages&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; apt-get install -y gcc &lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; apt-get install -y build-essential&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; apt-get install -y clang&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; apt-get install -y openssl &lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; apt-get install -y checkinstall &lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; apt-get install -y libgdbm-dev &lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; apt-get install -y libc6-dev&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; apt-get install -y libtool&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; apt-get install -y zlib1g-dev&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; apt-get install -y libffi-dev&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; apt-get install -y libxslt1-dev&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ENV&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;PATH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/venv/bin:&lt;span class=&#34;nv&#34;&gt;$PATH&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ENV&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;PATH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/root/.cargo/bin:&lt;span class=&#34;nv&#34;&gt;$PATH&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ENV&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;PATH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/root/.local/bin/:&lt;span class=&#34;nv&#34;&gt;$PATH&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ENV&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;UV_LINK_MODE&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;copy
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Install uv&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; curl -LsSf https://astral.sh/uv/install.sh &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; sh&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# set up a virtual env to use for temp dependencies in isolation.&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; --mount&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;cache,target&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/root/.cache uv venv --python 3.14 /venv&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# test that uv is working&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; uv --version &lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;WORKDIR &lt;span class=&#34;s2&#34;&gt;&amp;#34;/&amp;#34;&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Install pip-audit&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; --mount&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;cache,target&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/root/.cache uv pip install --python /venv/bin/python3 pip-audit&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This installs a bunch of Linux libraries used for edge-case builds of Python packages. It takes a moment, but you only need to build the image once. Then you&amp;rsquo;ll run it again and again. If you want to use a newer version of Python later, change the version in &lt;code&gt;uv venv --python 3.14 /venv&lt;/code&gt;. Even then on rebuilds, the apt-get steps are reused from cache.&lt;/p&gt;
&lt;p&gt;Next you build with a fixed tag so you can create aliases to run using this image:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# In the same folder as the Dockerfile above.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;docker build -t pipauditdocker .
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Finally, we need to run the container with a few bells and whistles. Add caching via a volume so subsequent runs are very fast: &lt;code&gt;-v pip-audit-cache:/root/.cache&lt;/code&gt;. And map a volume so whatever working directory you are in will find the local requirements.txt: &lt;code&gt;-v \&amp;quot;\$(pwd)/requirements.txt:/workspace/requirements.txt:ro\&amp;quot;&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Here is the alias to add to your &lt;code&gt;.bashrc&lt;/code&gt; or &lt;code&gt;.zshrc&lt;/code&gt; accomplishing this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nb&#34;&gt;alias&lt;/span&gt; pip-audit-proj&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;s2&#34;&gt;&amp;#34;echo &amp;#39;🐳 Launching isolated test env in Docker...&amp;#39; &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;docker run --rm \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;  -v pip-audit-cache:/root/.cache \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;  -v \&amp;#34;\$(pwd)/requirements.txt:/workspace/requirements.txt:ro\&amp;#34; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;  pipauditdocker \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;  /bin/bash -c \&amp;#34;echo &amp;#39;📦 Installing requirements from /workspace/requirements.txt...&amp;#39; &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;    uv pip install --quiet -r /workspace/requirements.txt &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;    echo &amp;#39;🔍 Running pip-audit security scan...&amp;#39; &amp;amp;&amp;amp; \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;    /venv/bin/pip-audit \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;      --ignore-vuln CVE-2025-53000 \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;      --ignore-vuln PYSEC-2023-242 \
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;s2&#34;&gt;      --skip-editable\&amp;#34;&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it! Once you reload your shell, all you have to do is type is &lt;code&gt;pip-audit-proj&lt;/code&gt; when you&amp;rsquo;re in the root of your project that contains your requirements.txt file. You should see something like this below. Slow the first time, fast afterwards.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/devops-python-supply-chain-security/pip-audit-in-docker.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;protecting-docker-in-production-too&#34;&gt;Protecting Docker in production too&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s handle one more situation while we are at it. You&amp;rsquo;re running your Python app &lt;strong&gt;IN&lt;/strong&gt; Docker. Part of the Docker build configures the image and installs your dependencies. We can add a pip-audit check there too:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Dockerfile for your app (different than validation image above)&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# All the steps to copy your app over and configure the image ...&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# After creating a venv in /venv and copying your requirements.txt to /app&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Check for any sketchy packages.&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# We are using mount rather than a volume because&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# we want to cache build time activity, not runtime activity.&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; --mount&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;cache,target&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/root/.cache uv pip install --python /venv/bin/python3 --upgrade pip-audit&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; --mount&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;cache,target&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/root/.cache /venv/bin/pip-audit --ignore-vuln CVE-2025-53000 --ignore-vuln PYSEC-2023-242 --skip-editable&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# ENTRYPOINT ... for your app&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;conclusion&#34;&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;There you have it. Two birds, one Docker stone for both. Our first Dockerfile built a reusable Docker image named &lt;strong&gt;pipauditdocker&lt;/strong&gt; to run isolated tests against a requirements file. This second one demonstrates how we can make our &lt;strong&gt;docker/docker compose build&lt;/strong&gt; completely fail if there is a bad dependency saving us from letting it slip into production.&lt;/p&gt;
&lt;p&gt;Cheers&lt;br&gt;
Michael&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Python Supply Chain Security Made Easy</title>
            <link>https://mkennedy.codes/posts/python-supply-chain-security-made-easy/</link>
            <pubDate>Mon, 22 Dec 2025 16:16:49 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/python-supply-chain-security-made-easy/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Wire &lt;code&gt;pip-audit&lt;/code&gt; into your CI and unit tests to automatically block known vulnerable dependencies. Add a delay with &lt;code&gt;uv pip compile --exclude-newer &amp;quot;1 week&amp;quot;&lt;/code&gt; to avoid installing packages before security issues are discovered. See &lt;a href=&#34;https://mkennedy.codes/posts/devops-python-supply-chain-security/&#34;&gt;Part 2&lt;/a&gt; for Docker-based protection.&lt;/p&gt;
&lt;p&gt;Maybe you&amp;rsquo;ve heard that hackers have been trying to take advantage of open source software to inject code into your machine, and worst case scenario, even the consumers of your libraries or your applications machines. In this quick post, I&amp;rsquo;ll show you how to integrate &lt;a href=&#34;https://pypi.org/project/pip-audit/&#34;&gt;Python&amp;rsquo;s &amp;ldquo;Official&amp;rdquo; package scanning technology&lt;/a&gt; directly into your continuous integration and your project&amp;rsquo;s unit tests. While pip-audit is maintained in part by &lt;a href=&#34;https://www.trailofbits.com/&#34;&gt;Trail of Bits&lt;/a&gt; with support from Google, it&amp;rsquo;s part of the &lt;a href=&#34;https://github.com/pypa/pip-audit&#34;&gt;PyPA organization&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;why-this-matters&#34;&gt;Why this matters&lt;/h2&gt;
&lt;p&gt;Here are 5 recent, high-danger PyPI security issues supply chain attacks where “pip install” can turn into “pip install a backdoor.” Afterwards, we talk about how to scan for and prevent these from making it to your users.&lt;/p&gt;
&lt;h3 id=&#34;compromised-popular-package-release-ultralytics-malicious-update-pushed-to-pypi&#34;&gt;Compromised popular package release (ultralytics), malicious update pushed to PyPI&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;What happened:&lt;/em&gt; A malicious version (8.3.41) of the widely-used &lt;code&gt;ultralytics&lt;/code&gt; package was published to PyPI, containing code that downloaded the XMRig coinminer. Follow-on versions also carried the malicious downloader, and the writeup attributes the initial compromise to a GitHub Actions script injection, plus later abuse consistent with a stolen PyPI API token. Source: &lt;a href=&#34;https://www.reversinglabs.com/blog/compromised-ultralytics-pypi-package-delivers-crypto-coinminer&#34;&gt;ReversingLabs&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;campaign-of-fake-packages-stealing-cloud-access-tokens-14100-downloads-before-removal&#34;&gt;Campaign of fake packages stealing cloud access tokens, 14,100+ downloads before removal&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;What happened:&lt;/em&gt; Researchers reported multiple bogus PyPI libraries (including “time-related utilities”) designed to exfiltrate cloud access tokens, with the campaign exceeding 14,100 downloads before takedown. If those tokens are real, this can turn into cloud account takeover. Source: &lt;a href=&#34;https://thehackernews.com/2025/03/malicious-pypi-packages-stole-cloud.html&#34;&gt;The Hacker News&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;typosquatting-and-name-confusion-targeting-colorama-with-remote-control-and-data-theft-payloads&#34;&gt;Typosquatting and name-confusion targeting &lt;code&gt;colorama&lt;/code&gt;, with remote control and data theft payloads&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;What happened:&lt;/em&gt; A campaign uploaded lookalike package names to PyPI to catch developers intending to install &lt;code&gt;colorama&lt;/code&gt;, with payloads described as enabling persistent remote access/remote control plus harvesting and exfiltration of sensitive data. High danger mainly because &lt;code&gt;colorama&lt;/code&gt; is popular and typos happen. Source: &lt;a href=&#34;https://checkmarx.com/zero-post/python-pypi-supply-chain-attack-colorama/&#34;&gt;Checkmarx&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;pypi-credential-phishing-led-to-real-account-compromise-and-malicious-releases-of-a-legit-project-num2words&#34;&gt;PyPI credential-phishing led to real account compromise and malicious releases of a legit project (&lt;code&gt;num2words&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;What happened:&lt;/em&gt; PyPI reported an email phishing campaign using a lookalike domain; 4 accounts were successfully phished, attacker-generated API tokens were revoked, and malicious releases of &lt;code&gt;num2words&lt;/code&gt; were uploaded then removed. This is the “steal maintainer creds, ship malware via trusted package name” playbook. Source: &lt;a href=&#34;https://blog.pypi.org/posts/2025-07-31-incident-report-phishing-attack/&#34;&gt;Python Package Index Blog&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;silentsync-rat-delivered-via-malicious-pypi-packages-sisaws-secmeasure&#34;&gt;SilentSync RAT delivered via malicious PyPI packages (&lt;code&gt;sisaws&lt;/code&gt;, &lt;code&gt;secmeasure&lt;/code&gt;)&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;What happened:&lt;/em&gt; Zscaler documented malicious packages (including typosquatting) that deliver a Python-based remote access trojan (RAT) with command execution, file exfiltration, screen capture, and browser data theft (credentials, cookies, etc.). Source: &lt;a href=&#34;https://www.zscaler.com/blogs/security-research/malicious-pypi-packages-deliver-silentsync-rat&#34;&gt;Zscaler&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;integrating-pip-audit&#34;&gt;Integrating pip-audit&lt;/h2&gt;
&lt;p&gt;Those are definitely scary situations. I&amp;rsquo;m sure you&amp;rsquo;ve heard about typo squatting and how annoying that can be. Caution will save you there. Where caution will not save you is when a legitimate package has its supply chain taken over. A lot of times this could look like a package that you use depends on another package whose maintainer was phished. And now everything that uses that library is carrying that vulnerability forward.&lt;/p&gt;
&lt;p&gt;Enter &lt;a href=&#34;https://github.com/pypa/pip-audit&#34;&gt;pip-audit&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;pip-audit&lt;/strong&gt; is great because you can just run it on the command line. It will check against PyPA&amp;rsquo;s official list of vulnerabilities and tell you if anything in your virtual environment or requirements files is known to be malicious.&lt;/p&gt;
&lt;p&gt;You could even set up a GitHub Action to do so, and I wouldn&amp;rsquo;t recommend against that at all. But it&amp;rsquo;s also valuable to make this check happen on developers&amp;rsquo; machines. It&amp;rsquo;s a simple two-step process to do so:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Add pip-audit to your project&amp;rsquo;s development dependencies or install it globally with &lt;code&gt;uv tool install pip-audit&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Create a unit test that simply shells out to execute pip-audit and fails the test if an issue is found.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Part one&amp;rsquo;s easy. Part two takes a little bit more work. That&amp;rsquo;s okay, because I got it for you. Just download the file here and drop it in your pytest test directory:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://gist.github.com/mikeckennedy/de70ce13231b407a8dccea758f83a5cd&#34;&gt;&lt;strong&gt;test_pypi_security_audit.py&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a small segment to give you a sense of what&amp;rsquo;s involved.&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;test_pip_audit_no_vulnerabilities&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;	  &lt;span class=&#34;c1&#34;&gt;# setup ...&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;c1&#34;&gt;# Run pip-audit with JSON output for easier parsing&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;try&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;result&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;subprocess&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;run&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;[&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;n&#34;&gt;sys&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;executable&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;s1&#34;&gt;&amp;#39;-m&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;s1&#34;&gt;&amp;#39;pip_audit&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;s1&#34;&gt;&amp;#39;--format=json&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;s1&#34;&gt;&amp;#39;--progress-spinner=off&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;s1&#34;&gt;&amp;#39;--ignore-vuln&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;s1&#34;&gt;&amp;#39;CVE-2025-53000&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# example of skipping an irrelevant cve&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                &lt;span class=&#34;s1&#34;&gt;&amp;#39;--skip-editable&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# don&amp;#39;t test your own package in dev&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;p&#34;&gt;],&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;cwd&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;project_root&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;capture_output&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;True&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;text&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;True&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;            &lt;span class=&#34;n&#34;&gt;timeout&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;120&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt;  &lt;span class=&#34;c1&#34;&gt;# 2 minute timeout&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;except&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;subprocess&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;TimeoutExpired&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;pytest&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fail&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;pip-audit command timed out after 120 seconds&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;except&lt;/span&gt; &lt;span class=&#34;ne&#34;&gt;FileNotFoundError&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;n&#34;&gt;pytest&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;fail&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;pip-audit not installed or not accessible&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That&amp;rsquo;s it! When anything runs your unit test, whether that&amp;rsquo;s continuous integration, a git hook, or just a developer testing their code, you&amp;rsquo;ll also run a pip-audit audit of your project.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-supply-chain-security-made-easy/pip-audit-post.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;let-others-find-out&#34;&gt;Let others find out&lt;/h2&gt;
&lt;p&gt;Now, pip-audit tests if a malicious package has been installed, In which case, for that poor developer or machine, it may be too late. If it&amp;rsquo;s CI, who cares? But one other feature you can combine with this that is really nice is &lt;a href=&#34;https://docs.astral.sh/uv&#34;&gt;uv&lt;/a&gt;&amp;rsquo;s ability to put a delay on upgrading your dependencies.&lt;/p&gt;
&lt;p&gt;Many developers, myself included, will typically run some kind of command that will pin your versions. Periodically we also run a command that looks for newer libraries and updates pinned versions so we&amp;rsquo;re using the latest code. So this way you upgrade in a stair-step manner at the time you&amp;rsquo;re intending to change versions.&lt;/p&gt;
&lt;p&gt;This works great. However, what if the malicious version of a package is released five minutes before before you run this command. You&amp;rsquo;re getting it installed. But pretty soon, the community is going to find out that something is afoot, report it, and it will be yanked from PyPI. Here bad timing got you hacked.&lt;/p&gt;
&lt;p&gt;While it&amp;rsquo;s not a guaranteed solution, certainly Defense In Depth would tell us maybe wait a few days to install a package. But you don&amp;rsquo;t want to review packages manually one by one, do you? For example, for &lt;a href=&#34;https://training.talkpython.fm&#34;&gt;Talk Python Training&lt;/a&gt;, we have over 200 packages for that website. It would be an immense hassle to verify the dates of each one and manually pick the versions.&lt;/p&gt;
&lt;p&gt;No need! We can just add a simple delay to our uv command:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;uv pip compile requirements.piptools --upgrade --output-file requirements.txt --exclude-newer &lt;span class=&#34;s2&#34;&gt;&amp;#34;1 week&amp;#34;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;In particular, notice &lt;strong&gt;&amp;ndash;exclude-newer &amp;ldquo;1 week&amp;rdquo;&lt;/strong&gt;. The exact duration isn&amp;rsquo;t the important thing. It&amp;rsquo;s about putting a little bit of a delay for issues to be reported into your workflow. You can &lt;a href=&#34;https://docs.astral.sh/uv/concepts/resolution/&#34;&gt;read about the full feature here&lt;/a&gt;. This way, we only incorporate packages that have survived in the public on PyPI for at least one week.&lt;/p&gt;
&lt;h2 id=&#34;part-2&#34;&gt;Part 2&lt;/h2&gt;
&lt;p&gt;Be sure to check out the follow up post &lt;a href=&#34;https://mkennedy.codes/posts/devops-python-supply-chain-security/&#34;&gt;DevOps Python Supply Chain Security&lt;/a&gt; for even more tips.&lt;/p&gt;
&lt;p&gt;Hope this helps. Stay safe out there.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>This Course Has Its Own Soundtrack</title>
            <link>https://mkennedy.codes/posts/this-course-has-its-own-soundtrack/</link>
            <pubDate>Sat, 25 Oct 2025 08:00:31 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/this-course-has-its-own-soundtrack/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;agentic-ai-soundtrack-shadows.webp&#34; alt=&#34;&#34;&gt;
Here&amp;rsquo;s a first: My latest course, &lt;a href=&#34;https://training.talkpython.fm/courses/agentic-ai-programming-for-python&#34;&gt;Agentic AI Programming for Python Devs&lt;/a&gt;, &lt;strong&gt;comes with a 5-track soundtrack&lt;/strong&gt;!&lt;/p&gt;
&lt;h2 id=&#34;wondering-why-a-soundtrack&#34;&gt;Wondering why a soundtrack?&lt;/h2&gt;
&lt;p&gt;Why in the world would you create a soundtrack for a course? This course has a few odd quiet, silent sections. Most of it is standard Michael talking style. But when Claude is working hard, sometimes I want you all to just see what it&amp;rsquo;s doing and I want to get out of the way. This isn&amp;rsquo;t a lot, but it&amp;rsquo;s enough that&amp;rsquo;d rather have something interesting while Claude is working.&lt;/p&gt;
&lt;p&gt;So I created a soundtrack!&lt;/p&gt;
&lt;p&gt;Then, I started using to also indicate that the AI is working more generally. In the course, when you hear some flow-state/chill music, you know the AI is working.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example from the course (small excerpt). Hit play and give it a listen/watch:&lt;/p&gt;
&lt;p&gt;&lt;video controls preload=&#34;none&#34; style=&#34;width: 100%; border-radius: 10px;&#34; src=&#34;https://video-cdn.talkpython.fm/0_marketing_videos/agentic-ai-with-soundtrack.mp4?token=m35yNwJAEB9vRk5Z-YhezP_FndVcZhe3Ov1Hkhg6XRY&amp;expires=2072366152&#34; 
poster=&#34;agentic-ai-with-soundtrack-poster.webp&#34;/&gt;&lt;/p&gt;
&lt;h2 id=&#34;its-great-for-coding-music-too&#34;&gt;It&amp;rsquo;s great for coding music too&lt;/h2&gt;
&lt;p&gt;While it&amp;rsquo;s very fun and different for the course (&lt;a href=&#34;https://training.talkpython.fm/courses/agentic-ai-programming-for-python&#34;&gt;you should consider taking it&lt;/a&gt; ;) ), I also made the full 5-track series available for download even if you&amp;rsquo;re not interested in the course. It&amp;rsquo;s really chill and turns out to be good focus music for coding. Have a listen to #2:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;#2: Dev Flow, Beat Craft&lt;/strong&gt;&lt;br /&gt;
&lt;audio src=&#34;https://blobs.talkpython.fm/general/02-agentic-ai-devflow-beatcraft.mp3?cache_id=a6d028&#34; controls preload=&#34;none&#34; style=&#34;width: 100%;&#34; /&gt;&lt;/p&gt;
&lt;h2 id=&#34;download-the-soundtrack&#34;&gt;Download the soundtrack&lt;/h2&gt;
&lt;p&gt;You can download the all tracks at&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://talkpython.fm/agentic-ai-soundtrack.zip&#34;&gt;talkpython.fm/agentic-ai-soundtrack.zip&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Enjoy!&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>AI Usage TUI</title>
            <link>https://mkennedy.codes/posts/ai-usage-tui/</link>
            <pubDate>Fri, 24 Oct 2025 08:51:10 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/ai-usage-tui/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/ai-usage-tui/usage-shadows.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Track your AI coding credits with &lt;a href=&#34;https://github.com/mikeckennedy/aiusage&#34;&gt;AI Usage TUI&lt;/a&gt;. See if you&amp;rsquo;re on pace to run out before your renewal day. Install with &lt;code&gt;uv tool install aiusage&lt;/code&gt; and run &lt;code&gt;aiusage&lt;/code&gt; to start tracking.&lt;/p&gt;
&lt;p&gt;I just published a new open source tool: &lt;a href=&#34;https://github.com/mikeckennedy/aiusage&#34;&gt;AI Usage TUI&lt;/a&gt;. The picture above tells most of the tail. But in this age of agentic AI coding, you&amp;rsquo;ll hear many complaints along the lines of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://www.reddit.com/r/vibecoding/comments/1l0p27i/anyone_else_burning_way_too_many_ai_credits_just/&#34;&gt;Anyone else burning way too many AI credits just to get a decent UI?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.reddit.com/r/Jetbrains/comments/1nfp2qy/ai_credits_expiring_within_hours/&#34;&gt;AI Credits expiring within hours&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.reddit.com/r/cursor/comments/1m4k9t5/did_i_use_up_all_my_cursor_pro_credits_for_the/&#34;&gt;Did I use up all my Cursor Pro credits for the month?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.reddit.com/r/Jetbrains/comments/1ny1wfw/ai_credits_used_while_i_slept/&#34;&gt;AI credits used while I slept!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.reddit.com/r/cursor/comments/1mvc38s/cursor_credits_draining_way_faster_now/&#34;&gt;Cursor credits draining way faster now??&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.reddit.com/r/cursor/comments/1nh42pq/how_do_you_use_all_your_credits/&#34;&gt;How do you use all your credits?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.reddit.com/r/cursor/comments/1nb3rue/how_to_not_burn_all_credits_in_4h/&#34;&gt;How to not burn all credits in 4h?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;And so on&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Wouldn&amp;rsquo;t it be great if you could just &lt;strong&gt;predict easily how fast your credits are vanishing&lt;/strong&gt; and if you&amp;rsquo;re going to run out before your credits reset (typically some fixed day of the month)?&lt;/p&gt;
&lt;p&gt;On the glass is half full side of things, i&lt;strong&gt;f you are going to end up with 50% of your credits unspent and not rolling over, maybe tackle a big project&lt;/strong&gt; or use a smarter (but more expensive credits-wise) model!&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s why I created &lt;strong&gt;ai usage&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;AI Usage 💳&lt;/strong&gt;&lt;br/&gt;
&lt;em&gt;A smart command-line tool for tracking AI credit consumption across billing cycles. Designed for AI-powered development tools like Cursor, GitHub Copilot, and other subscription-based AI services.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s available on GitHub and PyPI. But I recommend you just install it with &lt;a href=&#34;https://docs.astral.sh/uv/&#34;&gt;uv&lt;/a&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;uv tool install aiusage
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Run it once with just &lt;code&gt;aiusage&lt;/code&gt; on the CLI and it&amp;rsquo;ll remember your settings (like renewal day) and then you can have it predict your current credit burn-rate.&lt;/p&gt;
&lt;p&gt;Read more about it on the &lt;a href=&#34;https://github.com/mikeckennedy/aiusage&#34;&gt;README.md&lt;/a&gt; for the project.&lt;/p&gt;
&lt;p&gt;If you find this useful, please give it a shoutout and cc me on social media: &lt;a href=&#34;https://bsky.app/profile/mkennedy.codes&#34;&gt;BSky&lt;/a&gt;, &lt;a href=&#34;https://fosstodon.org/@mkennedy&#34;&gt;Mastodon&lt;/a&gt;, &lt;a href=&#34;https://x.com/mkennedy&#34;&gt;X&lt;/a&gt;&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Course: Agentic AI for Python Devs</title>
            <link>https://mkennedy.codes/posts/agentic-ai-programming-for-python-devs-course/</link>
            <pubDate>Thu, 23 Oct 2025 14:48:52 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/agentic-ai-programming-for-python-devs-course/</guid>
            <description>&lt;p&gt;&lt;a href=&#34;https://training.talkpython.fm/courses/agentic-ai-programming-for-python&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/agentic-ai-programming-for-python-devs-course/agentic-ai.webp&#34; alt=&#34;&#34;&gt;&lt;/a&gt;
I just published a brand new course over at &lt;a href=&#34;https://talkpython.fm&#34;&gt;Talk Python&lt;/a&gt;: &lt;a href=&#34;https://training.talkpython.fm/courses/agentic-ai-programming-for-python&#34;&gt;Agentic AI Programming for Python Devs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This course teaches you how to collaborate with &lt;strong&gt;agentic AI tools&lt;/strong&gt;, not just chatbots or autocomplete, but AI that can understand your entire project, execute commands, run tests, format code, and build complete features autonomously. You&amp;rsquo;ll learn to guide these tools like you would a talented junior developer on your team, setting up the right guardrails and roadmaps so they consistently deliver well-structured, maintainable code that matches your standards. Think of it as pair programming with an AI partner who learns your preferences, follows your conventions, and gets more effective the better you communicate.&lt;/p&gt;
&lt;p&gt;I think you&amp;rsquo;ll find it immensely valuable. Check it out over at &lt;a href=&#34;https://talkpython.fm/agentic-ai&#34;&gt;talkpython.fm/agentic-ai&lt;/a&gt;&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Show me your ls</title>
            <link>https://mkennedy.codes/posts/show-me-your-ls/</link>
            <pubDate>Tue, 14 Oct 2025 13:24:59 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/show-me-your-ls/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Replace your default &lt;code&gt;ls&lt;/code&gt; with &lt;a href=&#34;https://pls.cli.rs&#34;&gt;pls&lt;/a&gt; for icons, colors, human-readable sizes, and .gitignore-aware output. Just &lt;code&gt;alias ls=&amp;quot;pls --details size&amp;quot;&lt;/code&gt; and install a NerdFont.&lt;/p&gt;
&lt;p&gt;Getting comfortable in the terminal is one of those coming-of-age moments for developers and data scientists. Whenever I open the default terminal on macOS or Linux, I&amp;rsquo;m shocked at just how bad it is. There are many tools and alternative terminal apps (&lt;a href=&#34;https://ohmyz.sh&#34;&gt;OhMyZSH&lt;/a&gt;, &lt;a href=&#34;https://ohmyz.sh&#34;&gt;Warp&lt;/a&gt;, &lt;strong&gt;Windows Terminal&lt;/strong&gt; [defunct link: https://apps.microsoft.com/detail/9n0dx20hk701?hl=en-US&amp;amp;gl=US] to name a few).&lt;/p&gt;
&lt;p&gt;With this essay, I thought it&amp;rsquo;d be fun to focus on something as minuscule as the basic &lt;code&gt;ls&lt;/code&gt; command (that&amp;rsquo;s &lt;code&gt;dir&lt;/code&gt; for my windows friends).&lt;/p&gt;
&lt;p&gt;Let me just &lt;strong&gt;set the lower bar&lt;/strong&gt; for how &lt;em&gt;meh&lt;/em&gt; this can be. Here is what you get from a $3,000 MacBook Pro running the latest macOS:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/show-me-your-ls/og-ls.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;It works, but it sure doesn&amp;rsquo;t inspire.&lt;/p&gt;
&lt;h2 id=&#34;heres-my-ls-today&#34;&gt;Here&amp;rsquo;s my ls today&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;Size, human-readable (KB vs. MB, etc.)&lt;/li&gt;
&lt;li&gt;Icons (via &lt;a href=&#34;https://www.nerdfonts.com&#34;&gt;NerdFonts.com&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Hidden files shown/omitted driven by developer-focused conventions and the &lt;code&gt;.gitignore&lt;/code&gt; file&lt;/li&gt;
&lt;li&gt;Color coding based on &lt;code&gt;.gitignore&lt;/code&gt; and more&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/show-me-your-ls/show-me-your-ls.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;This is simply aliasing &lt;a href=&#34;https://pls.cli.rs&#34;&gt;the great pls library&lt;/a&gt; to &lt;code&gt;ls&lt;/code&gt; (&lt;code&gt;alias ls=&amp;quot;pls --details size&amp;quot;&lt;/code&gt;) and using the details flag. The bare &lt;code&gt;pls&lt;/code&gt; is good too.&lt;/p&gt;
&lt;h2 id=&#34;now-show-me-your-ls&#34;&gt;Now show me your ls&lt;/h2&gt;
&lt;p&gt;Share a screenshot and back story with &lt;em&gt;your ls&lt;/em&gt; on one of the social posts about this essay.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;X:  &lt;a href=&#34;https://x.com/mkennedy/status/1978196983803531533&#34;&gt;https://x.com/mkennedy/status/1978196983803531533&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Mastodon: &lt;a href=&#34;https://fosstodon.org/@mkennedy/115374424791253424&#34;&gt;https://fosstodon.org/@mkennedy/115374424791253424&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Bluesky: &lt;a href=&#34;https://bsky.app/profile/mkennedy.codes/post/3m36l6hosz22t&#34;&gt;https://bsky.app/profile/mkennedy.codes/post/3m36l6hosz22t&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Primed: Should You Hype Your AI Before You Start?</title>
            <link>https://mkennedy.codes/posts/primed-should-you-hype-your-ai-before-you-start/</link>
            <pubDate>Mon, 13 Oct 2025 17:44:50 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/primed-should-you-hype-your-ai-before-you-start/</guid>
            <description>&lt;p&gt;I&amp;rsquo;ve been having some amazing successes with agentic AI and coding lately. A run-down post on these would be interesting. You can find one write-up at &lt;a href=&#34;https://mkennedy.codes/posts/goodbye-wordpress-thanks-ai/&#34;&gt;Goodbye Wordpress, thanks AI&lt;/a&gt;. With this as the backdrop, let me ask you a question.&lt;/p&gt;
&lt;h2 id=&#34;should-you-amp-up-your-ai-at-the-start-of-a-project&#34;&gt;Should you amp up your AI at the start of a project?&lt;/h2&gt;
&lt;p&gt;We&amp;rsquo;ve heard the debates about whether you should be polite and thank your AI. I think yes but never as a separate follow up comment. But &lt;strong&gt;should we try to get it HYPED and excited to work on the project&lt;/strong&gt;?&lt;/p&gt;
&lt;p&gt;I just published &lt;a href=&#34;https://mkennedy.codes/posts/talk-python-in-production-book-is-out/&#34;&gt;my first solo book&lt;/a&gt;. One of the challenges is that on the &lt;a href=&#34;https://talkpython.fm/books/python-in-production/buy&#34;&gt;buy page&lt;/a&gt; there is a simple pair of images/buttons: &lt;strong&gt;Buy on Gumroad&lt;/strong&gt; and &lt;strong&gt;Buy on Amazon&lt;/strong&gt;. Things are rarely as simple as they seem. The Kindle version of the book is available in 12 different locales (US, Canada, Germany, etc.). Each one of these is a different URL based on the location of the web visitor! That means the website &lt;em&gt;should&lt;/em&gt; adapt to each visitor and point them at their store if possible. To accomplish this, I combined some magic from &lt;a href=&#34;https://dev.maxmind.com/geoip/geolite2-free-geolocation-data/&#34;&gt;GeoIpLite&lt;/a&gt;, &lt;a href=&#34;https://github.com/grantjenks/python-diskcache?featured_on=pythonbytes&#34;&gt;diskcache&lt;/a&gt;, and a text file full of links to various Amazon stores&amp;rsquo; listings of my book. Rather than grinding through this, I asked Cursor and Claude Sonnet 4.5 to help make &lt;a href=&#34;https://talkpython.fm/books/python-in-production/buy&#34;&gt;that page&lt;/a&gt; dynamic.&lt;/p&gt;
&lt;p&gt;I am finding huge success if I have a top-tier model work with me to create a detailed, reviewed plan. Then have the AI work step by step through the plan.&lt;/p&gt;
&lt;p&gt;I was excited for the book and wanted my coding buddy to share in my excitement!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/primed-should-you-hype-your-ai-before-you-start/ready-to-rock.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;And it delivered! &amp;ldquo;I&amp;rsquo;m absolutely ready to rock! 🎸&amp;rdquo;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m really enjoying this. I think going forward, each time I kick off one of these detailed, plan-based projects, &lt;strong&gt;I am going to hype my AI coding buddy&lt;/strong&gt;. Even if it doesn&amp;rsquo;t make a difference, it makes it more fun. :)&lt;/p&gt;
&lt;p&gt;BTW, I&amp;rsquo;m working on an Agentic Coding course at Talk Python. &lt;a href=&#34;https://training.talkpython.fm/getnotified&#34;&gt;Join the mailing list&lt;/a&gt; if that sounds interesting. There will be plenty of excitement there too.&lt;/p&gt;
&lt;p&gt;Cheers&lt;br /&gt;Michael&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Talk Python in Production book is out!</title>
            <link>https://mkennedy.codes/posts/talk-python-in-production-book-is-out/</link>
            <pubDate>Sat, 11 Oct 2025 22:12:52 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/talk-python-in-production-book-is-out/</guid>
            <description>&lt;p&gt;&lt;a href=&#34;https://talkpython.fm/books/python-in-production&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/talk-python-in-production-book-is-out/talk-python-in-production-cover.webp&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m thrilled to share that my first solo book is published: &lt;a href=&#34;https://talkpython.fm/books/python-in-production&#34;&gt;Talk Python in Production&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&#34;heres-the-deal&#34;&gt;Here&amp;rsquo;s the deal&lt;/h3&gt;
&lt;p&gt;Much of the advice we get about running our apps and APIs on the internet is overly complex. &lt;strong&gt;Many people and organizations are selling you this complexity&lt;/strong&gt;. The hyperscale clouds see their lock-in of your app to their 100+ services as a golden ticket. And there are plenty of developers out there who see it as a flex and credibility boost to show how they are using the latest mix of these services regardless of whether they are the best fit.&lt;/p&gt;
&lt;p&gt;Leonardo da Vinci &lt;a href=&#34;https://www.goodreads.com/quotes/9010638-simplicity-is-the-ultimate-sophistication-when-once-you-have-tasted&#34;&gt;knew&lt;/a&gt; that &amp;ldquo;&lt;strong&gt;Simplicity is the ultimate sophistication&lt;/strong&gt;&amp;rdquo;. No, he never wrote a line of code. He never even vibe-coded an app. His advice still applies to software.&lt;/p&gt;
&lt;p&gt;At its core, &lt;strong&gt;this book is two things&lt;/strong&gt;:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A story arc, 10 years in the making, of how I run Talk Python and the associated 20 or so services, APIs, and more today. I started as a total noob, apprehensive of Linux. Today, it&amp;rsquo;s a testament to how much can be accomplished as a very small team spending 100s of dollars, not $10,000s+.&lt;/li&gt;
&lt;li&gt;A detailed, step-by-step guide to transform your set of applications into simpler, lock-in-free infrastructure that is easily understandable and repeatable.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I hope you will &lt;a href=&#34;https://talkpython.fm/books/python-in-production&#34;&gt;join me&lt;/a&gt; on this journey.&lt;/p&gt;
&lt;h3 id=&#34;more-than-your-standard-book&#34;&gt;More than your standard book&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;ve worked really hard to &lt;strong&gt;provide you with extra resources that most tech books ignore&lt;/strong&gt;. Here are some extras that&amp;rsquo;ll help you get more out of Talk Python in Production:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/mikeckennedy/talk-python-in-production-devops-book/tree/main/galleries/code-gallery&#34;&gt;Code gallery&lt;/a&gt;: Contains all the code blocks from the book organized by chapter. Each code block is listed with its language and can be easily copied and used. No need to hunt through the book for code blocks. They are all in one place. This is in the book itself and on GitHub.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/mikeckennedy/talk-python-in-production-devops-book/tree/main/galleries/figure-gallery&#34;&gt;Figure gallery&lt;/a&gt;: Ditto for images. But it&amp;rsquo;s even more important here. I&amp;rsquo;ve published the original max quality images that cannot be put into Kindle devices or would just be hard to see on many EPUB readers. This is in the book itself and on GitHub.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/mikeckennedy/talk-python-in-production-devops-book/tree/main/galleries/links-gallery&#34;&gt;Resources and Links Gallery&lt;/a&gt;: Contains all external links and resources referenced in the book. Links are organized by chapter for easy reference. Each link shows the text as it appears in the book along with the full URL. This is in the book itself and on GitHub.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/mikeckennedy/talk-python-in-production-devops-book/discussions&#34;&gt;Discussion forum&lt;/a&gt;: A dedicated discussion forum where you can discuss ideas with other readers of the book.&lt;/li&gt;
&lt;li&gt;Audio Readers&amp;rsquo; Briefs: The book includes a total of 1 hour and 20 minutes of short 2 to 4 minute conversations that bookend each chapter. Use them to prime your focus before reading or to broaden your takeaways after. &lt;a href=&#34;https://blobs.talkpython.fm/readers-brief-preview.mp3&#34; target=&#34;_blank&#34;&gt;Listen to an example&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;how-much-of-this-book-was-written-by-ai&#34;&gt;How much of this book was written by AI?&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Zero percent&lt;/strong&gt;. This book is written entirely by yours truly over a period of 9 months. Yes, some of the artwork is AI-based. The code and prose is all mine.  Don&amp;rsquo;t we live in wild times that this is even a question some people might ask?&lt;/p&gt;
&lt;h3 id=&#34;what-topics-are-covered&#34;&gt;What topics are covered?&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Early and evolving architectures&lt;/strong&gt;: Real-world hosting journeys, from small PaaS approaches to robust multi-VM setups.&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;One big server strategy&lt;/strong&gt;: Why consolidating resources on a powerful VM can outperform many small nodes in practice.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Docker &amp;amp; Docker Compose&lt;/strong&gt;: Practical guides to containerizing Python apps, building images efficiently, and automating deployments.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;NGINX &amp;amp; Let&amp;rsquo;s Encrypt&lt;/strong&gt;: Reverse proxy basics, SSL/TLS certificates, and best practices for production traffic and security.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Self-hosted services&lt;/strong&gt;: Tools like Umami (analytics) and Uptime Kuma (monitoring) that you can run yourself instead of paying for cloud solutions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Performance optimization&lt;/strong&gt;: Setting up caching, using modern Python app servers, integrating a CDN, etc.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Migrations &amp;amp; frameworks&lt;/strong&gt;: Real-life examples of moving between frameworks (Pyramid to Quart) and cloud providers (PythonAnywhere to DigitalOcean and Hetzner).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Stack-native philosophy&lt;/strong&gt;: How to keep your infrastructure streamlined yet powerful and avoid the complexity of deeply &amp;ldquo;cloud-native&amp;rdquo; approaches.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;early-reviews&#34;&gt;&lt;strong&gt;Early reviews&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;I&amp;rsquo;m getting just a few early reviews and they are looking good:&lt;/p&gt;
&lt;p&gt;⭐⭐⭐⭐⭐ 3 ratings on &lt;a href=&#34;https://mikeckennedy.gumroad.com/l/talk-python-in-production-book&#34;&gt;Gumroad&lt;/a&gt; (5-stars)&lt;/p&gt;
&lt;p&gt;And a few folks have shared their feedback.&lt;/p&gt;
&lt;p&gt;&amp;ldquo;&lt;em&gt;I finished reading the book. I really enjoyed it. The massive hyperscale Kubernetes clusters everyone pushes today never made sense to me from a cost and complexity standpoint for the vast majority of cases.&lt;/em&gt;&amp;rdquo; &amp;ndash; Paul&lt;/p&gt;
&lt;p&gt;&amp;ldquo;&lt;em&gt;This is a great book. I have used Python for 20 years. The book follows much of what I would do in my own judgment, and taught me some new tricks too.&amp;rdquo;&lt;/em&gt; &amp;ndash; Chris&lt;/p&gt;
&lt;h3 id=&#34;give-it-a-try&#34;&gt;&lt;strong&gt;Give it a try&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;The book comes with a 30-day refund policy if you get it on Gumroad. You can easily &lt;a href=&#34;https://talkpython.fm/books/python-in-production/buy&#34;&gt;give it a try&lt;/a&gt; for &amp;ldquo;no risk.&amp;rdquo; I hope you enjoy the book. And if you do get it, please send me a review to feature or share how it&amp;rsquo;s changed how you and your team run software (hopefully for the better!).&lt;/p&gt;
&lt;p&gt;Regardless, thanks for the interest and supporting my work. Cheers.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Goodbye Wordpress, thanks AI</title>
            <link>https://mkennedy.codes/posts/goodbye-wordpress-thanks-ai/</link>
            <pubDate>Tue, 30 Sep 2025 16:55:13 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/goodbye-wordpress-thanks-ai/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/goodbye-wordpress-thanks-ai/ai-keyboard.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Used Cursor + Claude Sonnet 4 to migrate 164 Wordpress articles to Hugo in 2 hours. AI handled downloading, converting to markdown, relinking images, and building NGINX redirects. Finally deleted my old Wordpress site and stopped paying $48/yr.&lt;/p&gt;
&lt;p&gt;To all the naysayers who think AI coding is just a bunch of slop and we&amp;rsquo;re better off just avoiding it, &lt;strong&gt;I want to share a story of massive success with agentic coding&lt;/strong&gt;. This is just one of five examples of similar scale I had in one month.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been blogging and writing online for just shy of 20 years, incredible. Yes, it&amp;rsquo;s been on again / off again with gaps here and there. But over the years, I&amp;rsquo;ve written 199 separate articles. 164 of them were on my older blog hosted under my personal domain on wordpress.com.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m a fan of picking whatever tech gets you writing and in the early days that was Wordpress. I know many of us choose a tech that is in a language / framework we work in. But I get plenty of Python coding over at &lt;a href=&#34;https://talkpython.fm&#34;&gt;talkpython.fm&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In 2022, I wanted something simpler than Wordpress and Markdown-based so I switched to &lt;a href=&#34;https://gohugo.io&#34;&gt;Hugo&lt;/a&gt; hosted right here at &lt;a href=&#34;https://mkennedy.codes&#34;&gt;mkennedy.codes&lt;/a&gt;. Hugo is amazing and it&amp;rsquo;s been great. But then came the $48/yr renewal once again from Wordpress.com. It&amp;rsquo;s not a lot of money but am I seriously going to be held hostage for the rest of my life? Maybe. I don&amp;rsquo;t want those older articles to vanish. But the thought of migrating to Hugo and markdown, including all the linked files and more, was just too much to justify the time and energy. So I&amp;rsquo;ve been renewing my custom domain fee again and again.&lt;/p&gt;
&lt;p&gt;Then I had the thought, &amp;ldquo;I bet &lt;a href=&#34;https://cursor.com&#34;&gt;Cursor&lt;/a&gt; can migrate the site.&amp;rdquo; I was sitting around one of the fleeting summer nights recently and thought why not let Cursor and Claude&amp;rsquo;s Sonnet-4 have a go at it.&lt;/p&gt;
&lt;p&gt;I gave it a very clear plan on getting the content from my old domain, migrating the content to markdown in Hugo, and copying and relinking all the binary files and images.&lt;/p&gt;
&lt;p&gt;It took us 2 hours to get everything downloaded, rewritten, and converted. But then I had ALL of my content that I had previously seen has trapped completely convert to my current Hugo site! It didn&amp;rsquo;t happen all at once, but with a bit of iteration, we got it just right.&lt;/p&gt;
&lt;p&gt;I even used a separate URL structure for the archived posts: &lt;code&gt;/posts/r/...&lt;/code&gt; rather than just&lt;code&gt; /posts/...&lt;/code&gt; so it&amp;rsquo;s immediately clear which are the older posts (at least to me).&lt;/p&gt;
&lt;p&gt;The last step was to make sure all the old URLs in other sites and search engines still exactly and safely found their way to the new ones. After all, one of my goals is to concentrate the SEO / GenAI-O into one place rather than two domains.&lt;/p&gt;
&lt;p&gt;Enter AI again, I asked Claude how to redirect and capture the URLs of the form:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;blog.michaelckennedy.net/YYYY/MM/DD/slug&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;The new format would be:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;mkenendy.codes/r/slug&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m pretty good with NGINX but the comprehensive regex to converted these was more than I wanted to write. So I asked AI how to redirect old URLs to new in NGINX. It has the perfect answer:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-nginx&#34; data-lang=&#34;nginx&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Handle blog post redirects
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Pattern: /YYYY/MM/DD/post-slug/ -&amp;gt; /posts/r/post-slug/
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;location&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;~&lt;/span&gt; &lt;span class=&#34;sr&#34;&gt;&amp;#34;^/([0-9]&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;kn&#34;&gt;4})/([0-9]{2})/([0-9]{2})/(.+?)/?$&amp;#34;&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;kn&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;301&lt;/span&gt; &lt;span class=&#34;s&#34;&gt;https://mkennedy.codes/posts/r/&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;$4/&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;;&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;For example, give this old Wordpress URL a click:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://mkennedy.codes/posts/r/a-bunch-of-online-python-courses/&#34;&gt;https://mkennedy.codes/posts/r/a-bunch-of-online-python-courses/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And you&amp;rsquo;ll land via a SEO-friendly 301 at:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://mkennedy.codes/posts/r/a-bunch-of-online-python-courses/&#34;&gt;https://mkennedy.codes/posts/r/a-bunch-of-online-python-courses/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Perfect!&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Here&amp;rsquo;s the takeaway&lt;/strong&gt;: I had this hassle of a split site, most of my articles trapped in the old domain at Wordpress and 3 years of newer ones on Hugo at my current site. I kept getting charged $48/yr just to keep my old links working.&lt;/p&gt;
&lt;p&gt;Enter agentic AI: After two fun hours of coding along with Claude Sonnet 4, I had everything unified and brought over to the newer, lighter style and I had my content living again in my new site.&lt;/p&gt;
&lt;p&gt;The best part? &lt;strong&gt;I just hit delete on my old Wordpress site&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/goodbye-wordpress-thanks-ai/delete-2.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Thanks Claude&lt;/strong&gt;!&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Roosevelt&#39;s Man in the Arena, But for Developers</title>
            <link>https://mkennedy.codes/posts/roosevelt-s-man-in-the-arena-but-for-developers/</link>
            <pubDate>Mon, 19 May 2025 14:40:56 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/roosevelt-s-man-in-the-arena-but-for-developers/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/roosevelt-s-man-in-the-arena-but-for-developers/arena.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;If you enjoy quotes and you&amp;rsquo;re into software development, I think you&amp;rsquo;ll get a kick out this. I&amp;rsquo;m a big fan of Theodore Roosevelt’s &lt;strong&gt;Man in the Arena&lt;/strong&gt; speech / quote. What if he was talking about software development and the challenges and persistence required to succeed there?&lt;/p&gt;
&lt;h2 id=&#34;the-og-theodore-roosevelts-man-in-the-arena&#34;&gt;The OG: Theodore Roosevelt’s Man in the Arena:&lt;/h2&gt;
&lt;p&gt;Original quote (very slighly modernized):&lt;/p&gt;
&lt;p&gt;&lt;em&gt;It is not the critic who counts; not the man who points out how the strong man or woman stumbles, or where the doer of deeds could have done them better. The credit belongs to the those actually in the arena, whose face is marred by dust and sweat and blood; who strives valiantly; who errs, who comes short again and again, because there is no effort without error and shortcoming; but who does actually strive to do the deeds; who knows great enthusiasms, the great devotions; who spends themself in a worthy cause; who at the best knows in the end the triumph of high achievement, and who at the worst, if they fail, at least fails while daring greatly, so that their place shall never be with those cold and timid souls who neither know victory nor defeat.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;now-for-software-developers-&#34;&gt;Now, For Software Developers ;)&lt;/h2&gt;
&lt;p&gt;Here are three variants. Pick your favorite.&lt;/p&gt;
&lt;h3 id=&#34;legacy-code-warriors&#34;&gt;Legacy Code Warriors&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;It is not the keyboard-warrior of comment threads who elevates the craft, but the engineer whose IDE still glows at midnight, whose mind is seared by stack-trace hieroglyphs, and whose resolve endures failed build after failed build. The honor rests with those who wade into legacy code knee-deep in technical debt, emerging grimy but triumphant with a cleaner architecture. Or, if defeated, bearing the proud scars of having fought for elegance.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;the-refactorer&#34;&gt;The Refactorer&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;History will not inscribe the names of those who idly mock version bumps from the safety of evergreen branches; it will memorialize the maintainer who shepherds migrating APIs across chasms of breaking changes, committing line by weary line while alarms of regression clang in their ears. Should the migration succeed, they savor a quiet victory; should it falter, they rest in the knowledge that progress is purchased with audacity, not armchair critique.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;open-source-maintainers&#34;&gt;Open source maintainers&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;No glory clings to the spectator who counts another’s failed builds; it crowns the open-source contributor whose pull request is battle-scarred by review, whose changelog tells of failures endured, and whose merged code becomes the unseen engine of tomorrow’s discoveries.&lt;/em&gt;&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Sunsetting Search?</title>
            <link>https://mkennedy.codes/posts/sunsetting-search/</link>
            <pubDate>Wed, 26 Mar 2025 16:52:39 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/sunsetting-search/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/sunsetting-search/sunsetting-search.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Have you noticed a change in your search engine use? I definitely have and it&amp;rsquo;s a strong downward trend.&lt;/p&gt;
&lt;p&gt;Last year I saw a stat that said the average person does fewer than 300 searches per month. I thought that was insanely low for my search usage and probably was very low for most developers and other tech people.&lt;/p&gt;
&lt;p&gt;I just checked for myself and in March 2025 across all of my devices (computers and mobile), it is 211.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Just 211&lt;/strong&gt;! Unbelievable. Were did all the searches go? I can only speculate for most people and be concrete for myself. They went into Chat. I don&amp;rsquo;t think I rely too heavily on AI, neither for coding or information in general. But it really struck me recently how much AI has changed the game. You can also clearly see this in &lt;a href=&#34;https://trends.stackoverflow.co/?tags=java%2Cc%2Cpython%2Cc%23%2Cvb.net%2Cjavascript%2Cassembly%2Cphp%2Cperl%2Cruby%2Cswift%2Cr%2Cobjective-c&#34;&gt;StackOverflow language trends&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This matters for me personally because it&amp;rsquo;s an interesting pattern change. It matters for &lt;a href=&#34;https://talkpython.fm&#34;&gt;my business&lt;/a&gt; because discovery via search is an important flow of customers and users.&lt;/p&gt;
&lt;p&gt;Three years ago I posed the question &amp;ldquo;&lt;a href=&#34;https://mkennedy.codes/posts/paying-for-search-in-2022-am-i-crazy/&#34;&gt;Paying for search, am I crazy?&lt;/a&gt;&amp;rdquo; The answer, for me at least, was definitely not. It was worth it. Today I&amp;rsquo;m not so sure. I&amp;rsquo;m trying out a privacy-focused search engine from the Netherlands called &lt;a href=&#34;https://www.startpage.com/&#34;&gt;Startpage&lt;/a&gt;. It&amp;rsquo;s not Kagi level of good, but my most important searchs don&amp;rsquo;t seem to go into either anymore.&lt;/p&gt;
&lt;p&gt;We do live in interesting times, don&amp;rsquo;t we?&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Share your thoughts? Discuss this article on &lt;a href=&#34;https://fosstodon.org/@mkennedy/114230714468536446&#34;&gt;Mastodon&lt;/a&gt;, &lt;a href=&#34;https://bsky.app/profile/mkennedy.codes/post/3llco5iduqk2u&#34;&gt;BlueSky&lt;/a&gt;, or &lt;a href=&#34;https://www.linkedin.com/posts/mkennedy_sunsetting-search-activity-7310767945280864257-AJA9?utm_source=share&amp;amp;utm_medium=member_desktop&amp;amp;rcm=ACoAAABOjqABPkOWTTbZXV9tmnQohvpkplQOibU&#34;&gt;LinkedIn&lt;/a&gt;.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Michael&#39;s Top Trends of 2024 for Python Web Devs</title>
            <link>https://mkennedy.codes/posts/michaels-top-trends-of-2024-for-python-web-devs/</link>
            <pubDate>Sat, 21 Dec 2024 08:54:32 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/michaels-top-trends-of-2024-for-python-web-devs/</guid>
            <description>&lt;p&gt;I wrote &lt;a href=&#34;https://blog.jetbrains.com/pycharm/2024/12/the-state-of-python/&#34;&gt;an in-depth article&lt;/a&gt; covering my top trends for 2024 based on the PSF Developer Survey data. Here are the top trends in a list, but you should definitely &lt;a href=&#34;https://blog.jetbrains.com/pycharm/2024/12/the-state-of-python/&#34;&gt;check out the full article&lt;/a&gt; for the pictures and the analysis.&lt;/p&gt;
&lt;h2 id=&#34;top-trends-for-python-web-devs&#34;&gt;Top Trends for Python Web Devs&lt;/h2&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a href=&#34;https://blog.jetbrains.com/pycharm/2024/12/the-state-of-python/#trend-1-python-usage-with-other-languages-drops-as-general-adoption-grows&#34;&gt;Python usage with other languages drops as general adoption grows&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://blog.jetbrains.com/pycharm/2024/12/the-state-of-python/#trend-2-41-of-python-developers-have-under-2-years-of-experience&#34;&gt;41% of Python developers have under 2 years of experience&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://blog.jetbrains.com/pycharm/2024/12/the-state-of-python/#trend-3-python-learning-expands-through-diverse-channels&#34;&gt;Python learning expands through diverse channels&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://blog.jetbrains.com/pycharm/2024/12/the-state-of-python/#trend-4-the-python-2-vs.-3-divide-is-in-the-distant-past&#34;&gt;The Python 2 vs. 3 divide is in the distant past&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://blog.jetbrains.com/pycharm/2024/12/the-state-of-python/#trend-5-flask,-django,-and-fastapi-remain-top-python-web-frameworks&#34;&gt;Flask, Django, and FastAPI remain top Python web frameworks&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://blog.jetbrains.com/pycharm/2024/12/the-state-of-python/#trend-6-most-python-web-apps-run-on-hyperscale-clouds&#34;&gt;Most Python web apps run on hyperscale clouds&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://blog.jetbrains.com/pycharm/2024/12/the-state-of-python/#trend-7-containers-over-vms-over-hardware&#34;&gt;Containers over VMs over hardware&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://blog.jetbrains.com/pycharm/2024/12/the-state-of-python/#trend-8-uv-takes-python-packaging-by-storm&#34;&gt;uv takes Python packaging by storm&lt;/a&gt;.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Share &lt;strong&gt;your number one trend&lt;/strong&gt; on the &lt;a href=&#34;https://fosstodon.org/@mkennedy/113691870492973522&#34;&gt;Mastodon&lt;/a&gt; and &lt;a href=&#34;https://bsky.app/profile/mkennedy.codes/post/3ldtesuuqyc2h&#34;&gt;BSky&lt;/a&gt; announcements.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Talk Python migrated to Quart web framework</title>
            <link>https://mkennedy.codes/posts/talk-python-migrated-from-pyramid-to-quart-web-framework/</link>
            <pubDate>Tue, 19 Nov 2024 10:04:03 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/talk-python-migrated-from-pyramid-to-quart-web-framework/</guid>
            <description>&lt;p&gt;Over at Talk Python, I just finished converting our existing web app from Pyramid to Quart and from synchronous to asynchronous web code. And I did a big write up in case it helps anyone on similar journeys:&lt;/p&gt;
&lt;p&gt;From the &lt;a href=&#34;https://talkpython.fm/blog/posts/talk-python-rewritten-in-quart-async-flask/&#34;&gt;Talk Python blog&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The code powering talkpython.fm is highly modern and leverages many new Python concepts. It makes extensive use of Pydantic with its entire data access layer powered the Beanie ODM. It has type hints at all the architectural boundaries (e.g. data access layer public functions). But we haven’t been able to fully take advantage of these benefits because our web framework has become frozen in time&amp;hellip;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Give &lt;a href=&#34;https://talkpython.fm/blog/posts/talk-python-rewritten-in-quart-async-flask/&#34;&gt;the full article a read&lt;/a&gt;.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Blue Skies ahead (follow me there)</title>
            <link>https://mkennedy.codes/posts/blue-skies-ahead-follow-me-there/</link>
            <pubDate>Sat, 16 Nov 2024 09:58:15 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/blue-skies-ahead-follow-me-there/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/blue-skies-ahead-follow-me-there/blueskys.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve found some Blue Skies! With a bunch of my favorite tech people moving over to &lt;a href=&#34;https://bsky.app/&#34;&gt;Bluesky&lt;/a&gt;, it&amp;rsquo;s time to set up shop over there. So if you&amp;rsquo;re on (or want to join) &lt;a href=&#34;https://bsky.app/&#34;&gt;bsky.app&lt;/a&gt;, consider giving me and the podcasts a follow:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Michael on bsky.app -&amp;gt; &lt;a href=&#34;https://bsky.app/profile/mkennedy.codes&#34;&gt;@mkennedy.codes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Talk Python on bsky.app -&amp;gt; &lt;a href=&#34;https://bsky.app/profile/talkpython.fm&#34;&gt;@talkpython.fm&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Python Bytes on bsky.app -&amp;gt; &lt;a href=&#34;https://bsky.app/profile/pythonbytes.fm&#34;&gt;@pythonbytes.fm&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And if you&amp;rsquo;re looking for some folks in the Python space to connect with, I&amp;rsquo;ve created a &lt;a href=&#34;https://bsky.app/profile/mkennedy.codes/post/3lbdnx7l6ck2v&#34;&gt;starter pack&lt;/a&gt; which let&amp;rsquo;s you follow a bunch of us all in one click (or pick from the list):&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://bsky.app/profile/mkennedy.codes/post/3lbdnx7l6ck2v&#34;&gt;&lt;img src=&#34;030-starter-pack.webp&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Introducing chameleon-flask package</title>
            <link>https://mkennedy.codes/posts/introducing-the-chameleon-flask-package/</link>
            <pubDate>Sat, 09 Nov 2024 08:17:10 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/introducing-the-chameleon-flask-package/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; I created &lt;a href=&#34;https://github.com/mikeckennedy/chameleon-flask&#34;&gt;chameleon-flask&lt;/a&gt;, a package that adds Chameleon template support to Flask. Use &lt;code&gt;@chameleon_flask.template(&#39;page.pt&#39;)&lt;/code&gt; as a decorator and return a dict to render templates. Works with both sync and async views.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m a big fan of the &lt;a href=&#34;https://chameleon.readthedocs.io/en/latest/&#34;&gt;Chameleon templating language&lt;/a&gt; for Python web apps. For a long time it was the default HTML flavor for Pyramid. But I&amp;rsquo;m kicking offer new project based on Flask. Flask has really &lt;a href=&#34;https://talkpython.fm/episodes/show/472/state-of-flask-and-pallets-in-2024&#34;&gt;seen a resurgence lately&lt;/a&gt; so it seems like the perfect choice for what I&amp;rsquo;m working on.&lt;/p&gt;
&lt;p&gt;One drawback, Flask only supports Jinja2. Jinja is fine, like most templating languages, it is full of Python syntax and begin/end blocks. It&amp;rsquo;s less pure web compared to Chameleon.&lt;/p&gt;
&lt;h2 id=&#34;so-i-fixed-it&#34;&gt;So I fixed it&lt;/h2&gt;
&lt;p&gt;I created a &lt;a href=&#34;https://github.com/mikeckennedy/chameleon-flask&#34;&gt;new open source package&lt;/a&gt; for making Chameleon feel very native in Flask:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nd&#34;&gt;@app.get&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;/async&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;nd&#34;&gt;@chameleon_flask.template&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;async.pt&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;async&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;def&lt;/span&gt; &lt;span class=&#34;nf&#34;&gt;async_world&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;asyncio&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;sleep&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mf&#34;&gt;.01&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;k&#34;&gt;return&lt;/span&gt; &lt;span class=&#34;p&#34;&gt;{&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;message&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;:&lt;/span&gt; &lt;span class=&#34;s2&#34;&gt;&amp;#34;Let&amp;#39;s go async Chameleon!&amp;#34;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Notice the second decorator: &lt;code&gt;@chameleon_flask.template(&#39;async.pt&#39;)&lt;/code&gt;. That&amp;rsquo;s the magic. Return a dictionary and it&amp;rsquo;ll render the Chameleon template at &lt;code&gt;/templates/async.pt&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;If you want to use Chameleon in Flask, check out &lt;a href=&#34;https://github.com/mikeckennedy/chameleon-flask&#34;&gt;chameleon-flask&lt;/a&gt; on GitHub. And if you&amp;rsquo;re using FastAPI, I created a sister project called &lt;a href=&#34;https://github.com/mikeckennedy/fastapi-chameleon&#34;&gt;fastapi-chameleon&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;two-caveats&#34;&gt;Two caveats&lt;/h2&gt;
&lt;p&gt;First, why didn&amp;rsquo;t I just use a few of the existing Flask + Chameleon libraries? They seem like they&amp;rsquo;d work. But they were either very minimal or didn&amp;rsquo;t support both sync and async view methods.&lt;/p&gt;
&lt;p&gt;Secondly, many Flask extensions assume they are working with Jinja as their template system. So this may or may not work with certain extensions. I generally don&amp;rsquo;t use extensions so it&amp;rsquo;s fine for me. Just figure out if this is fine for you.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Talk Python has moved to Hetzner</title>
            <link>https://mkennedy.codes/posts/talk-python-has-moved-to-hetzner/</link>
            <pubDate>Wed, 06 Nov 2024 20:47:43 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/talk-python-has-moved-to-hetzner/</guid>
            <description>&lt;p&gt;From the &lt;a href=&#34;https://talkpython.fm/blog/posts/we-have-moved-to-hetzner/&#34;&gt;Talk Python blog&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;After almost 10 years at &lt;a href=&#34;https://m.do.co/c/88e9531cb6aa&#34;&gt;Digital Ocean&lt;/a&gt;, we’ve moved our entire infrastructure to &lt;a href=&#34;https://www.hetzner.com/cloud/&#34;&gt;Hetzner&lt;/a&gt;’s US-based data center in Virginia.&lt;/p&gt;
&lt;p&gt;This is a big change. We have over 20 different web apps, APIs, background daemons, and databases all working loosely together. Plus I have 10 years of experience running infrastructure in Digital Ocean’s cloud.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Give &lt;a href=&#34;https://talkpython.fm/blog/posts/we-have-moved-to-hetzner/&#34;&gt;the full article a read&lt;/a&gt;. There are some graphs and other comparisons plus why we made the move.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Opposite of Cloud Native is?</title>
            <link>https://mkennedy.codes/posts/opposite-of-cloud-native-is-stack-native/</link>
            <pubDate>Tue, 05 Nov 2024 13:47:36 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/opposite-of-cloud-native-is-stack-native/</guid>
            <description>&lt;p&gt;&lt;a href=&#34;https://cdn.mkennedy.codes/posts/opposite-of-cloud-native-is-stack-native/36-cloud-native-diagram-final.webp&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/opposite-of-cloud-native-is-stack-native/36-cloud-native-diagram-final.webp&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; The opposite of cloud-native isn&amp;rsquo;t &amp;ldquo;lift-and-shift&amp;rdquo; or building your own data center. It&amp;rsquo;s &lt;strong&gt;stack-native&lt;/strong&gt;: building your app with just enough full-stack building blocks to run reliably with minimal complexity. We run 28 apps serving 9M requests/month on one $65/mo Hetzner server. Same setup in AWS? $1,226/mo.&lt;/p&gt;
&lt;p&gt;There seems to be a lot of contention lately about whether you should go all-in on the cloud or to keep things as simple as possible. Those on the side of &amp;ldquo;build for the cloud&amp;rdquo; often frame this as &lt;strong&gt;cloud-native&lt;/strong&gt;. By that, I believe they mean you should look at every single service offered by your cloud provider when architecting your application and generally the more services you pull together the better. See &lt;a href=&#34;https://learn.microsoft.com/en-us/dotnet/architecture/cloud-native/definition&#34;&gt;Microsoft&amp;rsquo;s&lt;/a&gt; and &lt;a href=&#34;https://cloud.google.com/learn/what-is-cloud-native&#34;&gt;Google&amp;rsquo;s definitions&lt;/a&gt; for cloud native.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;What is the opposite of cloud-native?&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In this essay, I&amp;rsquo;ll define a new term for the modern opposite of cloud-native. I think you&amp;rsquo;ll find it appealing.&lt;/p&gt;
&lt;p&gt;Those on the cloud side look back at the dark days of running physical servers with monolithic applications directly on hardware at your location with valid skepticism. These apps were often little more than just a single application and a huge local database. Maybe they were even client-server desktop apps (gasp!).&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re not up on all the cloud services and managed this or that, you&amp;rsquo;re not cloud-native. You&amp;rsquo;d probably do what they call &amp;ldquo;lifting-and-shifting&amp;rdquo; your app to cloud. Did you have one huge server in the office? Well, now you get one huge server in AWS EC2 and copy your app to it. You&amp;rsquo;ll also pay extreme prices for that privilege. You&amp;rsquo;re really not with the times, are you?&lt;/p&gt;
&lt;p&gt;This particular view of a mostly bygone time is not the opposite of cloud-native. The fallacy here is comparing two modes of running applications disjoined across time. Cloud-native is one view of how to build apps in 2024. This lift-and-shift style is a view of how to run your own apps in 2001. It&amp;rsquo;s not constructive to debate them as peers.&lt;/p&gt;
&lt;p&gt;I recently watched a livestream about moving out of AWS. So many people participated in the conversation were contrasting two worlds: Cloud vs. On-prem. Either you run in AWS/Azure or you build a data center, you string ethernet cables, you buy a backup generator, you hire a couple of engineers to keep these running when hard drives fail.&lt;/p&gt;
&lt;p&gt;What? No. This is not the alternative.&lt;/p&gt;
&lt;p&gt;The livestream conversation was about a company moving into another managed data center. That data center came with generators. It already has ethernet. The tradeoff is not cloud or build your own data center in 2024. Please do not let people drag you into these false dichotomies.&lt;/p&gt;
&lt;p&gt;You should be wary of these &lt;a href=&#34;https://world.hey.com/dhh/merchants-of-complexity-4851301b&#34;&gt;merchants of complexity&lt;/a&gt;. These hyperscale cloud providers want you to buy in fully to all their services. The amount of lock-in this provides is tremendous. Once &lt;a href=&#34;https://www.youtube.com/watch?v=SCIfWhAheVw&#34;&gt;your bill leaps far beyond what you expected&lt;/a&gt;, it&amp;rsquo;s tough to change course.&lt;/p&gt;
&lt;h2 id=&#34;the-opposite-of-cloud-native-is-stack-native&#34;&gt;The opposite of cloud-native is stack-native&lt;/h2&gt;
&lt;p&gt;Consider that insane diagram at the top of this essay. You have a little tiny bit in the center labeled Flask. It is surrounded by every cloud-native service you might want. One portion of our app might need to scale a bit so lambda/serverless functions. We broke our single Flask-based API into 100 serverless endpoints, so we now need something to manage their deployment. What about logs and monitoring? Across all these services? We need a few log services for that. And we may need to scale the Flask portion so Kubernetes! And on it goes.&lt;/p&gt;
&lt;p&gt;I present to you an alternative philosophy: stack-native.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;&lt;strong&gt;&lt;u&gt;Stack-native&lt;/u&gt;&lt;/strong&gt; is building your app with just enough full-stack building blocks to make it run reliably with minimal complexity.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This is what it looks like when displayed at the same scale as our cloud-native diagram:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.mkennedy.codes/posts/opposite-of-cloud-native-is-stack-native/36-stack-native-to-scale-final.webp&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/opposite-of-cloud-native-is-stack-native/36-stack-native-to-scale-final.webp&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a breath of fresh air, isn&amp;rsquo;t it? But let&amp;rsquo;s zoom in so you can actually see it!&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.mkennedy.codes/posts/opposite-of-cloud-native-is-stack-native/36-stack-native-final.webp&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/opposite-of-cloud-native-is-stack-native/36-stack-native-final.webp&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;What do we have in this stack-native diagram?&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;We have Flask at the center still.&lt;/li&gt;
&lt;li&gt;Flask might be managed by Docker and potentially scaled via web workers. It could be run directly on the VM if that makes sense.&lt;/li&gt;
&lt;li&gt;Those workers are running in a WSGI Python server, in this case Granian&lt;/li&gt;
&lt;li&gt;The web app is exposed to the internet via nginx&lt;/li&gt;
&lt;li&gt;The self-hosted database can live here too (Postgres or MongoDB)&lt;/li&gt;
&lt;li&gt;We have a server mapped volume for all our database data, container images, and more.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That&amp;rsquo;s it. And everything in that picture is free and open source other than the cloud VM. It&amp;rsquo;s running on a single medium-sized server in a modern and affordable cloud host (e.g. &lt;a href=&#34;https://m.do.co/c/88e9531cb6aa&#34;&gt;Digital Ocean&lt;/a&gt; or &lt;a href=&#34;https://hetzner.com/cloud/&#34;&gt;Hetzner&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Is this lifted-and-shifted? I don&amp;rsquo;t think so. It&amp;rsquo;s closer to Kubernetes than to old-timey client-server on-prem actually.&lt;/p&gt;
&lt;p&gt;Would you have to buy a generator? Run ethernet? Fix hardware? Of course not, it runs in a state-of-the-art data center with global reach. It&amp;rsquo;s just not cloud-native. It&amp;rsquo;s stack-native and that is a &lt;em&gt;good thing&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id=&#34;is-stack-native-for-toy-apps&#34;&gt;Is stack-native for toy apps?&lt;/h2&gt;
&lt;p&gt;No. We have a very similar setup powering &lt;a href=&#34;https://talkpython.fm/episodes/all&#34;&gt;Talk Python&lt;/a&gt;, &lt;a href=&#34;https://training.talkpython.fm/courses/all&#34;&gt;the courses&lt;/a&gt;, the &lt;a href=&#34;https://training.talkpython.fm/apps&#34;&gt;mobile APIs&lt;/a&gt;, and &lt;a href=&#34;https://uptimekuma.talkpython.fm/status/python-bytes&#34;&gt;much&lt;/a&gt; &lt;a href=&#34;https://uptimekuma.talkpython.fm/status/talk-python&#34;&gt;much more&lt;/a&gt;. Across all our apps and services we receive about 9M Python / database backed requests per month (no caching because it&amp;rsquo;s not needed). And we handle about 10TB of traffic with 1TB straight out of Python.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the crazy part. All of our infrastructure is running on one medium-sized server in a US-based data center from Hetzner. We have a single 8 CPU / 16 GB RAM server that we partition up across 28 apps and databases using docker. Most of these apps are as simple or simpler than the stack-native diagram above. For this entire setup, including bandwidth, we pay $65/month. That&amp;rsquo;s $25/mo for the server and another $40 for bandwidth.&lt;/p&gt;
&lt;p&gt;I just finished doing some tentative load testing using &lt;a href=&#34;https://locust.io&#34;&gt;the amazing Locust.io framework&lt;/a&gt;. At its peak, this setup running Nginx + Granian + Python + Pyramid + MongoDB would handle over 100M Python requests / month. For $25.&lt;/p&gt;
&lt;p&gt;In contrast, what would this setup cost in AWS? Well, the server is about $205 / month. The bandwidth out of that server is another $100/mo. If we put all our bandwidth through AWS (for example mp3s and videos through S3) the price jumps up by another whopping $921. This brings the total to $1,226/mo.&lt;/p&gt;
&lt;p&gt;The contrast is stark. If we chose cloud-native, we&amp;rsquo;d be tied into cloud-front, EKS, S3, EC2, etc. That&amp;rsquo;s the way you use the cloud, you noobie. Let&amp;rsquo; the company cover the monthly costs.&lt;/p&gt;
&lt;p&gt;But stack-native can move. We can run it in Digital Ocean for a few years as we did. When a company like Hetzner opens a data center in the US with 1/6th pricing, we can take our setup and move. The hardest part of this is Let&amp;rsquo;s Encrypt and DNS. There is nearly zero lock-in.&lt;/p&gt;
&lt;h2 id=&#34;the-final-take-away&#34;&gt;The final take away&lt;/h2&gt;
&lt;p&gt;It may sound like I&amp;rsquo;m telling you to never use Kubernetes. Never use PaaS and so on. That&amp;rsquo;s not quite it though. You can definitely choose PaaS. Rather than using the cloud provider&amp;rsquo;s PaaS, maybe host your own version of &lt;a href=&#34;https://coolify.io&#34;&gt;Coolify&lt;/a&gt;? You want to run serverless? Maybe &lt;a href=&#34;https://knative.dev/docs/&#34;&gt;Knative&lt;/a&gt; works. Whatever you pick, just be very cognizant of the lock-in and ultimate price in terms of flexibility in the future. Try to fit it into a single box, even if that box is a pretty big VM in the cloud.&lt;/p&gt;
&lt;h2 id=&#34;get-the-whole-python-in-production-series&#34;&gt;Get the whole Python in production series&lt;/h2&gt;
&lt;p&gt;I plan on writing a whole series on this topic focused on this topic. Please consider subscribing to the &lt;a href=&#34;https://mkennedy.codes/index.xml&#34;&gt;RSS feed&lt;/a&gt; here or &lt;a href=&#34;https://talkpython.fm/friends-of-the-show&#34;&gt;joining the mailing list&lt;/a&gt; at Talk Python.&lt;/p&gt;
&lt;h2 id=&#34;what-do-you-think&#34;&gt;What do you think?&lt;/h2&gt;
&lt;p&gt;If this article made you think or you just want to share your thoughts, here are a few places with comments you could jump (no comments directly on my blog):&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;X.com - &lt;a href=&#34;https://x.com/mkennedy/status/1853918500349501671&#34;&gt;x.com/mkennedy/status/1853918500349501671&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Mastodon - &lt;a href=&#34;https://fosstodon.org/@mkennedy/113432568775276850&#34;&gt;fosstodon.org/@mkennedy/113432568775276850&lt;/a&gt;&lt;/li&gt;
&lt;/ol&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Search Talk Python from your address bar</title>
            <link>https://mkennedy.codes/posts/search-talk-python-from-your-browsers-address-bar/</link>
            <pubDate>Wed, 16 Oct 2024 08:57:07 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/search-talk-python-from-your-browsers-address-bar/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Add Talk Python and Python Bytes as custom search engines in your browser. Use shortcuts like &amp;ldquo;tp&amp;rdquo; to search 9 years of episodes and transcripts directly from your address bar. Setup instructions at search.talkpython.fm/api/browser.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve put a ton of effort into &lt;a href=&#34;https://talkpython.fm/?ref=mkennedycodes&#34;&gt;Talk Python&lt;/a&gt; and &lt;a href=&#34;https://pythonbytes.fm/?ref=mkennedycodes&#34;&gt;Python Bytes&lt;/a&gt; to make our content discoverable and open. Our RSS feeds have all 9 years of episodes to download and listen to. However, given that much content, it&amp;rsquo;s often hard to know what&amp;rsquo;s out there.&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s why our search includes not just the show notes and links on the pages, but full text search of all the high-quality transcripts as well. &lt;strong&gt;You can search every spoken word from the podcasts&lt;/strong&gt;. This &lt;a href=&#34;https://talkpython.fm/search&#34;&gt;feature has been out&lt;/a&gt; for quite awhile.&lt;/p&gt;
&lt;p&gt;I just added an API for &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/OpenSearch&#34;&gt;Open Search Description&lt;/a&gt; to both Talk Python and Python Bytes. That means you can now &lt;strong&gt;add the podcasts as search providers in your browser&lt;/strong&gt;. As you can see, just assign a shortcut (like &lt;code&gt;tp&lt;/code&gt;) and you get both direct search and search suggestions:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/search-talk-python-from-your-browsers-address-bar/35-talk-python-suggest.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;add-it-to-your-browser&#34;&gt;Add it to your browser&lt;/h2&gt;
&lt;p&gt;Every browser is a bit different in the steps and capabilities. To add yours, just visit the browser setup page to enter the details (it takes less than a minute to do) and have the podcasts&amp;rsquo; immense back catalog of information always at your fingertips.&lt;/p&gt;
&lt;p&gt;Browser setup instructions &lt;a href=&#34;https://search.talkpython.fm/api/browser&#34;&gt;for Talk Python&lt;/a&gt; and &lt;a href=&#34;https://search.pythonbytes.fm/api/browser&#34;&gt;for Python Bytes&lt;/a&gt;.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>We Must Replace uWSGI With Something Else</title>
            <link>https://mkennedy.codes/posts/we-must-replace-uwsgi-with-something-else-but-with-what/</link>
            <pubDate>Thu, 03 Oct 2024 12:12:37 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/we-must-replace-uwsgi-with-something-else-but-with-what/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/we-must-replace-uwsgi-with-something-else-but-with-what/34-rusted-lock.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; uWSGI is now in maintenance-only mode. Replace it with Granian (recommended), uvicorn, hypercorn, or gunicorn with uvicorn workers. All support ASGI for async Python web apps.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;ve been using uWSGI as your Python web server, you&amp;rsquo;ll be interested in this prominent message at the &lt;a href=&#34;https://uwsgi-docs.readthedocs.io/en/latest/&#34;&gt;top of the uWSGI project&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Note: The project is in maintenance mode (only bug fixes and updates for new languages apis). Do not expect quick answers on GitHub issues and/or pull requests (sorry for that) A big thanks to all of the users and contributors since 2009.&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I stumbled across this when poking around GitHub in &lt;a href=&#34;https://github.com/emmett-framework/granian&#34;&gt;the Granian project&lt;/a&gt;. In there was a reference from &lt;strong&gt;OpenEdX with &lt;a href=&#34;https://github.com/overhangio/tutor/issues/937?featured_on=pythonbytes&#34;&gt;an issue&lt;/a&gt; entitled &amp;ldquo;We Must Replace Uwsgi With Something Else&amp;rdquo;&lt;/strong&gt;. Having been an avid user of uWSGI, this caught my attention. Hence the note just above.&lt;/p&gt;
&lt;h2 id=&#34;replace-it-but-with-what&#34;&gt;Replace it, but with what?&lt;/h2&gt;
&lt;p&gt;I think there are some excellent modern options to replace uWSGI. The good news is that it&amp;rsquo;s super fast and easy to do. Basically, change the start up command and the server you install (via &lt;code&gt;uv pip install ...&lt;/code&gt;) and you&amp;rsquo;re off to the races. After all, isn&amp;rsquo;t that the promise of WSGI and ASGI?&lt;/p&gt;
&lt;p&gt;In this context, by &amp;ldquo;modern&amp;rdquo;, I mean two things: 1) It is actively supported and 2) it supports ASGI for async-based Python web apps (e.g. FastAPI).&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;To me, reasonable options include:
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/emmett-framework/granian&#34;&gt;&lt;strong&gt;Granian&lt;/strong&gt;&lt;/a&gt; (what we&amp;rsquo;re using across the board at &lt;a href=&#34;https://talkpython.fm&#34;&gt;Talk Python&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.uvicorn.org/&#34;&gt;&lt;strong&gt;uvicorn&lt;/strong&gt;&lt;/a&gt; (now an option as &lt;a href=&#34;https://www.uvicorn.org/deployment/&#34;&gt;a stand-alone production server&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://hypercorn.readthedocs.io/en/latest/index.html&#34;&gt;&lt;strong&gt;hypercorn&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://gunicorn.org/&#34;&gt;&lt;strong&gt;gunicorn&lt;/strong&gt;&lt;/a&gt; (potentially with uvicorn workers for async, pronounced &amp;ldquo;Gee Unicorn&amp;rdquo;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;and-thanks&#34;&gt;And thanks&lt;/h2&gt;
&lt;p&gt;Thanks to the whole uWSGI team and contributors for 15 years of a great web server. It was a good run.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Let&#39;s go easy on PyPI, OK?</title>
            <link>https://mkennedy.codes/posts/lets-go-easy-on-pypi-ok/</link>
            <pubDate>Sat, 28 Sep 2024 10:04:38 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/lets-go-easy-on-pypi-ok/</guid>
            <description>&lt;p&gt;[&lt;strong&gt;Post updated&lt;/strong&gt; Sept 30, 2024 based on lots of community conversations.]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Stop reinstalling all Python packages on every Docker build. Copy and install requirements.txt before your source code, and pre-install core dependencies in an unchanging layer. Docker&amp;rsquo;s layer caching will reduce your PyPI calls by 100x.&lt;/p&gt;
&lt;p&gt;What do you think of when you visualize the &lt;a href=&#34;https://pypi.org&#34;&gt;Python Package Index&lt;/a&gt; and someone using it? A developer or data scientist building something new and creative? That&amp;rsquo;s cute (and yes of course it&amp;rsquo;s amazing and it does happen).&lt;/p&gt;
&lt;h2 id=&#34;closer-to-reality&#34;&gt;Closer to reality&lt;/h2&gt;
&lt;p&gt;For every individual periodically installing packages with &lt;code&gt;pip&lt;/code&gt; and friends, there are literally 1,000s or hundreds of thousands of automated systems building Docker images (or containers for Kubernetes or other infrastructure).&lt;/p&gt;
&lt;p&gt;Maybe it should look more like this in your mind, an absolute hive of traffic and busy-ness and few humans in the loop at all:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/lets-go-easy-on-pypi-ok/33-traffic-heavy.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;This is because containers are supposed to be isolated. So of course they &lt;strong&gt;have&lt;/strong&gt; to download flask-sqlalchemy 100 times day, right?&lt;/p&gt;
&lt;h2 id=&#34;or-do-they&#34;&gt;Or do they?&lt;/h2&gt;
&lt;p&gt;Containers also support caching and layering. That is, each line of the file that creates a container is a separate instruction that adds on the ones that came before. And if nothing the came before has changed, then why run it again?&lt;/p&gt;
&lt;p&gt;We can use this layering to make things much easier and cheaper for Python and PyPI.&lt;/p&gt;
&lt;p&gt;Did you know that PyPI had a total of 284+ billion downloads for the half million projects in 2023? That&amp;rsquo;s &lt;code&gt;$50k&lt;/code&gt; - &lt;code&gt;$100k&lt;/code&gt; of traffic per month. Yikes.&lt;/p&gt;
&lt;p&gt;What if we could get faster Docker builds and &lt;strong&gt;dramatically limit the demand on PyPI&lt;/strong&gt;? I&amp;rsquo;ll show you a technique that we use at &lt;a href=&#34;https://talkpython.fm&#34;&gt;Talk Python&lt;/a&gt; (I&amp;rsquo;m sure there are variations).&lt;/p&gt;
&lt;h2 id=&#34;towards-a-gentler-dockerfile&#34;&gt;Towards a gentler Dockerfile&lt;/h2&gt;
&lt;p&gt;Consider this Docker image file (don&amp;rsquo;t worry if you don&amp;rsquo;t know Docker, I&amp;rsquo;ll explain and what you need to know is simple). &lt;strong&gt;This is the most abusive one&lt;/strong&gt;, yet it&amp;rsquo;s the straightforward and common one as well:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;ubuntu:latest&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Set up the path for our tooling&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ENV&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;PATH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/venv/bin:&lt;span class=&#34;nv&#34;&gt;$PATH&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ENV&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;PATH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/root/.cargo/bin:&lt;span class=&#34;nv&#34;&gt;$PATH&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Install uv (because it&amp;#39;s caching is awesome)&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; curl -LsSf https://astral.sh/uv/install.sh &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; sh&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Create a venv with Pyton 3.12.6, installing Python along the way&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; uv venv --python 3.12.6 /venv&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Copy the full source code to be run&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;COPY&lt;/span&gt; src/yourapp /app&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Install the requirements via uv &amp;amp; requirements.txt&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; uv pip install -r requirements.txt&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Run your app in a web server via the entry point&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ENTRYPOINT&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;  &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;s2&#34;&gt;&amp;#34;/venv/bin/...&amp;#34;&lt;/span&gt; &lt;span class=&#34;se&#34;&gt;\
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;    &lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;First, for you all uninitiated in Docker, the steps are:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Base a container on Ubuntu&lt;/li&gt;
&lt;li&gt;Set up some path to keep the tooling simple later&lt;/li&gt;
&lt;li&gt;Use uv over basic pip and install Python and create a venv with it&lt;/li&gt;
&lt;li&gt;Copy your source code from a local src folder into /app&lt;/li&gt;
&lt;li&gt;Install the requirements (via uv) into your venv&lt;/li&gt;
&lt;li&gt;Run the app via some web server or something on the entry point&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;em&gt;Note&lt;/em&gt;: If you&amp;rsquo;re wondering what&amp;rsquo;s up with all this uv stuff, &lt;a href=&#34;https://mkennedy.codes/posts/python-docker-images-using-uv-s-new-python-features/&#34;&gt;I wrote about it&lt;/a&gt; a week or two ago.&lt;/p&gt;
&lt;h2 id=&#34;why-is-this-abusive&#34;&gt;Why is this abusive?&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;If any source file whatsoever changes&lt;/strong&gt;, that will trigger the lines below&lt;/p&gt;
&lt;p&gt;&lt;code&gt;# Copy the full source code to be run&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;to run and hence invalidating all of Docker&amp;rsquo;s caching and &lt;strong&gt;will reinstall all of the requirements&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Over &lt;strong&gt;at &lt;a href=&#34;https://training.talkpython.fm/courses/all&#34;&gt;Talk Python Training&lt;/a&gt;, our web app has 205 packages&lt;/strong&gt; in use (not top-level, but with the transitive closure of them). &lt;strong&gt;205&lt;/strong&gt;! That is massive load on pypi.org. Massive. And yet, most of the time, no packages changed. This is especially true if you pin your dependencies as we do with &lt;code&gt;uv pip compile&lt;/code&gt;. But we would still reinstall all of them, pulling fresh copies from PyPI through Fastly every time with this docker setup.&lt;/p&gt;
&lt;p&gt;That is down right abusive and wasteful.&lt;/p&gt;
&lt;h2 id=&#34;a-simple-change-for-the-better&#34;&gt;A simple change for the better&lt;/h2&gt;
&lt;p&gt;One very minor change (but not final) that we can do is to just copy the requirements.txt file and install them first, then overwrite that with the full app.&lt;/p&gt;
&lt;p&gt;See the &lt;strong&gt;NEW&lt;/strong&gt; section:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# NEW: Copy just the requirements.txt and install them&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;COPY&lt;/span&gt; src/yourapp/requirements.txt /app&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; uv pip install -r requirements.txt&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Copy the full app to be run&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;COPY&lt;/span&gt; src/yourapp /app&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Seems silly, right? If we are going to copy all the files anyway, why copy just the requirements.txt one first?&lt;/p&gt;
&lt;p&gt;Because of Docker&amp;rsquo;s cached layers.&lt;/p&gt;
&lt;p&gt;This change means &lt;strong&gt;you only install the requirements if the requirements.txt file itself has changed&lt;/strong&gt; (or lines before it such as a new ubuntu image).&lt;/p&gt;
&lt;p&gt;Way better! Now, most source changes don&amp;rsquo;t pull a new copy of requirements from PyPI and the container builds faster: win-win.&lt;/p&gt;
&lt;p&gt;But&amp;hellip;&lt;/p&gt;
&lt;p&gt;If any requirement changes, we pull all of them by rerunning &lt;code&gt;uv pip install -r requirements.txt&lt;/code&gt; Ugh. It&amp;rsquo;s better, but not great.&lt;/p&gt;
&lt;h2 id=&#34;one-more-layer&#34;&gt;One more layer&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s add one more change. Suppose your app is built on Flask and uses Beanie for an ODM against MongoDB. So your core requirements are &lt;strong&gt;flask&lt;/strong&gt;, &lt;strong&gt;beanie&lt;/strong&gt;, and that&amp;rsquo;s it. And let&amp;rsquo;s imagine your web server is &lt;strong&gt;gunicorn&lt;/strong&gt; to boot.&lt;/p&gt;
&lt;p&gt;We can add this new-new section:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# NEW-NEW: Install the latest of all main dependencies.&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Do NOT pin these versions, we want the latest here&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; uv pip install flask beanie gunicorn&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Copy just the requirements.txt and install them&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;COPY&lt;/span&gt; src/yourapp/requirements.txt /app&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; uv pip install -r requirements.txt&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Copy the full app to be run&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;COPY&lt;/span&gt; src/yourapp /app&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;That new line makes all the difference:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;uv pip install flask beanie gunicorn
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This layer will never change, never. Yet, it&amp;rsquo;ll pull every dependency if you put your top-level ones in that line. But you may be thinking:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Great, however it&amp;rsquo;ll get out of sync over time&amp;hellip;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yes, but think 205 dependencies, not 3: At first build time, your container (well uv) will see that you already have all the dependencies when you run&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;uv pip install -r requirements.txt
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It won&amp;rsquo;t hit PyPI again on subsequent builds and it&amp;rsquo;ll run lightening fast because of uv caching and Docker&amp;rsquo;s caching on top of that.&lt;/p&gt;
&lt;p&gt;Over time, they will drift. Maybe a new version of pydantic ships and it&amp;rsquo;ll get changed when the requirements.txt line is run. The older, cached one will get uninstalled when you install the pinned requirements.txt (or pyproject.toml, or whatever).&lt;/p&gt;
&lt;p&gt;Fine. But this only pulls the new pydantic from PyPI, not all 205 of the dependencies!&lt;/p&gt;
&lt;p&gt;My experience is &lt;strong&gt;most packages stay static for months if not years&lt;/strong&gt; and only a few change frequently. So why are we pulling all 205 from PyPI every time something minor changes. We shouldn&amp;rsquo;t be, right?&lt;/p&gt;
&lt;p&gt;Eventually there will be something to trigger a full rebuild (such as a new ubuntu image ships). Then everything will updated, get resynced and realigned.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;And the cycle continues&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&#34;and-potentially-some-help-from-docker&#34;&gt;And potentially some help from Docker&lt;/h2&gt;
&lt;p&gt;[&lt;em&gt;This section added after lots of great feedback from readers.&lt;/em&gt;]&lt;/p&gt;
&lt;p&gt;So far, we have cached our packages in the uv cache inside the container. This layering from Docker is great and will reuse that cache most of the time as mentioned above. However, for full rebuilts, we will pull full copies of everything again. This is the case if we ask for a full rebuild but also if we have the base image or lines prior to ours change.&lt;/p&gt;
&lt;p&gt;Docker will optionally use a host-OS folder for a subsection of the docker image file system during build with the &lt;code&gt;mount&lt;/code&gt; option. We can leverage this to permanently locate and use the uv cache on the host OS so even full rebuilds mostly use the local cache. Notice the addtion of &lt;code&gt;--mount=type=cache,target=/root/.cache&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Copy just the requirements.txt and install them&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;COPY&lt;/span&gt; src/yourapp/requirements.txt /app&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; --mount&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;nv&#34;&gt;type&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;cache,target&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/root/.cache uv pip install -r requirements.txt&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Copy the full app to be run&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;COPY&lt;/span&gt; src/yourapp /app&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# ...&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This will create a local cached folder managed by Docker that maps &lt;code&gt;/root/.cache&lt;/code&gt; inside the container for build time that redirects uv&amp;rsquo;s cache (&lt;code&gt;/root/.cache/uv&lt;/code&gt;) as well as pip&amp;rsquo;s cache (&lt;code&gt;/root/.cache/pip&lt;/code&gt;) to a local persistant host folder.&lt;/p&gt;
&lt;p&gt;In this setup, you can skip the ahead-of-time &lt;code&gt;RUN uv pip install flask beanie gunicorn&lt;/code&gt; section because after the first run it&amp;rsquo;ll be cached to the OS. That&amp;rsquo;s a big plus.&lt;/p&gt;
&lt;p&gt;However, now the host OS will be the source of caching for pip/uv with this setup. The packages will not be cached in the docker image. So if you ship the container to other locations that build upon it, then they&amp;rsquo;ll be starting from scratch. This would be the case if someone is &lt;code&gt;docker pull&lt;/code&gt;-ing your image as their base image. So consider the use-case for &lt;code&gt;--mount&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;and-for-ci-on-github&#34;&gt;And for CI on GitHub&lt;/h2&gt;
&lt;p&gt;One the largest consumers of pypi.org traffic has to be CI. And among them, GitHub is probably the biggest. This is pretty resistant to any of the caching help we&amp;rsquo;ve discussed so far. Luckily GitHub actions have a cache option just for this. Here&amp;rsquo;s &lt;a href=&#34;https://github.com/actions/cache/blob/main/examples.md#python---pip&#34;&gt;the pip example from GitHub&lt;/a&gt;. Use the directory &lt;code&gt;/root/.cache/uv&lt;/code&gt; for uv if you&amp;rsquo;re using uv instead.&lt;/p&gt;
&lt;h2 id=&#34;the-take-away&#34;&gt;The take-away&lt;/h2&gt;
&lt;p&gt;Adding just small and simple sections to your Dockerfile could reduce your load on PyPI by over 100x (for real, no hyperbole here).&lt;/p&gt;
&lt;p&gt;First, we pre-cache and install all of the core dependencies of our app like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Install all of the main dependencies&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Do NOT pin these versions, we want the latest here&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; uv pip install flask beanie gunicorn&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Second, we only update these if the requirements.txt file (or pyproject.toml, or whatever) changes:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# Copy just the requirements.txt and install them&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;COPY&lt;/span&gt; src/yourapp/requirements.txt /app&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; uv pip install -r requirements.txt&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;It&amp;rsquo;ll make your containers build way way faster once it&amp;rsquo;s cached and it&amp;rsquo;ll take a massive load off of PyPI and do the PSF and the whole Python community a favor making PyPI much cheaper to operate for all of us.&lt;/p&gt;
&lt;p&gt;You can find a working example in a previous post, &lt;a href=&#34;https://mkennedy.codes/posts/python-docker-images-using-uv-s-new-python-features/&#34;&gt;Docker images using uv&amp;rsquo;s python&lt;/a&gt;. That one covered some of these ideas but focused on making Docker builds fast for Python apps, rather than the focus of this essay which is all about being kind to the PSF and PyPI.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Passkeys are great, careful of the lock-in</title>
            <link>https://mkennedy.codes/posts/passkey-great-but-careful-of-the-lock-in/</link>
            <pubDate>Tue, 24 Sep 2024 19:43:42 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/passkey-great-but-careful-of-the-lock-in/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Passkeys are great for security, but Apple/Google/Microsoft want to lock you in by syncing them only within their ecosystems. Use a third-party password manager like 1Password or Bitwarden to store passkeys instead and keep your freedom.&lt;/p&gt;
&lt;p&gt;People suck at passwords, just look at &lt;a href=&#34;https://haveibeenpwned.com&#34;&gt;haveibeenpwned&lt;/a&gt;. Yet, they have been with us for a very long time and still today serve a very important purpose.&lt;/p&gt;
&lt;p&gt;Just look at the truly misguided attempts to replace them with &lt;a href=&#34;https://www.keepersecurity.com/blog/2024/03/07/magic-links-what-they-are-and-how-they-work/&#34;&gt;&amp;ldquo;magic link&amp;rdquo; logins&lt;/a&gt;. You know the ones. Enter your email address, then they email you a code or link every time you want to log in. Please, &lt;strong&gt;never do this&lt;/strong&gt;, it&amp;rsquo;s incredibly user hostile even if it makes your app &amp;ldquo;safer&amp;rdquo; (&lt;a href=&#34;https://www.keepersecurity.com/blog/2024/03/07/magic-links-what-they-are-and-how-they-work/&#34;&gt;?&lt;/a&gt;). It&amp;rsquo;ll make you yearn for passwords again.&lt;/p&gt;
&lt;h2 id=&#34;passkeys-are-the-answer&#34;&gt;Passkeys are the answer?&lt;/h2&gt;
&lt;p&gt;Along came &lt;a href=&#34;https://fidoalliance.org/passkeys/&#34;&gt;passkeys&lt;/a&gt;. A cool idea that &lt;strong&gt;quickly turned evil&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/passkey-great-but-careful-of-the-lock-in/32-passkey-scray.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;How do cryptographic blobs that can&amp;rsquo;t be faked or brute forced become evil? When tech giants leverage a good idea to further lock you into their platforms. Just do a quick FaceID and you&amp;rsquo;re in &amp;ndash; as long as you&amp;rsquo;re on your Apple device. Or that handy Windows Hello that keeps your passkeys synced &amp;ndash; across your Windows machines.&lt;/p&gt;
&lt;p&gt;So I&amp;rsquo;ve been against them pretty much from the start when I saw them going this way.&lt;/p&gt;
&lt;h2 id=&#34;password-managers-third-party-to-the-rescue&#34;&gt;Password managers (third-party!) to the rescue&lt;/h2&gt;
&lt;p&gt;However, the tides are turning back to open and to choice, and that&amp;rsquo;s awesome. Many of the solid password managers are now supporting passkeys [&lt;a href=&#34;https://1password.com/product/passkeys&#34;&gt;1&lt;/a&gt;, &lt;a href=&#34;https://bitwarden.com/passwordless-passkeys/?featured_on=pythonbytes&#34;&gt;2&lt;/a&gt;]. This means you get your no hassle, cryptographic login (often without 2FA or device login SMSes), but you skip the lock-in.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m a 1Password user and I found that 36 of my logins supported Passkeys (&lt;a href=&#34;https://passkeys.directory&#34;&gt;check here&lt;/a&gt;). Just visit WatchTower and click on Passkeys. After a few hours, I had them all created and registered so no more accounts need attention:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/passkey-great-but-careful-of-the-lock-in/32-1password-passkeys.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;give-it-a-try&#34;&gt;Give it a try&lt;/h2&gt;
&lt;p&gt;So give it a try. And if you&amp;rsquo;re not using a password manager, stop right now and go get one (I recommend 1Password and BitWarden). Then add some passkeys to your &lt;a href=&#34;&#34;&gt;accounts that support them&lt;/a&gt; and save them in your password manager.&lt;/p&gt;
&lt;h2 id=&#34;follow-up-polls&#34;&gt;Follow up polls&lt;/h2&gt;
&lt;p&gt;After some back and forth with folks online, I realized it would be worthwhile to run a couple of polls to see what others think and what they are doing for their logins.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the result of asking a bunch of Python enthusiasts on Twitter and Mastodon:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://x.com/mkennedy/status/1839133106860863980&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/passkey-great-but-careful-of-the-lock-in/32-twitter-poll.png&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://fosstodon.org/@mkennedy/113201550483677441&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/passkey-great-but-careful-of-the-lock-in/32-mastodon-poll.png&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Cheers, Michael&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Docker images using uv&#39;s python</title>
            <link>https://mkennedy.codes/posts/python-docker-images-using-uv-s-new-python-features/</link>
            <pubDate>Thu, 05 Sep 2024 15:12:04 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/python-docker-images-using-uv-s-new-python-features/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-docker-images-using-uv-s-new-python-features/31-docker-banner.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Stop building Python from source in Docker (10+ minutes). Use &lt;code&gt;uv venv --python 3.14 /venv&lt;/code&gt; to install Python in seconds. Pre-install common packages in a base image and use &lt;code&gt;uv pip install&lt;/code&gt; for near-instant builds.&lt;/p&gt;
&lt;h2 id=&#34;uv-has-changed-the-landscape&#34;&gt;uv has changed the landscape&lt;/h2&gt;
&lt;p&gt;A lot has been written [&lt;a href=&#34;https://lucumr.pocoo.org/2024/8/21/harvest-season/&#34;&gt;1&lt;/a&gt;, &lt;a href=&#34;https://hynek.me/articles/docker-uv/?featured_on=pythonbytes&#34;&gt;2&lt;/a&gt;, &lt;a href=&#34;https://simonwillison.net/2024/Aug/20/uv-unified-python-packaging&#34;&gt;3&lt;/a&gt;] about &lt;a href=&#34;https://astral.sh/blog/uv-unified-python-packaging?featured_on=pythonbytes&#34;&gt;uv&amp;rsquo;s new features&lt;/a&gt; allowing it to not just work within Python but &lt;strong&gt;to manage Python itself&lt;/strong&gt;. Many people have written me over email or social media to tell me that this will have a big impact. As a result, I just had Charlie Marsh (creator and lead over at Astral) &lt;a href=&#34;https://talkpython.fm/episodes/show/476/unified-python-packaging-with-uv&#34;&gt;on Talk Python&lt;/a&gt; to dive into the changes and implications.&lt;/p&gt;
&lt;p&gt;And all of these conversations got me thinking:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I should really rework my infrastructure workflow to fully leverage uv.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;how-we-run-talk-python&#34;&gt;How we run Talk Python&lt;/h2&gt;
&lt;p&gt;I haven&amp;rsquo;t written up how we run our devops over at &lt;a href=&#34;https://talkpython.fm&#34;&gt;Talk Python&lt;/a&gt;. It&amp;rsquo;s something I&amp;rsquo;ve been meaning to do as it&amp;rsquo;s undergone big changes recently.&lt;/p&gt;
&lt;p&gt;The short version is that &lt;strong&gt;we&amp;rsquo;ve consolidated all our 6 smaller servers into one properly big machine (8 CPUs)&lt;/strong&gt; and run 17 multi-tier apps all running within Docker containers using Docker Compose. You can get a sense of some of these apps by looking at our &lt;a href=&#34;https://uptimekuma.talkpython.fm/status/all-list&#34;&gt;infrastructure status page&lt;/a&gt;, which also runs there.&lt;/p&gt;
&lt;p&gt;To keep things simple and consistent, &lt;strong&gt;we have two Docker images that all our apps we write are based upon&lt;/strong&gt;. One provides a custom Ubuntu Linux base and on that, we build a custom Python 3 image that has much of the shared tooling configured the same.&lt;/p&gt;
&lt;p&gt;The Python 3 container had previously been managing Python by building it from source. And before you tell me there are official Python images, &lt;a href=&#34;https://hub.docker.com/_/python&#34;&gt;I know&lt;/a&gt;. But they don&amp;rsquo;t have Ubuntu and I&amp;rsquo;d rather have more complete control over my images. Plus, if I wanted to use another image that doesn&amp;rsquo;t base itself off a Python image, I&amp;rsquo;d have to manage it somehow anyway.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Building Python 3.14 from source is slow&lt;/strong&gt;! It took up to 10 minutes to build that image. Luckily Docker caches it and we&amp;rsquo;ve only had to rebuild it 4 times this year.&lt;/p&gt;
&lt;h2 id=&#34;and-now-a-better-way&#34;&gt;And now a better way&lt;/h2&gt;
&lt;p&gt;One of the exciting new features of uv is the ability to create a virtual environment based on a specified Python binary that it manages and installs if needed. &lt;strong&gt;This takes just several seconds&lt;/strong&gt; rather than 10 minutes! Here&amp;rsquo;s the command:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;uv venv --python 3.14 /venv
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;With that capability, we can create a new hierarchy of images:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;linux&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;   &lt;span class=&#34;o&#34;&gt;|-&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;python&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;base&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# (starting from the uv python command)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;o&#34;&gt;|&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;        &lt;span class=&#34;o&#34;&gt;|-&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;your&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;app&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;-&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;container&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;A Dockerfile for python-base might look like this:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;FROM&lt;/span&gt;&lt;span class=&#34;w&#34;&gt; &lt;/span&gt;&lt;span class=&#34;s&#34;&gt;linuxbase:latest&lt;/span&gt; &lt;span class=&#34;c1&#34;&gt;# our customized Ubuntu&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ENV&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;PATH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/venv/bin:&lt;span class=&#34;nv&#34;&gt;$PATH&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;ENV&lt;/span&gt; &lt;span class=&#34;nv&#34;&gt;PATH&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;/root/.cargo/bin:&lt;span class=&#34;nv&#34;&gt;$PATH&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# install uv&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; curl -LsSf https://astral.sh/uv/install.sh &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; sh&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# set up a virtual env to use for whatever app is destined for this container.&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; uv venv --python 3.14 /venv&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;making-it-really-fly&#34;&gt;Making it really fly&lt;/h2&gt;
&lt;p&gt;With a few &lt;em&gt;impure&lt;/em&gt; practicalities, we can really make this fast.&lt;/p&gt;
&lt;p&gt;For example, if most of our apps are based on &lt;a href=&#34;https://talkpython.fm/episodes/show/472/state-of-flask-and-pallets-in-2024&#34;&gt;Flask&lt;/a&gt; and use &lt;a href=&#34;https://talkpython.fm/episodes/show/463/running-on-rust-granian-web-server&#34;&gt;Granian&lt;/a&gt;, then we could add this line to the &lt;strong&gt;python-base&lt;/strong&gt; image:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; uv pip install --upgrade flask pydantic beanie&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; uv pip install --upgrade granian&lt;span class=&#34;o&#34;&gt;[&lt;/span&gt;pname&lt;span class=&#34;o&#34;&gt;]&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# No these packages should not have their versions pinned.&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Because uv is SO fast, subsequent installs of common libraries will be instant if you happen to have the same version, which is mostly what happens.&lt;/p&gt;
&lt;p&gt;Then be sure to run uv rather than pip for your app level dependencies. For example:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;k&#34;&gt;RUN&lt;/span&gt; uv pip install -r requirements.txt&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c&#34;&gt;# These packages do have their versions pinned, but mostly overlap with latest because I&amp;#39;m not a monster.&lt;/span&gt;&lt;span class=&#34;err&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;h2 id=&#34;a-working-flask-example&#34;&gt;A working Flask example&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s an example you can play with that runs the &amp;ldquo;Hello World&amp;rdquo; Flask app with Granian in Docker:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://shared.mkennedy.codes/faster-docker-v2.zip?v=2&#34;&gt;faster-docker.zip&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Just unzip it and run &lt;code&gt;docker compose build&lt;/code&gt; in the same folder as &lt;code&gt;compose.yaml&lt;/code&gt; to build and see how quickly it all comes together. In particular, once the linuxbase is built, just run &lt;code&gt;docker compose build pythonbase --no-cache&lt;/code&gt; to see &lt;strong&gt;how quickly we get from Ubuntu to Python 3 with uv&lt;/strong&gt;. Then start the website with &lt;code&gt;docker compose up&lt;/code&gt;.&lt;/p&gt;
&lt;h2 id=&#34;some-screenshots&#34;&gt;Some screenshots&lt;/h2&gt;
&lt;p&gt;Python base image installing uv and Python plus some common dependencies in 7 seconds:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-docker-images-using-uv-s-new-python-features/31-oythonbase-build.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Our app&amp;rsquo;s container with dependencies and set up in 800ms!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-docker-images-using-uv-s-new-python-features/31-app-build.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;its-already-in-action&#34;&gt;It&amp;rsquo;s already in action&lt;/h2&gt;
&lt;p&gt;So the next time you &lt;a href=&#34;https://talkpython.fm/episodes/show/476/unified-python-packaging-with-uv&#34;&gt;listen to a podcast episode&lt;/a&gt; or &lt;a href=&#34;https://training.talkpython.fm/courses/all&#34;&gt;take one of our courses&lt;/a&gt;, you can think about this running behind the scenes making our continuous deployment story even better.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Keynote: The State of Python in 2024</title>
            <link>https://mkennedy.codes/posts/keynote-the-state-of-python-in-2024-pycon-philippines/</link>
            <pubDate>Thu, 04 Apr 2024 17:35:23 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/keynote-the-state-of-python-in-2024-pycon-philippines/</guid>
            <description>&lt;p&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=coz1CGRxjQ0&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/keynote-the-state-of-python-in-2024-pycon-philippines/30-keynote.webp&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This is a short post. I&amp;rsquo;m excited to announce that &lt;strong&gt;my keynote at PyCon Philippines is now available&lt;/strong&gt; for everyone &lt;a href=&#34;https://www.youtube.com/watch?v=coz1CGRxjQ0&#34;&gt;on YouTube&lt;/a&gt;. Have a watch and let me know what you think in the comment section on the video.&lt;/p&gt;
&lt;p&gt;Cheers, Michael.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Use Custom Search Engines Way More</title>
            <link>https://mkennedy.codes/posts/you-should-use-custom-search-engines-way-more/</link>
            <pubDate>Sun, 28 Jan 2024 09:04:10 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/you-should-use-custom-search-engines-way-more/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/you-should-use-custom-search-engines-way-more/29-search.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Add custom search engines to your browser with short prefixes. Type &amp;ldquo;gh react&amp;rdquo; to search GitHub, &amp;ldquo;pypi requests&amp;rdquo; for PyPI, &amp;ldquo;u mountains&amp;rdquo; for Unsplash. Skip the middleman and search sites directly from your address bar.&lt;/p&gt;
&lt;p&gt;What&amp;rsquo;s your favorite search engine? Do you live in the privacy-side of the web and choose DuckDuckGo, Kagi, or Brave Search? Are you chasing search points over at Bing? YOLO and just Google it?&lt;/p&gt;
&lt;p&gt;Regardless of how you search, I&amp;rsquo;ve got a quick tip that&amp;rsquo;ll make you more productive on the web and preserve some privacy as a nice side benefit.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s how I started this essay.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/you-should-use-custom-search-engines-way-more/29-essay-start.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;I was looking for a fun hero image to get my mind rolling and make this essay more fun: &lt;code&gt;u search&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;What the heck is the u? It&amp;rsquo;s &lt;a href=&#34;https://unsplash.com&#34;&gt;Unsplash&lt;/a&gt;, a lovely free and attribution optional stock image site.&lt;/p&gt;
&lt;p&gt;Many of the proper browsers let you enter custom search engines. My mental picture of these had basically been alternatives to Google and Bing such as Kagi. But I realized that almost any website that lets you search it, can be a custom search engine.&lt;/p&gt;
&lt;p&gt;Adding sites you often search lets you skip the step of either navigating there or searching your main search engine and then navigating around there for results on that site.&lt;/p&gt;
&lt;h2 id=&#34;so-many-niche-search-engines&#34;&gt;So many niche &amp;ldquo;search engines&amp;rdquo;&lt;/h2&gt;
&lt;p&gt;Here&amp;rsquo;s what I&amp;rsquo;m using that might inspire you:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;GitHub&lt;/strong&gt; - search GitHub repositories directly by using the prefix &lt;strong&gt;gh&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wikipedia&lt;/strong&gt; - all of Wikipedia with the prefix &lt;strong&gt;w&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;YouTube&lt;/strong&gt; - Search for videos directly on YouTube with &lt;strong&gt;y&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PyPI&lt;/strong&gt; - Find Python packages with &lt;strong&gt;pypi&lt;/strong&gt; prefix&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;FontAwesome&lt;/strong&gt; - Just the right &amp;ldquo;font as an icon&amp;rdquo; for my web design with &lt;strong&gt;fa&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;LinkedIn&lt;/strong&gt; - Find that contact with &lt;strong&gt;l&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unsplash&lt;/strong&gt; - Get a sweet stock photo with &lt;strong&gt;u&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;StackOverflow&lt;/strong&gt; - Still good for programming help, use &lt;strong&gt;so&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&amp;hellip;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In &lt;a href=&#34;https://vivaldi.com&#34;&gt;Vivaldi&lt;/a&gt;, we just go to Search &amp;gt; + and enter a name, search URL with &lt;code&gt;%s&lt;/code&gt; for the search term (e.g. &lt;code&gt;https://stackoverflow.com/search?q=%s&lt;/code&gt;), and a prefix to use when searching from the address bar (e.g. &lt;strong&gt;so&lt;/strong&gt;). Chrome, Firefox, and others have similar features.&lt;/p&gt;
&lt;p&gt;If you can find a URL on the site you care about that roughly matches that URL for StackOverflow, you can add it as a search engine.&lt;/p&gt;
&lt;p&gt;Now when I&amp;rsquo;m looking for a new Python package, I start my search with &lt;code&gt;pypi term&lt;/code&gt; I hope this has inspired you.&lt;/p&gt;
&lt;h2 id=&#34;search-talk-python-and-python-bytes-too&#34;&gt;Search Talk Python and Python Bytes Too&lt;/h2&gt;
&lt;p&gt;I liked this idea so much that I started wondering if maybe it was possible to surface important sources of data of mine to the world. Over at the &lt;a href=&#34;https://talkpython.fm/search&#34;&gt;Talk Python To Me&lt;/a&gt; and &lt;a href=&#34;https://pythonbytes.fm/search&#34;&gt;Python Bytes&lt;/a&gt; podcasts we have search that is very comprehensive.&lt;/p&gt;
&lt;p&gt;After a bit of work, you can now add two more excellent search locations to this list. They even come with keyword completion.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Talk Python search integration [&lt;a href=&#34;https://search.talkpython.fm/api/browser&#34;&gt;step by step set up&lt;/a&gt;]&lt;/li&gt;
&lt;li&gt;Python Bytes search integration [&lt;a href=&#34;https://search.pythonbytes.fm/api/browser&#34;&gt;step by step set up&lt;/a&gt;]&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;It turns out this involves the &lt;a href=&#34;https://developer.mozilla.org/en-US/docs/Web/OpenSearch&#34;&gt;OpenSearch description&lt;/a&gt; if you want to create something like this for your site as well.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Unsolicited Advice for Mozilla and Firefox</title>
            <link>https://mkennedy.codes/posts/michael-kennedys-unsolicited-advice-for-mozilla-and-firefox/</link>
            <pubDate>Thu, 11 Jan 2024 22:25:39 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/michael-kennedys-unsolicited-advice-for-mozilla-and-firefox/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/michael-kennedys-unsolicited-advice-for-mozilla-and-firefox/28-firefox-top-image.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Firefox is dying from lack of focus. My 4 paths forward: (1) Build a privacy-focused office/cloud suite to compete with Google, (2) Go all-in on privacy like Vivaldi/Brave, (3) Become a VC for like-minded startups, (4) Spoof Chrome&amp;rsquo;s user agent to stop &amp;ldquo;Firefox not supported&amp;rdquo; warnings.&lt;/p&gt;
&lt;p&gt;I recently read an article entitled &amp;ldquo;&lt;a href=&#34;https://techcrunch.com/2024/01/03/whats-next-for-mozilla/&#34;&gt;What&amp;rsquo;s next for Mozilla&lt;/a&gt;?&amp;rdquo; over at TechCrunch. Firefox has been off-track for a while now. That makes me sad because I was a longtime user and advocate of Firefox. I&amp;rsquo;ve since moved on to &lt;a href=&#34;https://vivaldi.com&#34;&gt;Vivaldi&lt;/a&gt;, but that&amp;rsquo;s another essay for another time.&lt;/p&gt;
&lt;p&gt;From the article:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;For the longest time, &lt;a href=&#34;https://www.mozilla.org/&#34;&gt;Mozilla&lt;/a&gt; was synonymous with the Firefox browser, but for the last few years, &lt;u&gt;Mozilla has started to &lt;a href=&#34;https://techcrunch.com/2022/11/17/mozilla-looks-to-its-next-chapter/&#34;&gt;look beyond Firefox&lt;/a&gt;&lt;/u&gt;, especially as its browser’s importance continues to wane.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Yikes. Firefox has been on a downward trend. Its usage is down from a high of around 30% to 3.5% generally, (7.6% on desktop). That&amp;rsquo;s not great. If that was just a fact of life, so it goes. But as I see it, the decline is due to a serious lack of focus and intention. Maybe Mozilla could use some unsolicited advice. They didn&amp;rsquo;t ask for it, but here it is from me anyway. I hope they take it as constructive feedback from someone who is a fan.&lt;/p&gt;
&lt;h2 id=&#34;missteps&#34;&gt;Missteps&lt;/h2&gt;
&lt;p&gt;My advice is informed by what I see as some of the missteps of recent times.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;First misstep&lt;/strong&gt;: Even while Firefox has been bleeding market share, they have been boosting the CEO pay ($2,458,350 in compensation from Mozilla, which represents a 400% pay raise since 2008) and &lt;a href=&#34;https://arstechnica.com/information-technology/2020/08/firefox-maker-mozilla-lays-off-250-workers-says-covid-19-lowered-revenue/&#34;&gt;laying off 25% of their employees&lt;/a&gt; (significantly in the developer tools and platform feature development areas). Platform feature development sure sounds like &amp;ldquo;Firefox team.&amp;rdquo;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second misstep&lt;/strong&gt;: Firefox has stepped away from web standards. One of the most important features for driving the web forward is PWAs (Progressive Web Apps). But Firefox decided they were too much trouble and actively removed them. The &lt;a href=&#34;https://connect.mozilla.org/t5/ideas/bring-back-pwa-progressive-web-apps/idi-p/35&#34;&gt;Mozilla Connect request to re-enable PWAs&lt;/a&gt; has over 32 pages of comments almost universally asking for them back. Meanwhile, look how much &lt;a href=&#34;https://support.apple.com/en-us/104996&#34;&gt;Apple is promoting the idea&lt;/a&gt; these days.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Third misstep&lt;/strong&gt;: Becoming a proxy for the biggest advertising company in the world. Google&amp;rsquo;s search deal means Mozilla &lt;a href=&#34;https://www.androidheadlines.com/2020/08/mozilla-firefox-google-search&#34;&gt;takes in 95% of its revenue&lt;/a&gt; directly from yearly agreements with Google. This &lt;strong&gt;seriously constrains the moves Firefox and Mozilla can make&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id=&#34;paths-forward-for-firefox-and-mozilla&#34;&gt;Paths forward for Firefox and Mozilla&lt;/h2&gt;
&lt;p&gt;According to the TechCrunch article above, Mozilla is moving into AI and launching &lt;a href=&#34;https://mozilla.ai&#34;&gt;Mozilla.ai&lt;/a&gt; because we need ethical AI. &lt;strong&gt;Nope&lt;/strong&gt;, that&amp;rsquo;s not going to fix things. There are 1,000s of companies running in every direction on AI, some ethical others not. But Mozilla trying to lead using ethical AI won&amp;rsquo;t change those who&amp;rsquo;d rather not be ethical in the industry. VCs are gonna VC after all.&lt;/p&gt;
&lt;h3 id=&#34;michaels-path-1-privacy-focused-docs-drive-and-browser&#34;&gt;Michael&amp;rsquo;s Path 1: Privacy-focused docs, drive, and browser&lt;/h3&gt;
&lt;p&gt;Mozilla should make an office suite in the cloud that is extremely privacy-focused and make sure Firefox is a top-tier client for it. We&amp;rsquo;re talking no advertising, end-to-end encryption, all the works.&lt;/p&gt;
&lt;p&gt;I envision this to be not just docs, but compete with many aspects of how people use the cloud SaaS world:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;GMail / Fastmail / Proton Mail&lt;/li&gt;
&lt;li&gt;Google Drive / Dropbox / Proton Drive&lt;/li&gt;
&lt;li&gt;Google Docs / Sheets / Presentation / Photos&lt;/li&gt;
&lt;li&gt;SalesForce CRM / All the other CRMs&lt;/li&gt;
&lt;li&gt;Invoicing / Document Signing / etc.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Just flood the market&lt;/strong&gt; with top-notch, privacy-focused, paid apps in this space.&lt;/p&gt;
&lt;p&gt;Can&amp;rsquo;t be done? No one can compete with Google Docs at this point? If you feel that way, Zoho would like to have a word with you. They &lt;a href=&#34;https://www.zoho.com/all-products.html&#34;&gt;have 55 (!) such apps&lt;/a&gt; and which are excellent. Zoho also &lt;a href=&#34;https://techcrunch.com/2022/09/10/how-zoho-became-1b-company-without-a-dime-of-external-investment/&#34;&gt;makes over $1B/yr revenue and are entirely self-funded&lt;/a&gt;. I&amp;rsquo;m a big fan. And that revenue is double what Mozilla makes even including the Google deal. Without Google, it&amp;rsquo;s 40x what Mozilla makes.&lt;/p&gt;
&lt;p&gt;For this suite, they&amp;rsquo;d have free tiers but charge for them earlier than Google. You pay Google with your data, you&amp;rsquo;d pay Mozilla for a product.&lt;/p&gt;
&lt;h3 id=&#34;michaels-path-2-go-all-in-on-privacy-in-firefox&#34;&gt;Michael&amp;rsquo;s Path 2: Go all-in on privacy in Firefox&lt;/h3&gt;
&lt;p&gt;Firefox is concerned with privacy, sorta. They don&amp;rsquo;t go &lt;em&gt;nearly&lt;/em&gt; as far as Vivaldi or Brave. Why? Well, misstep 3 is likely the reason. They are 100% beholden to a company that is the antithesis of privacy. If they anger Google too much, bankruptcy is quick on the heals.&lt;/p&gt;
&lt;p&gt;So they can be private-ish, but not &lt;em&gt;&lt;strong&gt;private&lt;/strong&gt;&lt;/em&gt;. Firefox doesn&amp;rsquo;t let you add 25 additional block lists for ads or malware like &lt;a href=&#34;https://nextdns.io/?from=a743zhpr&#34;&gt;nextdns.io&lt;/a&gt;. They don&amp;rsquo;t have super agressive built-in options for ad-blocking like Vivaldi and Brave. And so on. I wonder why&amp;hellip;&lt;/p&gt;
&lt;p&gt;Vivaldi and Brave are better than Firefox and are inspring. However, mimicking them probably won&amp;rsquo;t change Firefox&amp;rsquo;s fate. They should specialize.&lt;/p&gt;
&lt;p&gt;I recently learned of a new &lt;a href=&#34;https://www.island.io/&#34;&gt;&amp;ldquo;Enterprise Browser&amp;rdquo; called Island&lt;/a&gt;. It&amp;rsquo;s super interesting and coincidentlly cofounded by a friend of mine. Watch the video on the landing page. Island may have the market cornered already, but something special like this could really change the fate of Firefox.&lt;/p&gt;
&lt;h3 id=&#34;michaels-path-3-become-a-vc-for-like-minded-startups&#34;&gt;Michael&amp;rsquo;s Path 3: Become a VC for like-minded startups&lt;/h3&gt;
&lt;p&gt;This one &lt;a href=&#34;https://techcrunch.com/2022/11/02/mozilla-launches-35m-venture-capital-fund-for-early-stage-responsible-startups/&#34;&gt;they sorta doing already&lt;/a&gt;. They have $.5B / year and if they took a good chunk of that and invested just like a VC firm into startups that shared their ethos (not necessarily in browsers or open source, just web = open and good, etc.), that could probably be a big winner in the long term.&lt;/p&gt;
&lt;h3 id=&#34;michaels-path-4-lie-to-the-world&#34;&gt;Michael&amp;rsquo;s Path 4: Lie to the world&lt;/h3&gt;
&lt;p&gt;What, what? Yes, lie to the world (of websites).&lt;/p&gt;
&lt;p&gt;Should you trust the stat counters that put Chrome so high? No. But they are probably right for Firefox. The difference is that Vivaldi and Brave both lie about their user agent. What&amp;rsquo;s my &lt;strong&gt;Vivalid user agent&lt;/strong&gt; right now?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Brave&amp;rsquo;s user agent&lt;/strong&gt;?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;What&amp;rsquo;s &lt;strong&gt;Chrome&amp;rsquo;s user agent&lt;/strong&gt; right now?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;See the difference? &lt;strong&gt;None, and that&amp;rsquo;s a major advantage&lt;/strong&gt;. Vivaldi claims to be Chrome so websites don&amp;rsquo;t refuse to run.&lt;/p&gt;
&lt;p&gt;But websites often rail against:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:121.0) Gecko/20100101 Firefox/121.0&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That&amp;rsquo;s Firefox right now. Maybe Firefox should just claim to be Chrome and all those warnings will go away. Firefox &lt;em&gt;usually&lt;/em&gt; works anyway. One of the main drivers pushing people away from Firefox are all those websites warning &amp;ldquo;This website won&amp;rsquo;t work in Firefox!!!!&amp;rdquo; A white lie like this one would completely elimante that pressure.&lt;/p&gt;
&lt;h2 id=&#34;there-it-is&#34;&gt;There it is&lt;/h2&gt;
&lt;p&gt;That&amp;rsquo;s my advice. I hope it gave you some food for thought. And for folks from Mozilla reading this, I wish you well and hope to see Firefox grow. If you want to talk more, hit me up in the email below.&lt;/p&gt;
&lt;h2 id=&#34;have-thoughts&#34;&gt;Have Thoughts?&lt;/h2&gt;
&lt;p&gt;Join the discussion of this post over on &lt;a href=&#34;https://fosstodon.org/@mkennedy/111762325365466280&#34;&gt;Mastodon&lt;/a&gt; or &lt;a href=&#34;https://x.com/mkennedy/status/1747647187373424936&#34;&gt;ExTwitter&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;ps---looking-for-tech-advice&#34;&gt;PS - Looking for tech advice?&lt;/h2&gt;
&lt;p&gt;If you&amp;rsquo;re looking for tech advice for your own company, generally or on a Python projects, I have recently started offering tech advising services and would be happy to talk with you. Just &lt;a href=&#34;mailto:michael@talkpython.fm&#34;&gt;shoot me an email&lt;/a&gt;.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>AI Features a Waste of Time?</title>
            <link>https://mkennedy.codes/posts/ai-features-a-waste-of-time/</link>
            <pubDate>Fri, 05 Jan 2024 17:15:38 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/ai-features-a-waste-of-time/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/ai-features-a-waste-of-time/27-ai-top-image.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;I was letting my mind wander the other day and this thought hit me hard:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;How many cumulative programmer-hours have been &lt;em&gt;&lt;strong&gt;utterly wasted&lt;/strong&gt;&lt;/em&gt; on adding very mediocre AI features to every app imaginable which rarely actually helps you?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Speaking from a pure product perspective, I’m thinking of putting off that work-a-day feature like “Autocomplete should work for name or email address, not just email” and &lt;strong&gt;instead we get months of effort for a garbage AI integration that you try once, just for the novelty&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;There was a great conversation on Mastodon about this, feel free to dive into it &lt;a href=&#34;https://fosstodon.org/@mkennedy/111699944505571758&#34;&gt;over there&lt;/a&gt;. Here&amp;rsquo;s a deeper version of that idea.&lt;/p&gt;
&lt;h2 id=&#34;an-example-spark-email&#34;&gt;An Example: Spark Email&lt;/h2&gt;
&lt;p&gt;Let&amp;rsquo;s expand on this a bit with an example (and make this idea more permanent than a social feed screaming past) and talk about the Spark Email client.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://sparkmailapp.com&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/ai-features-a-waste-of-time/27-spark-page.webp&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I&amp;rsquo;m actually a huge fan of this app&lt;/strong&gt;. It&amp;rsquo;s revolutionized how I handle email personally and across many accounts at Talk Python (contact@, sales@, support@, etc.).&lt;/p&gt;
&lt;p&gt;But Spark is deep in the mode of adding AI for hype&amp;rsquo;s sake and who cares if it screws the product or distracts from incredibly low-hanging bug fixes and features.&lt;/p&gt;
&lt;p&gt;You see the &amp;ldquo;Now with +AI&amp;rdquo; in the image above, right? That&amp;rsquo;s foreshadowing.&lt;/p&gt;
&lt;h3 id=&#34;a-work-a-day-improvement-they-missed&#34;&gt;A work-a-day improvement they missed&lt;/h3&gt;
&lt;p&gt;Spark lets you snooze email and does so, along with many other commands, through their command palette (CMD/CTRL + K). Just hit those keys, type snooze, then type how long.&lt;/p&gt;
&lt;p&gt;But what can you enter? It let you pick tomorrow and next week from a list or type a custom time. But the accepted text for a time is very very limited.&lt;/p&gt;
&lt;p&gt;You can type &lt;strong&gt;jan, 6 at 1:30&lt;/strong&gt; and it&amp;rsquo;ll snooze until then, awesome! But what if you screw up and type &lt;strong&gt;January, 6 at 1:30&lt;/strong&gt;?&lt;/p&gt;
&lt;p&gt;Nothing, you can&amp;rsquo;t snooze until then! What if Jan 6 is actually next Tuesday? Can you say snooze until &lt;strong&gt;Next Tuesday at 1:30&lt;/strong&gt;? &lt;em&gt;Nope&lt;/em&gt;, it&amp;rsquo;s Jan not January and not next Tuesday, nor is it &lt;strong&gt;in 3 days&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The app is &lt;em&gt;full&lt;/em&gt; of little rough edges like this.&lt;/p&gt;
&lt;p&gt;Improving snooze is probably an afternoon for one of the main devs.&lt;/p&gt;
&lt;p&gt;But no, it&amp;rsquo;s &amp;ldquo;Now with +AI&amp;rdquo;. Don&amp;rsquo;t you see how much better it is?&lt;/p&gt;
&lt;h2 id=&#34;what-do-we-get-with-this-ai&#34;&gt;What do we get with this AI?&lt;/h2&gt;
&lt;p&gt;Because it has AI, it does awesome stuff like using the AI to fix grammar and spelling. Let&amp;rsquo;s see how this amazing feature works.&lt;/p&gt;
&lt;p&gt;I first write a nice email (lovely looking, right?)&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/ai-features-a-waste-of-time/27-email-before.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;See that &lt;strong&gt;just&lt;/strong&gt; was misspelled along with a few other words in that sentence. Here comes the AI to the rescue.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/ai-features-a-waste-of-time/27-email-menu.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Below, you can see it &lt;em&gt;perfectly&lt;/em&gt; corrects the spelling for the email. Thank you!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/ai-features-a-waste-of-time/27-email-after.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Yes indeed! The spelling is fixed. AI for the win. But because this is just jammed into the product, &lt;strong&gt;it absolutely destroyed all the formatting&lt;/strong&gt;. The cool quote thing is gone. The image is actually deleted. The bold is gone. Everything is gone except for plaintext.&lt;/p&gt;
&lt;h2 id=&#34;can-we-focus-for-a-bit&#34;&gt;Can we focus for a bit?&lt;/h2&gt;
&lt;p&gt;Just how much better would this already nice app be if they just hit all the little rough edges and fixed things like that insane percision that is needed for snooze? Or even make the AI work like you&amp;rsquo;d expect.&lt;/p&gt;
&lt;p&gt;This is what I mean when I asked: How many cumulative programmer-hours have been &lt;em&gt;&lt;strong&gt;utterly wasted&lt;/strong&gt;&lt;/em&gt; on adding &lt;strong&gt;very mediocre&lt;/strong&gt; AI features to every app imaginable which rarely actually helps you?&lt;/p&gt;
&lt;p&gt;And yes, I did report both of these bugs (lack of variations for snoozing and format breaking under AI spell checking) a few months ago. Maybe in 6 months when the AI features are done, we can tweak the regex for snooze, maybe, &amp;hellip;, some day. Meanwhile, keep on proofreading with AI I guess.&lt;/p&gt;
&lt;h2 id=&#34;have-thoughts&#34;&gt;Have Thoughts?&lt;/h2&gt;
&lt;p&gt;Join the discussion of this post over on &lt;a href=&#34;https://fosstodon.org/@mkennedy/111711193230437295&#34;&gt;Mastodon&lt;/a&gt;.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Hide Those Terminal Secrets!</title>
            <link>https://mkennedy.codes/posts/hide-those-terminal-secrets/</link>
            <pubDate>Mon, 04 Dec 2023 11:59:55 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/hide-those-terminal-secrets/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Use Warp terminal&amp;rsquo;s secret redaction feature to automatically hide IP addresses, emails, API keys, and other sensitive data when screen sharing or recording. Enable it in settings and add custom regexes for your specific secrets.&lt;/p&gt;
&lt;p&gt;Do you do presentations and want/need to show your terminal? Maybe it&amp;rsquo;s a conference talk and it&amp;rsquo;d be awesome to pull an example from a real log file but you&amp;rsquo;re terrified that, maybe, a user&amp;rsquo;s email or IP address were to appear on screen?&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s a real concern.&lt;/p&gt;
&lt;p&gt;Personally, I do a ton of live streaming for the podcasts (&lt;a href=&#34;https://talkpython.fm&#34;&gt;1&lt;/a&gt;, &lt;a href=&#34;https://pythonbytes.fm&#34;&gt;2&lt;/a&gt;) as well as &lt;a href=&#34;https://training.talkpython.fm/courses/all&#34;&gt;course recording&lt;/a&gt; and I&amp;rsquo;ve definitely stopped short for this reason.&lt;/p&gt;
&lt;h2 id=&#34;warp-a-nicer-terminal&#34;&gt;Warp: A Nicer Terminal&lt;/h2&gt;
&lt;p&gt;Back at &lt;a href=&#34;https://www.youtube.com/watch?v=aKcolk8lGGk&#34;&gt;PyBay 2023&lt;/a&gt;, I was introduced to &lt;a href=&#34;https://app.warp.dev/referral/96PYZY&#34;&gt;Warp&lt;/a&gt;. It&amp;rsquo;s an awesome terminal for many reason which I&amp;rsquo;ve completely switched to. I suggest you watch the &lt;a href=&#34;https://www.youtube.com/watch?v=XWQY8LgkiXM&#34;&gt;YouTube trailer&lt;/a&gt; to see what it&amp;rsquo;s about.&lt;/p&gt;
&lt;p&gt;But it also has a &lt;strong&gt;new secret redaction feature&lt;/strong&gt;. It&amp;rsquo;s off by default, but easy to turn on:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/hide-those-terminal-secrets/26-setting.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;I even added a few custom regexes to hide a couple of API keys that were appearing in my logs.&lt;/p&gt;
&lt;h2 id=&#34;some-examples&#34;&gt;Some Examples&lt;/h2&gt;
&lt;p&gt;Try this: Log into one of your servers and &lt;strong&gt;post a screenshot of the welcome screen&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;No? I would &lt;strong&gt;not&lt;/strong&gt; do it with all the various settings/versions/ip addresses/etc. visible with the basic terminal. Here&amp;rsquo;s mine from Warp:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/hide-those-terminal-secrets/26-login.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Cool eh?&lt;/p&gt;
&lt;p&gt;How about &lt;strong&gt;tailing your log&lt;/strong&gt; which may include API keys, emails, IP addresses (just 127.0.0.1 in this case but you still see the effect), etc:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.mkennedy.codes/posts/hide-those-terminal-secrets/26-tail.webp&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/hide-those-terminal-secrets/26-tail.webp&#34; alt=&#34;Click to enlarge&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;thats-it&#34;&gt;That&amp;rsquo;s It&lt;/h2&gt;
&lt;p&gt;There&amp;rsquo;s not much to add other than &lt;strong&gt;now you know&lt;/strong&gt;! I hope this helps you work with clients and consultants as well as in those presentation-type situations.&lt;/p&gt;
&lt;p&gt;PS - If you do happen to want to check out Warp, use my &lt;a href=&#34;https://app.warp.dev/referral/96PYZY&#34;&gt;referral link&lt;/a&gt;. All it gets me is a t-shirt, but why not, eh?&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Don&#39;t Sweat the Ad Blocker Drama</title>
            <link>https://mkennedy.codes/posts/dont-sweat-the-ad-blocker-drama/</link>
            <pubDate>Tue, 28 Nov 2023 13:05:08 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/dont-sweat-the-ad-blocker-drama/</guid>
            <description>&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/dont-sweat-the-ad-blocker-drama/25-blocked-anyway.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Skip browser-based ad blockers entirely. Use NextDNS ($1.65/mo) for network-level blocking that works on all devices, everywhere. Chrome&amp;rsquo;s Manifest V3 can&amp;rsquo;t touch it.&lt;/p&gt;
&lt;p&gt;You may have read with some dread (as I did) about Google&amp;rsquo;s various initiatives to block ad-blockers in Chrome by effectively breaking the Chrome Extension features via something called Manifest V3. What are we going to call these anyway, &lt;em&gt;ad-blocker-blockers&lt;/em&gt;? :)&lt;/p&gt;
&lt;p&gt;Ars Technica has a great write-up entitled &lt;a href=&#34;https://arstechnica.com/gadgets/2023/11/google-chrome-will-limit-ad-blockers-starting-june-2024/&#34;&gt;Google Chrome will limit ad blockers starting June 2024&lt;/a&gt; if you want to learn more.&lt;/p&gt;
&lt;p&gt;But here&amp;rsquo;s a quick tip to help you avoid this limitation and so so much more.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Just use a network-level ad blocker&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You may have heard of a PiHole - it&amp;rsquo;s not an insult but a way to use a RaspberryPi to filter your DNS traffic on your network. It&amp;rsquo;s fine and all but it&amp;rsquo;s another thing to maintain and only works on your home network (although that&amp;rsquo;s excellent), and seems generally limited to me.&lt;/p&gt;
&lt;p&gt;Instead, &lt;strong&gt;I&amp;rsquo;ve been using a service that is similar, takes zero set up, and works anywhere I am in the world: &lt;a href=&#34;https://nextdns.io/?from=a743zhpr&#34;&gt;NextDNS&lt;/a&gt;&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Rather than leaning on your browser to block all the baddies, just let your network deny access entirely. This works in your web browser, but it also works on all your mobile apps, on your smart TVs, everything. It&amp;rsquo;s &lt;em&gt;way better&lt;/em&gt; than an ad blocker.&lt;/p&gt;
&lt;p&gt;It starts out free, but if you do a lot on the internet (or have many people and devices at your house like I do), you&amp;rsquo;ll probably have to pay. &lt;strong&gt;It&amp;rsquo;s only $1.65/mo and so worth it&lt;/strong&gt;.&lt;/p&gt;
&lt;h1 id=&#34;how-it-works&#34;&gt;How it works&lt;/h1&gt;
&lt;p&gt;Just create an account at &lt;a href=&#34;https://nextdns.io/?from=a743zhpr&#34;&gt;NextDNS&lt;/a&gt; (that &lt;a href=&#34;https://nextdns.io/?from=a743zhpr&#34;&gt;link&lt;/a&gt; gives me a very small credit towards my account BTW), &amp;ldquo;link your IP address&amp;rdquo; so that your local network is protected across all your devices (as I mentioned above) and &lt;strong&gt;adjust your router to use their DNS IP addresses instead of your ISP&amp;rsquo;s&lt;/strong&gt;. Do make sure to update the linked IP if yours changes, mine rarely does.&lt;/p&gt;
&lt;p&gt;If your router has support for DNS over HTTPS, you can set that and forget it.&lt;/p&gt;
&lt;h2 id=&#34;add-it-to-your-browser&#34;&gt;Add it to your browser&lt;/h2&gt;
&lt;p&gt;That works when you&amp;rsquo;re at home. But what if you are at a coffee shop or traveling, or even using a VPN that changes the DNS?&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;ll definitely want to enter the DNS over HTTP from your account into your browser as well. Then you have it globally!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/dont-sweat-the-ad-blocker-drama/25-dns.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;enjoy-the-cleaner-and-safer-web-again&#34;&gt;Enjoy the cleaner and safer web again&lt;/h2&gt;
&lt;p&gt;Now you should have all your devices at home running ad/malware/tracking free as well as your browser while out and about. You can see just one day of a couple of us a home here:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/dont-sweat-the-ad-blocker-drama/25-stats.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;please-disable-your-ad-blocker&#34;&gt;Please disable your ad blocker&amp;hellip;&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Ooops&lt;/strong&gt;&lt;/em&gt;. If you run an ad blocker, you&amp;rsquo;ll run across some sh*tty company that makes you disable your ad blocker to continue all the while forcing potentially malicuous and definitely privacy invading content on you. That&amp;rsquo;s why you care about this post at all, right?&lt;/p&gt;
&lt;p&gt;But what if you need to disable this whole setup? It would be a mega pain to reconfigure your DNS and your router. But no need.&lt;/p&gt;
&lt;p&gt;Just install a second browser and set its DNS to use a non-blocking DNS such as Cloudflare&amp;rsquo;s 1.1.1.1. Even though this is sold as a privacy-oriented DNS, it&amp;rsquo;s not blocking anything other than malware.&lt;/p&gt;
&lt;p&gt;I use Vivaldi as my main browser. So I set up Firefox as a fallback. When anything fails to work because of all the blocking, I just launch Firefox and everything works (albeit without the privacy).&lt;/p&gt;
&lt;p&gt;Here are my DNS settings in Firefox:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/dont-sweat-the-ad-blocker-drama/25-escape-hatch.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Note: &lt;strong&gt;Safari won&amp;rsquo;t work here&lt;/strong&gt; because it doesn&amp;rsquo;t allow you to configure the DNS (hence making it a crummy browser choice all together anyway).&lt;/p&gt;
&lt;h2 id=&#34;chrome-gonna-chrome&#34;&gt;Chrome Gonna Chrome&lt;/h2&gt;
&lt;p&gt;So regardless of whether Chrome and Google go further away from their &amp;ldquo;Don&amp;rsquo;t be evil&amp;rdquo; pledge and turn Chrome even more into an ad surveillance tool by breaking ad blockers, you&amp;rsquo;ll be way more protected.&lt;/p&gt;
&lt;p&gt;You probably should be using &lt;a href=&#34;https://vivaldi.com&#34;&gt;Vivaldi&lt;/a&gt; or another browser other than Chrome. But that&amp;rsquo;s another conversation for another post. Even so, this makes privacy-first browsers like Vivaldi and Firefox better as well.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>You Can Ignore This Post</title>
            <link>https://mkennedy.codes/posts/github-gitignore-repo-is-open-to-all/</link>
            <pubDate>Thu, 25 May 2023 10:43:38 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/github-gitignore-repo-is-open-to-all/</guid>
            <description>&lt;p&gt;GitHub has a whole repo full of ignores you can make use of. You might know that uncertainty of creating a new GitHub repository and are confronted with the dropdown &amp;ldquo;&lt;strong&gt;Add .gitignore&lt;/strong&gt;&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;Do you choose Python? You are writing a Flask app, right? Wait, should you choose VisualStudio if you&amp;rsquo;re using VS Code? Maybe Node is the right choice if you are using npm for some front-end things.&lt;/p&gt;
&lt;p&gt;You do have to think about this up front because it&amp;rsquo;s not possible in GitHub to change your mind after creating one of these repos.&lt;/p&gt;
&lt;p&gt;But don&amp;rsquo;t despair. GitHub actually has all of these &lt;code&gt;.gitignore&lt;/code&gt; files in an open-source repo here:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/github/gitignore&#34;&gt;github.com/github/gitignore&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/github-gitignore-repo-is-open-to-all/24-ignore-repo.webp&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;So you can choose which ignore matches your project best. If you add a technology, just visit the repo, find the tech you&amp;rsquo;re adding, and copy over the relevant .gitignore lines into your existing repo. Boom, upgraded.&lt;/p&gt;
&lt;h2 id=&#34;bonus&#34;&gt;Bonus&lt;/h2&gt;
&lt;p&gt;You can also open PRs and file issues against the standard templete that GitHub uses if you feel the default .gitignore for a tech is out of date. I&amp;rsquo;m not sure how receiptive GitHub is to these suggestions but it&amp;rsquo;s an option now that you know where to look.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Rebuilding Mobile Apps at Talk Python</title>
            <link>https://mkennedy.codes/posts/mobile-apps-at-talk-python-python-flutter/</link>
            <pubDate>Mon, 15 May 2023 11:12:00 -0700</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/mobile-apps-at-talk-python-python-flutter/</guid>
            <description>&lt;p&gt;&lt;a href=&#34;https://training.talkpython.fm/apps&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/mobile-apps-at-talk-python-python-flutter/23-new-app-export.png&#34; style=&#34;width: 100%; border-radius: 10px;&#34; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Talk Python&amp;rsquo;s mobile apps have been rebuilt from scratch in Flutter, replacing the end-of-life Xamarin version. Download the new apps for iOS and Android to watch courses offline with a much better UI.&lt;/p&gt;
&lt;p&gt;We just completed a wonderful journey over at &lt;a href=&#34;https://training.talkpython.fm/courses/all&#34;&gt;&lt;strong&gt;Talk Python Training&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We offer &lt;strong&gt;over 240 hours&lt;/strong&gt; of Python course videos at Talk Python. Of course, we want our users to have the very best experience taking our courses. Unfortunately, the mobile web experience is less than ideal for watching video - especially video that is broken into small blocks best for reference usage after taking the course. For example, on iOS you can neither auto advance to the next video nor hide the navigation around the web browser leading postage stamp size videos you have to keep clicking. Yuck.&lt;/p&gt;
&lt;p&gt;The most important missing feature is the ability to download and use our content offline (for trips, sketchy or restricted networks, and other situations).&lt;/p&gt;
&lt;h2 id=&#34;enter-the-mobile-apps&#34;&gt;Enter the mobile apps&lt;/h2&gt;
&lt;p&gt;Back in 2018, we knew this was a problem and build a set of mobile apps for phones and tablets on both iOS and Android. At the time, we chose Xamarin (as C# / .NET based mobile framework). It worked fairly well, but never gave the polished, truly professional experience I was hoping for. Still, it solved many of the shortcomings discussed above.&lt;/p&gt;
&lt;p&gt;Since then, &lt;a href=&#34;https://ballardchalmers.com/resources/end-of-support-date-announced-for-xamarin/#:~:text=News%20for%20Xamarin%20developers%20with,until%20May%201st%202024.&#34;&gt;&lt;strong&gt;Xamarin has reached end of the line&lt;/strong&gt;&lt;/a&gt; and will not be supported further. So the last thing we wanted to do is build on top of a dying framework.&lt;/p&gt;
&lt;h2 id=&#34;mobile-apps-v2-flutter&#34;&gt;Mobile apps, v2 (Flutter)&lt;/h2&gt;
&lt;p&gt;What framework to choose? After considering 3-4 options, I chose &lt;a href=&#34;https://flutter.dev&#34;&gt;&lt;strong&gt;Flutter&lt;/strong&gt;&lt;/a&gt; for our platform. It&amp;rsquo;s based on Dart, a nice language. The UI is beautiful. It compiles to native code on every platform (iOS, Android, macOS, Windows, and Linux). And, it&amp;rsquo;s a very popular ecosystem with 3x the number of Github stars as Python (153k vs. 53k) and nearly 2x as many as React Native (110k).&lt;/p&gt;
&lt;p&gt;After working many long days with the talented &lt;strong&gt;&lt;a href=&#34;https://www.linkedin.com/in/loren-aguey-51827947/&#34;&gt;Loren Aguey&lt;/a&gt;&lt;/strong&gt; (Flutter developer on this project), the next generation of Talk Python Training&amp;rsquo;s mobile apps are available on all the mobile app stores. Just look at this UI, brilliant.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://training.talkpython.fm/apps&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/mobile-apps-at-talk-python-python-flutter/23-new-app-export.png&#34; style=&#34;width: 100%; border-radius: 10px;&#34; /&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;download-the-app&#34;&gt;Download the app&lt;/h2&gt;
&lt;p&gt;Get the app on your platform of choice. It comes in both a phone and tablet form-factor:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://itunes.apple.com/us/app/talk-python-training/id1460583670&#34;&gt;&lt;strong&gt;iOS AppStore&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://play.google.com/store/apps/details?id=fm.talkpython.training.player&#34;&gt;&lt;strong&gt;Android GooglePlay Store&lt;/strong&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;More to come&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;like-the-new-version-please-review-it&#34;&gt;Like the new version? Please review it&lt;/h2&gt;
&lt;p&gt;If you like this new version of the Talk Python courses app, please consider taking a few minutes to review it at your platform&amp;rsquo;s app store. It&amp;rsquo;d make a big difference to us. Many of the existing reviews are either very old or are complaints about issues with the old version. Help us give the app&amp;rsquo;s public face a clean up.&lt;/p&gt;
&lt;p&gt;Thank you to everyone who has supported me and Talk Python by taking one or more of &lt;a href=&#34;https://training.talkpython.fm/courses/all&#34;&gt;&lt;strong&gt;our courses&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Dev on the Road</title>
            <link>https://mkennedy.codes/posts/dev-on-the-road-leaving-your-laptop-at-home/</link>
            <pubDate>Mon, 09 Jan 2023 10:40:00 -0800</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/dev-on-the-road-leaving-your-laptop-at-home/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Leave your laptop at home. For travel emergencies, use: (1) Prompt app for SSH access, (2) GitHub.dev for editing code in the browser, (3) GitPod for a full dev environment. All work on an iPad.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h3 id=&#34;the-problem&#34;&gt;The Problem&lt;/h3&gt;
&lt;p&gt;I am going on a non-work trip such as a weekend getaway to the coast or a week of skiing. I won&amp;rsquo;t be doing much or any work &lt;strong&gt;unless there is a critical infrastructure error&lt;/strong&gt;. If there is a &lt;em&gt;the website is down!&lt;/em&gt; sort of situation I have to jump in.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;You may identify with this problem.&lt;/p&gt;
&lt;p&gt;Luckily for me, this is a very rare occurrence. But the consequences are also high if it does arise.&lt;/p&gt;
&lt;p&gt;What if something goes bonkers with the &lt;a href=&#34;https://training.talkpython.fm/courses/all&#34;&gt;Talk Python Training&lt;/a&gt; site and its backing MongoDB server? Possibly something happens at &lt;a href=&#34;https://talkpython.fm/digitalocean-partner&#34;&gt;DigitalOcean&lt;/a&gt; and I need to SSH into the server to fix it.&lt;/p&gt;
&lt;p&gt;I really don&amp;rsquo;t want to drag my $3,500 &lt;a href=&#34;https://www.apple.com/macbook-pro-14-and-16/&#34;&gt;MacBook Pro Max&lt;/a&gt; on the trip &lt;em&gt;just in case&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;But I&amp;rsquo;m always taking my iPad on trips. It&amp;rsquo;s my TV, my library, my magazine, photo album, and game station. All perfect for vacations and relaxing in the evenings.&lt;/p&gt;
&lt;h2 id=&#34;is-there-a-way-to-make-your-ipad-a-dev-machine&#34;&gt;Is there a way to make your iPad a dev machine?&lt;/h2&gt;
&lt;p&gt;Wow, I don&amp;rsquo;t know. That&amp;rsquo;s a stretch. But we can solve the problem above pretty well by approximating this. That is: &lt;strong&gt;How do I make my iPad enough of a dev machine to fix minor emergencies&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;ve been thinking about this problem for a long time (and dragging the laptop along too). I think I have a pretty solid set up.&lt;/p&gt;
&lt;h3 id=&#34;1-well-need-ssh-access-to-the-server-infrastructure&#34;&gt;1. We&amp;rsquo;ll need SSH access to the server infrastructure&lt;/h3&gt;
&lt;p&gt;SSH and especially SSH keys make me nervous. But keeping them locked on my iPad behind biometrics seems safe enough. So I chose &lt;a href=&#34;https://www.panic.com/prompt/&#34;&gt;Prompt from Panic Software&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;h3 id=&#34;prompt&#34;&gt;Prompt&lt;/h3&gt;
&lt;p&gt;It’s the &lt;em&gt;best SSH client for iOS&lt;/em&gt;. Restart your server from a coffee shop. Fix a web page from the back of a car. It’s elegant, powerful, and &lt;em&gt;always ready&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Prompt costs $15, once, but it&amp;rsquo;s very capable. It adds a special keyboard with modifier keys such as CTRL making it terminal friendly. And the SSH keys and logins are protected with FaceID.&lt;/p&gt;
&lt;h3 id=&#34;2-accessing-source-code&#34;&gt;2. Accessing source code&lt;/h3&gt;
&lt;p&gt;I don&amp;rsquo;t usually code with VS Code but it&amp;rsquo;s a very solid option. And, I have continuous deployment set up on a production branch over on GitHub. So if I can just make a change and somehow push code to that prod branch, then CI/CD will take it from there (or I&amp;rsquo;ll use Prompt above and get in the server to kick things over).&lt;/p&gt;
&lt;p&gt;You&amp;rsquo;re surely familiar with &lt;a href=&#34;https://GitHub.com&#34;&gt;GitHub.com&lt;/a&gt; but how about &lt;a href=&#34;https://GitHub.dev&#34;&gt;GitHub.dev&lt;/a&gt;? That&amp;rsquo;s VS Code hosted in the browser running on GitHub.&lt;/p&gt;
&lt;p&gt;If you open your repo on GitHub, for example:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/mikeckennedy/jinja_partials&#34;&gt;https://github.com/mikeckennedy/jinja_partials&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Then change the .com to .dev&lt;/strong&gt;, boom! It&amp;rsquo;ll drop you into that project in hosted VS Code. You&amp;rsquo;ll need write access to the repo so &lt;strong&gt;open one of your repos and give it a try&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/dev-on-the-road-leaving-your-laptop-at-home/22-github-dev.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;h3 id=&#34;3-dev-environment&#34;&gt;3. Dev environment&lt;/h3&gt;
&lt;p&gt;We have access to the servers with SSH, to the code with GitHub + VS Code in the browser. Yet sometimes you really need to run the code to figure things out.&lt;/p&gt;
&lt;p&gt;The final piece (which you hopefully don&amp;rsquo;t have to use) is &lt;a href=&#34;https://www.gitpod.io&#34;&gt;GitPod&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/dev-on-the-road-leaving-your-laptop-at-home/22-gitpod.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;h3 id=&#34;gitpod&#34;&gt;GitPod&lt;/h3&gt;
&lt;p&gt;Spin up pre-configured, standardized dev environments from any git context when you need them and close them when you&amp;rsquo;re done. You won’t go back to the friction of long-living stateful environments.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The idea is you connect GitPod to your GitHub repo. Configure it once to set up things like your version of Python and project dependencies and it builds the environment (in a Docker container I think).&lt;/p&gt;
&lt;p&gt;They also have a hosted in the browser VS Code that then connects you to that environment. From there, you should be able to write what you need and test + debug it on their infrastructure.&lt;/p&gt;
&lt;p&gt;The GitPod folks are on the same page as us here:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;Code anywhere, on any device&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;You no longer need an over-powered laptop to code, Gitpod works just as smoothly on a Chromebook or iPad. All you need is a browser.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Finally, pricing is free with 500 credits/mo or up to 50 hours per month.&lt;/p&gt;
&lt;h2 id=&#34;apple-this-is-your-fault&#34;&gt;Apple, this is your fault&lt;/h2&gt;
&lt;p&gt;And you can fix it.&lt;/p&gt;
&lt;p&gt;Of course, this whole iPad vs. laptop discussion is only because Apple is being dogmatic about iPadOS (ironic given &lt;a href=&#34;https://www.managingcommunities.com/2009/05/25/steve-jobs-dont-be-trapped-by-dogma-which-is-living-with-the-results-of-other-peoples-thinking/&#34;&gt;the Steve Jobs quote&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;If we had access to full macOS on the iPad, then we&amp;rsquo;d just do everything locally. After all, my iPad has an M1 processor and is just as fast as my desktop (Mac Mini) which I have been using daily for over 2 years. Apple has crippled the iPad with iPadOS.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Please Apple: Let us dual boot iPads into macOS for Apple Silicon&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;I&amp;rsquo;m not suggesting adding touch to macOS or anything invasive like that. If you could just dual boot the iPad into macOS it would all be self-contained and the same tools as we usually use would be available to us.&lt;/p&gt;
&lt;p&gt;This could easily require a keyboard and mouse, no touch. Thus, it would not require any changes to macOS. We&amp;rsquo;d be golden. I&amp;rsquo;m holding out for that. Until then, the tools above are a pretty good &amp;ldquo;just in case&amp;rdquo; travel set up.&lt;/p&gt;
&lt;h2 id=&#34;discuss&#34;&gt;Discuss&lt;/h2&gt;
&lt;p&gt;What do you think? Do you do something like this for traveling or are you dragging your laptop along these days?&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://fosstodon.org/@mkennedy/109660721346086147&#34;&gt;fosstodon.org/@mkennedy/109660721346086147&lt;/a&gt;&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Welcome back RSS</title>
            <link>https://mkennedy.codes/posts/welcome-back-rss/</link>
            <pubDate>Tue, 03 Jan 2023 00:00:00 +0000</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/welcome-back-rss/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; RSS is making a comeback as people tire of algorithmic feeds and walled gardens. It powers podcasting (Talk Python serves 0.2 TB of RSS XML monthly), and great readers like Reeder make it easy. If you&amp;rsquo;re a developer, add RSS to your projects.&lt;/p&gt;
&lt;p&gt;When was the &lt;strong&gt;last time you thought about RSS&lt;/strong&gt;? You know, that wacky XML format for subscribing to blogs?&lt;/p&gt;
&lt;p&gt;There are some interesting tech and cultural trends shining a light on RSS. They are making the case that RSS is key to sustaining and growing the web that we all of us want: A thriving and independent aggregation of 1,000,000s of points of light rather than 3 large tech giants feeding us content through algorithms.&lt;/p&gt;
&lt;h2 id=&#34;some-interesting-articles-i-ran-across-today&#34;&gt;Some interesting articles I ran across today&lt;/h2&gt;
&lt;p&gt;A couple of articles came my way today to nudge me to write this post.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://whereismytribe.net/back-to-rss&#34;&gt;Back to RSS&lt;/a&gt; by @wimt@whereismytribe.net starts with:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;It&amp;rsquo;s 2023. Or 1999&amp;hellip; Whatever. Personal sites are back. Blogs are back. RSS is back. Owning your data is becoming real&amp;hellip;&amp;rdquo;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Indeed. Owning your data, owning your privacy, and owning your legacy is increasingly interesting and relevant. Turns out RSS might be an important part here.&lt;/p&gt;
&lt;p&gt;Then there&amp;rsquo;s &lt;a href=&#34;https://www.theverge.com/23513418/bring-back-personal-blogging&#34;&gt;Bring back personal blogging&lt;/a&gt;: &lt;em&gt;Twitter is creaking. Social media seems less fun than ever. Maybe it’s time to get a little more personal&lt;/em&gt; from The Verge.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;In the beginning, there were blogs, and they were the original social web. We built community. We found our people. We wrote personally. We wrote frequently. We self-policed, and we linked to each other so that newbies could discover new and good blogs.&lt;/p&gt;
&lt;p&gt;I want to go back there. &amp;quot;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id=&#34;turns-out-i-do-a-lot-of-rss&#34;&gt;Turns out I &amp;ldquo;do&amp;rdquo; a lot of RSS&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;RSS is the foundation of podcasting&lt;/strong&gt;. Publishing a podcast really is just sending out an RSS feed that includes &lt;code&gt;&amp;lt;enclosure&amp;gt;&lt;/code&gt; tags with MP3 links in them.&lt;/p&gt;
&lt;p&gt;It&amp;rsquo;s probably no accident that podcasting is also one of these bastions of 1,000,000&amp;rsquo;s of small points of light being largely independent and open. Although, companies such as Spotify and others are &lt;a href=&#34;https://www.theguardian.com/technology/2019/feb/06/spotify-buys-podcast-firms-gimlet-and-anchor-streaming-profits-music&#34;&gt;trying their best to aggregate them&lt;/a&gt;. BTW, if it&amp;rsquo;s all the same to you, please consider listening on an independent player like Overcast or PocketCasts rather than one of these tracking aggregator platforms.&lt;/p&gt;
&lt;p&gt;Here are some fun stats from &lt;a href=&#34;https://talkpython.fm&#34;&gt;Talk Python&lt;/a&gt;&amp;rsquo;s and &lt;a href=&#34;https://pythonbytes.fm&#34;&gt;Python Bytes&lt;/a&gt;&amp;rsquo;s RSS feeds in December 2022.&lt;/p&gt;
&lt;table&gt;
  &lt;thead&gt;
      &lt;tr&gt;
          &lt;th&gt;HITS&lt;/th&gt;
          &lt;th&gt;VISITORS&lt;/th&gt;
          &lt;th&gt;TX. AMOUNT&lt;/th&gt;
          &lt;th&gt;URL&lt;/th&gt;
      &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
      &lt;tr&gt;
          &lt;td&gt;1,016,148&lt;/td&gt;
          &lt;td&gt;285,648&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;128.17 GB&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;a href=&#34;https://talkpython.fm/rss&#34;&gt;talkpython.fm/rss&lt;/a&gt;&lt;/td&gt;
      &lt;/tr&gt;
      &lt;tr&gt;
          &lt;td&gt;513,993&lt;/td&gt;
          &lt;td&gt;146,296&lt;/td&gt;
          &lt;td&gt;&lt;strong&gt;51.18 GB&lt;/strong&gt;&lt;/td&gt;
          &lt;td&gt;&lt;a href=&#34;https://pythonbytes.fm/rss&#34;&gt;pythonbytes.fm/rss&lt;/a&gt;&lt;/td&gt;
      &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That&amp;rsquo;s over 400,000 people pulling down &lt;strong&gt;0.2 TB of XML monthly&lt;/strong&gt;. I can only guess how insanely much XML that is when seen uncompressed given how dramatically text can be compressed.&lt;/p&gt;
&lt;p&gt;So yeah, RSS is pretty important to me.&lt;/p&gt;
&lt;h2 id=&#34;there-are-some-pretty-great-rss-readers&#34;&gt;There are some pretty great RSS readers&lt;/h2&gt;
&lt;p&gt;When I hear conversations about RSS, they quickly get to &amp;ldquo;then &lt;a href=&#34;https://www.wired.com/2013/06/why-google-reader-got-the-ax/&#34;&gt;Google killed Reader&lt;/a&gt;&amp;rdquo; and that was that. We were all done with RSS.&lt;/p&gt;
&lt;p&gt;But recently started using &lt;a href=&#34;https://reederapp.com&#34;&gt;Reeder for macOS and iOS&lt;/a&gt;. If you&amp;rsquo;re in the Apple ecosystem, the desktop and mobile apps work together like hand in glove and sync your feeds and history privately through your own iCloud account. It&amp;rsquo;s a paid app, but not a subscription and the price is fair.&lt;/p&gt;
&lt;p&gt;Remember that The Verge article? Know how I found it? RSS + Reeder:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.mkennedy.codes/posts/welcome-back-rss/21-reeder-article.jpg&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/welcome-back-rss/21-reeder-article.jpg&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Yes, Reeder is only for Apple people. I&amp;rsquo;m sure there are similar excellent ones for Windows and for Android. But I don&amp;rsquo;t have enough experience recommend any beyond a DuckDuckGo search for popular ones. Feel free to chime in below with recommendations.&lt;/p&gt;
&lt;p&gt;In fact, I&amp;rsquo;ve started to embrace reading sites that have an RSS feed even if they don&amp;rsquo;t ship the full article (like The Verge one above). And I&amp;rsquo;ve curtailed reading sites that don&amp;rsquo;t offer some form of RSS. After all, how much effort is an RSS feed if you&amp;rsquo;re already a news organization.&lt;/p&gt;
&lt;h2 id=&#34;while-youre-here&#34;&gt;While you&amp;rsquo;re here&amp;hellip;&lt;/h2&gt;
&lt;p&gt;If you are trying out a new RSS reader and looking for feeds, I have one for this site:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://mkennedy.codes/index.xml&#34;&gt;https://mkennedy.codes/index.xml&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Consider adding it to your reader along with the Talk Python and Python Bytes feeds listed above. :)&lt;/p&gt;
&lt;h2 id=&#34;if-you-are-a-developer&#34;&gt;If you are a developer&lt;/h2&gt;
&lt;p&gt;Then &lt;strong&gt;you have the power to shift the balance towards openness and independence with RSS&lt;/strong&gt; even if by just a tiny little bit. Add RSS to your things. It&amp;rsquo;s not particularly hard and you can use template language tools you&amp;rsquo;re likely familiar with. For example, Jinja2 for Python folks or Razor Pages for .NET.&lt;/p&gt;
&lt;p&gt;What can you put an RSS feed over at your org?&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re not a developer, just ask. Ask companies and websites that don&amp;rsquo;t offer some way to subscribe to their content to add an open protocol such as RSS.&lt;/p&gt;
&lt;h2 id=&#34;one-example-i-was-inspired-to-add&#34;&gt;One example I was inspired to add&lt;/h2&gt;
&lt;p&gt;If I&amp;rsquo;m giving advice, I&amp;rsquo;d better take it too, right?&lt;/p&gt;
&lt;p&gt;As I mentioned above, we are shipping an insane amount of RSS for the podcasts. But what else could I do?&lt;/p&gt;
&lt;p&gt;I decided that &lt;a href=&#34;https://training.talkpython.fm/courses/all&#34;&gt;our courses&lt;/a&gt; over at Talk Python Training might be a good candidate for &lt;em&gt;&lt;strong&gt;RSSification&lt;/strong&gt;&lt;/em&gt;. So I took an hour or two and put that into place. I present to you:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://training.talkpython.fm/courses/rss&#34;&gt;training.talkpython.fm/&lt;strong&gt;courses/rss&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Another area at Talk Python Training is our free office hours. We offer these to all our students as part of the benefit of learning with us. So we also now have an RSS feed for when those office hours are put on the calendar:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://training.talkpython.fm/office_hours/rss&#34;&gt;training.talkpython.fm/&lt;strong&gt;office_hours/rss&lt;/strong&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Now it&amp;rsquo;s your turn!&lt;/p&gt;
&lt;h2 id=&#34;discuss&#34;&gt;Discuss&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m not a fan of comments on my site. They lead to too many spammers and the platforms to plug them in usually add a bunch of tracking scripts too.&lt;/p&gt;
&lt;p&gt;So join the discussion over on my announcement for this article on Mastodon: &lt;a href=&#34;https://fosstodon.org/@mkennedy/109624369441214016&#34;&gt;fosstodon.org/@mkennedy/109624369441214016&lt;/a&gt;&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Paying for search in 2022, am I crazy?</title>
            <link>https://mkennedy.codes/posts/paying-for-search-in-2022-am-i-crazy/</link>
            <pubDate>Tue, 13 Dec 2022 00:00:00 +0000</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/paying-for-search-in-2022-am-i-crazy/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; After 6+ months using Kagi, I&amp;rsquo;m still paying $10/month for search. The results match Google, there are no ads or trackers, and you can permanently block SEO-spam sites. Worth it if you can afford it and value privacy.&lt;/p&gt;
&lt;p&gt;I have some core beliefs about the technology world as a software developer and netizien.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Open software and tools should be preferred&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Surveillance_capitalism&#34;&gt;Surveillance capitalism&lt;/a&gt; is net-net bad for society&lt;/li&gt;
&lt;li&gt;Tracking and retargeting on the Internet is morally wrong (it&amp;rsquo;s not just about ads, think about hidden levers like the &lt;a href=&#34;https://en.wikipedia.org/wiki/Facebook%E2%80%93Cambridge_Analytica_data_scandal&#34;&gt;Facebook–Cambridge Analytica data scandal&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;When people support small tech companies, we all win&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;search-at-the-intersection&#34;&gt;Search at the intersection&lt;/h2&gt;
&lt;p&gt;Many of those ideas come to a head when thinking about search and web browsers. Today, we&amp;rsquo;ll focus on search:&lt;/p&gt;
&lt;p&gt;Search engines are &lt;em&gt;often,&lt;/em&gt; but not always, ad supported. Some of those more on the &lt;a href=&#34;https://spreadprivacy.com/duckduckgo-revenue-model/&#34;&gt;neutral-good side of the spectrum&lt;/a&gt; like DuckDuckGo. Others, are &lt;strong&gt;fully on the dark side&lt;/strong&gt; [defunct link: https://ads.google.com/intl/en_uk/home/resources/retargeting-ads/].&lt;/p&gt;
&lt;p&gt;There are a bunch of smaller, independent, privacy-oriented search engines. Most of those also have ads but do their best to limit tracking exposure. The one I&amp;rsquo;ve been using lately is called &lt;a href=&#34;https://kagi.com&#34;&gt;Kagi&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/paying-for-search-in-2022-am-i-crazy/20-kagi-home.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Kagi is a paid search engine that costs around $10/month. Premium indeed.&lt;/p&gt;
&lt;h2 id=&#34;live-with-it-series&#34;&gt;Live with it series&lt;/h2&gt;
&lt;p&gt;On &lt;a href=&#34;https://pythonbytes.fm/episodes/show/289/textinator-is-coming-for-your-text-wherever-it-is&#34;&gt;the Python Bytes podcast&lt;/a&gt;, back in June 2022 I said I&amp;rsquo;d give Kagi a try and live with it for a month and then give a report. That report is overdue but here we have it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;I&amp;rsquo;m happy to say that I&amp;rsquo;m still using Kagi and really appreciate it.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Before Kagi I used DuckDuckGo and am a big fan. But Kagi &lt;a href=&#34;https://dkb.io/post/kagi-interview&#34;&gt;shares a lot of the same philosphy&lt;/a&gt; as I laid out above. And it has some pretty great features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;100% privacy-respecting&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No ads&lt;/strong&gt; (so no perverse incentives &lt;a href=&#34;https://www.msn.com/en-us/news/technology/duckduckgo-in-hot-water-over-hidden-tracking-agreement-with-microsoft/ar-AAXILR1&#34;&gt;like what DuckDuckGo got caught up in&lt;/a&gt;, I still 💕 you DuckDuckGo)&lt;/li&gt;
&lt;li&gt;Ability to &lt;strong&gt;permanently block crappy websites that win with SEO only&lt;/strong&gt;. How do you feel about w3schools? Yuck:&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/paying-for-search-in-2022-am-i-crazy/20-w3-schools.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Just block it right there in the search results with Kagi (there is an option button by each result on the page):&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/paying-for-search-in-2022-am-i-crazy/20-w3-schools-blocked.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;look-before-you-leap&#34;&gt;Look before you leap&lt;/h2&gt;
&lt;p&gt;Kagi also lets you know you&amp;rsquo;re about to enter a hellscape of surveillance capitalism. CNN is &lt;strong&gt;one of those evil companies who thinks there just aren&amp;rsquo;t enough trackers&lt;/strong&gt; and retargeting companies they can latch onto you.&lt;/p&gt;
&lt;p&gt;The search results have a red warning symbol next to them.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/paying-for-search-in-2022-am-i-crazy/20-cnn-red.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Let&amp;rsquo;s see why&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/paying-for-search-in-2022-am-i-crazy/20-cnn-trackers.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Oh, I see. There are 43 trackers on that page. 43 trackers! There is just no excuse for this.&lt;/p&gt;
&lt;p&gt;But with Kagi, you at least know what you&amp;rsquo;re headed into before you click. You also get nice web stats like SSL status, site popularity, and speed in that report.&lt;/p&gt;
&lt;h2 id=&#34;so-do-i-recommend-kagi&#34;&gt;So do I recommend Kagi?&lt;/h2&gt;
&lt;p&gt;Is switching to Kagi worth it? Three angles to consider:&lt;/p&gt;
&lt;p&gt;First, even with all the highfalutin privacy and customization, &lt;strong&gt;a search engine is only as good as its results&lt;/strong&gt;. I believe Kagi is actually buying search data from Google and Bing. The results seem basically the same as Google so, a solid checkmark here.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Second is the privacy&lt;/strong&gt;. Yes, this is absolutely worth it and they seem to be true their word. Privacy is very important to me. Maybe it&amp;rsquo;s worthwhile to you too. Then again, most people would rather be the product and recieve a service for free. I also pay $6/mo for email for the same reason.&lt;/p&gt;
&lt;p&gt;Third, &lt;strong&gt;is Kagi worth $10/month&lt;/strong&gt;? Oh this one is right on the border. More than once I&amp;rsquo;ve had my mouse cursor over the cancel account button. Am I crazy to pay $10/month for search, I ask myself?&lt;/p&gt;
&lt;p&gt;But every time I switch back to another search engine, I like it just a little less. I&amp;rsquo;m lucky. I can spend $10 extra a month on a service. I&amp;rsquo;m a software developer making a good income. I have &lt;a href=&#34;https://talkpython.fm&#34;&gt;my own business&lt;/a&gt; that is successful. So yes, it&amp;rsquo;s worth it. Your situation may be totally different and that&amp;rsquo;s fine. But if it is, maybe try DuckDuckGo over Google?&lt;/p&gt;
&lt;p&gt;This post is definitely not an ad and is in no way sponsored by Kagi. But if the principles I laid out here appeal, consider giving Kagi a try. Worst case, you&amp;rsquo;ll dodge a few ad trackers and support a small tech company for a month.&lt;/p&gt;
&lt;p&gt;Feel free to &lt;strong&gt;pick up the discussion on my Mastodon post&lt;/strong&gt; for this essay:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://fosstodon.org/@mkennedy/109507827811863800&#34;&gt;fosstodon.org/@mkennedy/109507827811863800&lt;/a&gt;&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Sometimes, You Should Build It Yourself</title>
            <link>https://mkennedy.codes/posts/sometimes-you-should-build-it-yourself/</link>
            <pubDate>Thu, 08 Dec 2022 00:00:00 +0000</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/sometimes-you-should-build-it-yourself/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Not Invented Here syndrome gets a bad rap, but sometimes building it yourself pays off. It did for Talk Python: better UX, custom integrations, real coding experience, and it&amp;rsquo;s fun. Just be selective about what you build.&lt;/p&gt;
&lt;p&gt;Developers are often itching to try out the latest shiny tech on problems they&amp;rsquo;ve been asked to solve, &lt;em&gt;even when there are perfectly good existing solutions they could adopt&lt;/em&gt; instead.&lt;/p&gt;
&lt;p&gt;Need some help desk software? Sure we could use Zendesk. But if we rewrote it in HTXM and FastAPI, we could do MUCH better and it&amp;rsquo;d be custom-built for our use case. Think of all the integrations.&lt;/p&gt;
&lt;p&gt;Adding silently: &lt;em&gt;&lt;a href=&#34;https://training.talkpython.fm/courses/htmx-flask-modern-python-web-apps-hold-the-javascript&#34;&gt;And I get to use HTMX&lt;/a&gt;&lt;/em&gt;!&lt;/p&gt;
&lt;p&gt;This is often called &lt;a href=&#34;https://en.wikipedia.org/wiki/Not_invented_here&#34;&gt;Not Invented Here syndrome&lt;/a&gt; and is strongly cautioned against:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;NIH&lt;/strong&gt;: The belief that in-house developments are inherently better suited, more secure, more controlled, quicker to develop, and incur lower overall cost (including maintenance cost) than using existing implementations.&lt;/p&gt;
&lt;p&gt;Just &lt;a href=&#34;https://pypi.org/search/?q=postgres&amp;amp;o=&#34;&gt;look at PyPI&lt;/a&gt; for packages to access PostgreSQL, there are &lt;strong&gt;2,348&lt;/strong&gt; results! Yet, there have to be multiple projects connecting Python and Postgres in development right now.&lt;/p&gt;
&lt;h2 id=&#34;but-sometimes-you-should-build-it-here&#34;&gt;But sometimes, you should build it here&lt;/h2&gt;
&lt;p&gt;I&amp;rsquo;m starting to think the advice to avoid NIH syndrome at all costs is &lt;strong&gt;not always good advice&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Looking back on the last 7 years at &lt;a href=&#34;https://talkpython.fm&#34;&gt;Talk Python&lt;/a&gt;, there have been plenty of times we could have slotted into someone else&amp;rsquo;s ecosystem but benefited from building parts of our tech ourselves. This is &lt;strong&gt;true both as a business and for me personally&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here are just some of the ways embracing a NIH tendency has benefited us.&lt;/p&gt;
&lt;h3 id=&#34;1-its-made-talk-python-a-better-service-for-everyone&#34;&gt;1. It&amp;rsquo;s made Talk Python a better service for everyone&lt;/h3&gt;
&lt;h4 id=&#34;very-polished-platforms-for-listeners-and-advertisers&#34;&gt;Very polished platforms for listeners and advertisers&lt;/h4&gt;
&lt;p&gt;Controlling our platform means we don&amp;rsquo;t have to slot into someone else&amp;rsquo;s poor design and slow web apps.&lt;/p&gt;
&lt;p&gt;Consider one of the more popular podcast hosts (think WordPress but for podcasters) and how &lt;em&gt;great&lt;/em&gt; of an impression you get from visiting their content.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Does this page below draw you in and entice you to listen&lt;/strong&gt;? I would think absolutely not.&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/sometimes-you-should-build-it-yourself/19-other-podcast.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;Contrast that with Talk Python&amp;rsquo;s episode pages:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/sometimes-you-should-build-it-yourself/19-us-podcast.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;You may not totally love the design. But it&amp;rsquo;s certainly something I can be proud of.&lt;/p&gt;
&lt;p&gt;The same goes for taking &lt;a href=&#34;https://training.talkpython.fm/courses/all&#34;&gt;courses at Talk Python&lt;/a&gt;. Here&amp;rsquo;s the clean and fast view of taking a course:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/sometimes-you-should-build-it-yourself/19-courses.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;We could have decided to publish our courses on other platforms like LinkedIn Learning. In fact, we have tried a few syndication deals. They were basically failures from a business perspective. So, I&amp;rsquo;m happy we built our course platform ourselves.&lt;/p&gt;
&lt;h4 id=&#34;integrations-abound&#34;&gt;Integrations abound&lt;/h4&gt;
&lt;p&gt;Do you have amazing ideas of leverging some aspect of your org for another? Taking someone else&amp;rsquo;s product/platform might mean that&amp;rsquo;s not an option.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s an example.&lt;/p&gt;
&lt;p&gt;We stream all our podcast events with a &lt;a href=&#34;https://www.youtube.com/@talkpython&#34;&gt;live audience on YouTube&lt;/a&gt;. That&amp;rsquo;s lots of fun and a cool experience for our listeners:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/sometimes-you-should-build-it-yourself/19-live-youtube.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;But because we control the platform, we can push a single button on our &lt;a href=&#34;https://www.elgato.com/en/stream-deck&#34;&gt;Stream Deck&lt;/a&gt;, that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Announces the live stream on Twitter&lt;/li&gt;
&lt;li&gt;Announces the live stream on Mastodon&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Puts the website into live stream&lt;/strong&gt; mode encouraging people to be part of the audience&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/sometimes-you-should-build-it-yourself/19-live-site.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;But it goes further. Once the live stream is done, &lt;strong&gt;the platform takes the thumbnail we&amp;rsquo;ve carefully crafted for our YouTube video and make visible on the episode page&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;After that, &lt;strong&gt;the website automatically pulls that image and uses it for our social share&lt;/strong&gt; in Twitter, Mastodon, and others:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/sometimes-you-should-build-it-yourself/19-twitter-share.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;That&amp;rsquo;s &lt;strong&gt;Stream Deck &amp;gt; YouTube &amp;gt; talkpython.fm Live Mode &amp;gt; talkpython.fm episode page &amp;gt; social share&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;All of that is fully automated. How is this possible? We built it ourselves.&lt;/p&gt;
&lt;h4 id=&#34;advertiser-dashboards&#34;&gt;Advertiser dashboards&lt;/h4&gt;
&lt;p&gt;The other side of the podcast story wins too. Podcast advertisers get a full dashboard with per second reporting of traffic to their sponsored episodes and the number of visitors to their sponsored links.&lt;/p&gt;
&lt;p&gt;We can do this with &lt;strong&gt;zero tracking&lt;/strong&gt; forced upon our listeners, unlike most platforms.&lt;/p&gt;
&lt;h3 id=&#34;2-i-gain-experience-writing-real-code&#34;&gt;2. I gain experience writing real code&lt;/h3&gt;
&lt;p&gt;As someone who talks a lot about writing code and teaches a lot about writing code, it&amp;rsquo;s important to remain connected to &lt;em&gt;actually&lt;/em&gt; writing code. Creating our platforms means I get daily experience working on high-end Python software in production.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We do around 20 TB and millions of requests of traffic monthly&lt;/li&gt;
&lt;li&gt;I evolve the software from older code to newer code (like MongoEngine to Beanie &amp;amp; Pydantic or integrating FastAPI)&lt;/li&gt;
&lt;li&gt;I manage this platform on 8 coordinating servers as well as &lt;a href=&#34;https://bunny.net?ref=b4f3tqcyae&#34;&gt;across CDNs&lt;/a&gt; and integrating with other platforms (like iTunes catalogs)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This means when I sit down to speak with guests or write a course, I often have very real world experience to draw from. Because I built it myself.&lt;/p&gt;
&lt;h3 id=&#34;3-it-is-a-lot-of-fun&#34;&gt;3. It is a lot of fun&lt;/h3&gt;
&lt;p&gt;Finally, writing this code and growing as a developer is just tons of fun.&lt;/p&gt;
&lt;h3 id=&#34;4-but-also-a-counterpoint&#34;&gt;4. But also a counterpoint&lt;/h3&gt;
&lt;p&gt;That doesn&amp;rsquo;t mean I don&amp;rsquo;t sometimes choose to NOT build it ourselves.&lt;/p&gt;
&lt;p&gt;Our credit card sales are handled by &lt;a href=&#34;https://www.paddle.com&#34;&gt;Paddle&lt;/a&gt;. This site is creating using the static site generator &lt;a href=&#34;https://gohugo.io&#34;&gt;Hugo&lt;/a&gt;. Neither of these was built here and I&amp;rsquo;m very happy to not work on them.&lt;/p&gt;
&lt;p&gt;So &lt;strong&gt;when it makes sense consider building it yourself&lt;/strong&gt; but just not for everything.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Properly Factor Your Jinja HTML Code with Jinja Partials</title>
            <link>https://mkennedy.codes/posts/properly-factor-your-jinja-html-code-with-jinja-partials/</link>
            <pubDate>Sat, 03 Dec 2022 00:00:00 +0000</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/properly-factor-your-jinja-html-code-with-jinja-partials/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Use the &lt;a href=&#34;https://github.com/mikeckennedy/jinja_partials&#34;&gt;jinja-partials&lt;/a&gt; package to add function-like reusability to your Jinja templates. It lets you call named template fragments with parameters, avoiding the 600-line template problem.&lt;/p&gt;
&lt;p&gt;If you work with Python code, then you&amp;rsquo;ve surely seen some bad code. Martin Fowler popularized the idea of &lt;a href=&#34;https://blog.codinghorror.com/code-smells/&#34;&gt;code smells&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;ldquo;Code smells are certain structures in the code that indicate violation of fundamental design principles and negatively impact design quality.&amp;rdquo; [&lt;a href=&#34;https://en.wikipedia.org/wiki/Code_smell&#34;&gt;wk&lt;/a&gt;]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This may have been your bad code. More likely, if you&amp;rsquo;re taking the time to read about code styles, it was someone else&amp;rsquo;s.&lt;/p&gt;
&lt;p&gt;Either way, one of the biggest offenders is the long method.&lt;/p&gt;
&lt;p&gt;You know, that &lt;strong&gt;single Python function that is&lt;/strong&gt;&lt;/p&gt;
&lt;h3 id=&#34;627&#34;&gt;627&lt;/h3&gt;
&lt;h4 id=&#34;lines&#34;&gt;lines&lt;/h4&gt;
&lt;h5 id=&#34;long&#34;&gt;long.&lt;/h5&gt;
&lt;p&gt;Really? Yes, apparently. We see this and immediately know it&amp;rsquo;s bad. Usually this has some some &lt;em&gt;foul code duplication&lt;/em&gt; mixed in for good measure.&lt;/p&gt;
&lt;p&gt;If you were responsible for this code, you&amp;rsquo;d fix it straight away.&lt;/p&gt;
&lt;h2 id=&#34;but-we-do-this-every-day-on-the-web&#34;&gt;But we do this every day on the web&lt;/h2&gt;
&lt;p&gt;While outrageous in code, we wouldn&amp;rsquo;t bat an eye if we had an HTML template (think Jinja2 from Flask/FastAPI, Django template from Django, or Chameleon from Pyramid) that was over 600 lines of HTML.&lt;/p&gt;
&lt;p&gt;There usually is no mechanism of a &lt;em&gt;function call&lt;/em&gt; within HTML templates for Python languages.&lt;/p&gt;
&lt;p&gt;Or is there?&lt;/p&gt;
&lt;p&gt;When I &lt;a href=&#34;https://training.talkpython.fm/courses/htmx-flask-modern-python-web-apps-hold-the-javascript&#34;&gt;started working with HTMX&lt;/a&gt; (incredible framework!), this problem came to a head in my Python apps. So I created the &lt;a href=&#34;https://github.com/mikeckennedy/jinja_partials&#34;&gt;Jinja Partials package&lt;/a&gt;. There is also a &lt;a href=&#34;https://github.com/mikeckennedy/chameleon_partials&#34;&gt;Pyramid/Chameleon version&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Jinja Partials&lt;/strong&gt; adds the concept of a function call with a named set of template code and a clear variables / parameters passed to that template. The &lt;a href=&#34;https://github.com/mikeckennedy/jinja_partials&#34;&gt;repo&lt;/a&gt; has some examples.&lt;/p&gt;
&lt;p&gt;I just used this concept to surface more options for subscribing to &lt;a href=&#34;https://talkpython.fm&#34;&gt;Talk Python&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;See the choices of podcast players in the &lt;a href=&#34;https://talkpython.fm/episodes/all&#34;&gt;list of episodes page&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/properly-factor-your-jinja-html-code-with-jinja-partials/jinja-list-page.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;And on the &lt;a href=&#34;https://talkpython.fm/episodes/show/392/data-science-from-the-command-line&#34;&gt;episode details page&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/properly-factor-your-jinja-html-code-with-jinja-partials/jinja-episode-page.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;As well as &lt;a href=&#34;https://talkpython.fm/subscribe-options&#34;&gt;the new subscribe page&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/properly-factor-your-jinja-html-code-with-jinja-partials/jinja-subscribe-page.jpg&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;That list of podcast players, style, and basic analytics (click counting) is all handled with just this HTML fragement:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;p&#34;&gt;{{&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;render_partial&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;s1&#34;&gt;&amp;#39;episodes/partials/subscribe-in-players.pt&amp;#39;&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                 &lt;span class=&#34;n&#34;&gt;is_listing&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;kc&#34;&gt;False&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; 
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;                 &lt;span class=&#34;n&#34;&gt;cdn_prefix&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;=&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;view&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;cdn_prefix&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt; &lt;span class=&#34;p&#34;&gt;}}&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If this idea of a function inside HTML appeals to you, then check out &lt;a href=&#34;https://github.com/mikeckennedy/jinja_partials&#34;&gt;jinja-partials&lt;/a&gt; or &lt;a href=&#34;https://github.com/mikeckennedy/chameleon_partials&#34;&gt;chameleon_partials&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Finally, if you&amp;rsquo;re one of the few people saying &amp;ldquo;Jinja already has &lt;code&gt;include&lt;/code&gt;!!!&amp;rdquo; Yes, it does. It&amp;rsquo;s not the same. See &lt;a href=&#34;https://github.com/mikeckennedy/jinja_partials/issues/1&#34;&gt;the whole discussion here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What about Jinja&amp;rsquo;s include?&lt;/strong&gt; Jinja&amp;rsquo;s include is analogous to C++ precompiler macros without passing parameters. Jinja Partials is like functions with parameters and local variable scope. They can both solve the same problem some of the time, but they are definitely not the same.&lt;/p&gt;
&lt;p&gt;Hope you find this idea of cleaning up your HTML templates with functions appealing!&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Black Friday: A Lesson in Python Performance</title>
            <link>https://mkennedy.codes/posts/black-friday-almost-melted-servers/</link>
            <pubDate>Thu, 24 Nov 2022 00:00:00 +0000</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/black-friday-almost-melted-servers/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; When Black Friday traffic nearly melted our servers, the bottleneck wasn&amp;rsquo;t Python. It was nginx handling SSL and static files. Adding a CDN (Bunny.net) dropped CPU from 85% to 7.5% for $2.&lt;/p&gt;
&lt;p&gt;On Wednesday, we &lt;a href=&#34;https://talkpython.fm/black-friday&#34;&gt;launched our Black Friday Sale&lt;/a&gt; over at Talk Python Training.&lt;/p&gt;
&lt;p&gt;If you know me, you know that I&amp;rsquo;d much rather just offer a fair price year-round (which I think we do) and avoid hyping these sales. But many many people asked for it. So we&amp;rsquo;ve done this the past few years and it seems to have been very appreciated by our users.&lt;/p&gt;
&lt;p&gt;Generally, our infrastructure has handled Black Friday fine. Yes, it&amp;rsquo;s busy on the first day for sure, but not &lt;em&gt;&lt;strong&gt;melt-your-servers&lt;/strong&gt;&lt;/em&gt; busy.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Until this year&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;After sending out the announcement, I fired up &lt;a href=&#34;https://nicolargo.github.io/glances/&#34;&gt;glances&lt;/a&gt; on our web server just to see how it was going.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The server was at 85% CPU usage &amp;hellip; and going up.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;88%, 91%, 92%, &amp;hellip; Uh oh.&lt;/p&gt;
&lt;p&gt;It wasn&amp;rsquo;t going to be able to handle much more. But &lt;strong&gt;there was a surprise&lt;/strong&gt;: It wasn&amp;rsquo;t the 8 uWSGI Python worker processes getting pounded. It was &lt;a href=&#34;https://nginx.org/en/&#34;&gt;nginx&lt;/a&gt;. I wish I took a screenshot but here&amp;rsquo;s what you might have seen:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;Process            &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt;  CPU Usage
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;----------------------------------------------------------------
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;nginx worker &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;     &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; *************************************
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;nginx worker &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt;     &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; ****************************************
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;uWSGI worker &lt;span class=&#34;m&#34;&gt;1&lt;/span&gt;     &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; **
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;uWSGI worker &lt;span class=&#34;m&#34;&gt;2&lt;/span&gt;     &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; ****
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;uWSGI worker &lt;span class=&#34;m&#34;&gt;3&lt;/span&gt;     &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; *
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;uWSGI worker &lt;span class=&#34;m&#34;&gt;4&lt;/span&gt;     &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; **
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;MongoDB            &lt;span class=&#34;p&#34;&gt;|&lt;/span&gt; **** &lt;span class=&#34;o&#34;&gt;(&lt;/span&gt;on dedicated server&lt;span class=&#34;o&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;This was so surprising to me. In case you&amp;rsquo;re unfamiliar with this deployment topology, here&amp;rsquo;s what each part is doing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;nginx (static content)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;Direct web browser connection&lt;/li&gt;
&lt;li&gt;Terminating SSL&lt;/li&gt;
&lt;li&gt;Sending HTML/CSS/Image content to clients&lt;/li&gt;
&lt;li&gt;gzipping relevant content (HTML, CSS, &amp;hellip;)&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;uWSGI (dynamic Python behaviors)&lt;/strong&gt;
&lt;ul&gt;
&lt;li&gt;The &amp;ldquo;app&amp;rdquo; of our Python app&lt;/li&gt;
&lt;li&gt;Running all our Python code&lt;/li&gt;
&lt;li&gt;Talking to MongoDB for DB queries and updates&lt;/li&gt;
&lt;li&gt;Queueing up emails&lt;/li&gt;
&lt;li&gt;Basically everything else&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The surprise is that Python handled Black Friday &lt;em&gt;perfectly&lt;/em&gt;. It was mostly idling along happy to serve up the requests in the &amp;ldquo;complicated&amp;rdquo; data-driven part of our app.&lt;/p&gt;
&lt;p&gt;It was nginx just trying to do SSL, gzip, and the rest of static things that was killing the system. And nginx is considered &lt;em&gt;fast&lt;/em&gt;! See &lt;a href=&#34;https://serverfault.com/questions/86674/why-is-nginx-so-fast&#34;&gt;Why is Nginx so fast?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;It is worth pointing out that I have endlessly optimized the Python &amp;lt;&amp;ndash;&amp;gt; MongoDB interaction and we have honed the DB indexes to a sharp point. But I still expected that area to be the point of contention.&lt;/p&gt;
&lt;p&gt;So next time you get into that debate about why you can&amp;rsquo;t use Python because it&amp;rsquo;s not compiled and it has that GIL thing and I think we should use NodeJS or whatever, feel free to refer back to here.&lt;/p&gt;
&lt;p&gt;And to wrap things up, thank you to everyone who cared enough to &lt;a href=&#34;https://training.talkpython.fm/courses/all&#34;&gt;check out our courses&lt;/a&gt; even if they almost killed the server! ;)&lt;/p&gt;
&lt;h2 id=&#34;why-nginx-was-the-real-bottleneck&#34;&gt;Why nginx was the real bottleneck&lt;/h2&gt;
&lt;p&gt;After posting this article and a few conversations on social media, I think I know the cause (but it doesn&amp;rsquo;t change my surprise and general point of view).&lt;/p&gt;
&lt;p&gt;Looking at the most commonly requested Black Friday page, it turns out there are a lot of requests we could offline (with added complexity of course).&lt;/p&gt;
&lt;p&gt;On the page &lt;a href=&#34;https://training.talkpython.fm/courses/all&#34;&gt;training.talkpython.fm/courses/all&lt;/a&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;1 HTML response (via Python)&lt;/li&gt;
&lt;li&gt;12 CSS requests (many bundled, but some need to be separate like Font Awesome)&lt;/li&gt;
&lt;li&gt;43 image requests&lt;/li&gt;
&lt;li&gt;1 JavaScript request&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The total size of the request is around 7MB with the HTML only 7.5kB. At the time of this Black Friday onslaught, we were not using a CDN because the content is served plenty fast and juggling dev vs. CDN URLs in production is a pain.&lt;/p&gt;
&lt;p&gt;But this whole experience opened my eyes that we could push 56 / 57 requests to a CDN and drastically reduce the amount of content (hence load) we&amp;rsquo;re putting on nginx.&lt;/p&gt;
&lt;p&gt;I chose &lt;strong&gt;&lt;a href=&#34;https://bunny.net?ref=b4f3tqcyae&#34;&gt;Bunny.net CDN&lt;/a&gt;&lt;/strong&gt;. After just 30 minutes, you can see the load that was put upon our nginx process, now shared across the world (and content closer to the users):&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://bunny.net?ref=b4f3tqcyae&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/black-friday-almost-melted-servers/cdn-distribution.jpg&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So far, &lt;a href=&#34;https://bunny.net?ref=b4f3tqcyae&#34;&gt;Bunny&lt;/a&gt; is fantastic software. Only took about 15 minutes to set up the CDN, a custom domain, and SSL. Then there was the hour of changing all the CSS, JS, and IMG links across &lt;a href=&#34;https://training.talkpython.fm&#34;&gt;Talk Python Training&lt;/a&gt; :).&lt;/p&gt;
&lt;p&gt;At $0.01 / GB, gaining access to this much distributed infrastructure seems like a great tradeoff. As of this writing, we just passed our first GB of content over the CDN. That&amp;rsquo;s a penny well-spent.&lt;/p&gt;
&lt;h2 id=&#34;another-update-and-cyber-monday&#34;&gt;Another update and Cyber Monday&lt;/h2&gt;
&lt;p&gt;The fun thing about Black Friday sales is they are generally bookended by Cyber Monday.&lt;/p&gt;
&lt;p&gt;Before I said I wish I had taken a screenshot when we sent out the announcement for the opening of Black Friday. It turns out there is roughly the same amount of traffic and interest in the closing day of our sale and this time I did take a screenshot.&lt;/p&gt;
&lt;p&gt;With our &lt;a href=&#34;https://bunny.net?ref=b4f3tqcyae&#34;&gt;Bunny.net CDN&lt;/a&gt; setup fully in place, things were quite different and our infrastructure was busy but happy. I now see why our nginx server was hurting so much.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s the realtime traffic on the CDN:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.mkennedy.codes/posts/black-friday-almost-melted-servers/cdn-speed-metrics.png&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/black-friday-almost-melted-servers/cdn-speed-metrics.png&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;1.4 Gb/sec&lt;/strong&gt; of static files. Glad we moved those requests the edge.&lt;/p&gt;
&lt;p&gt;And here is our app server at the exact same moment, the one that nearly melted. Note: Cache misses are generally pulled from other CDN nodes, not this server.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.mkennedy.codes/posts/black-friday-almost-melted-servers/glances-cyber-monday.png&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/black-friday-almost-melted-servers/glances-cyber-monday.png&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;7.5% CPU usage total and &lt;strong&gt;only 3% across both nginx workers&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Finally, we all know that with enough complexity and money, you can solve almost any scaling problem. But this was not expensive:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.mkennedy.codes/posts/black-friday-almost-melted-servers/cdn-costs.png&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/black-friday-almost-melted-servers/cdn-costs.png&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;I switched most our traffic over the past 2 days. So far we&amp;rsquo;ve spent $2 in total.&lt;/p&gt;
&lt;p&gt;I hope my sharing our experience here has given you something concrete to consider when scaling your Python apps (or any web app really) for relatively small teams and deployments.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Python 3.11 in 100 Seconds</title>
            <link>https://mkennedy.codes/posts/python-311-in-100-seconds/</link>
            <pubDate>Sun, 20 Nov 2022 00:00:00 +0000</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/python-311-in-100-seconds/</guid>
            <description>&lt;p&gt;Python 3.11 is a massive release. The release notes doc is over 175,000 words. That&amp;rsquo;s more than double the size of a typical novel or nonfiction book!&lt;/p&gt;
&lt;p&gt;Most important is its speed. Python 3.11 is between 10% and 60% faster than even 3.10 for typical applications. But there is a lot more to 3.11 than just speed. If you have 100 seconds to spare, &lt;strong&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=8NQGZA1wWiQ&#34;&gt;watch my latest video&lt;/a&gt;&lt;/strong&gt;, Python 3.11 in 100 Seconds:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=8NQGZA1wWiQ&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-311-in-100-seconds/python-311-quickly.jpg&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=8NQGZA1wWiQ&#34;&gt;&lt;em&gt;Watch on YouTube&lt;/em&gt;&lt;/a&gt;&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Mastodon First: My New Social Attitude </title>
            <link>https://mkennedy.codes/posts/mastodon-first-my-new-social-attitude/</link>
            <pubDate>Sat, 19 Nov 2022 00:00:00 +0000</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/mastodon-first-my-new-social-attitude/</guid>
            <description>&lt;p&gt;Without a doubt, &lt;strong&gt;my most beloved social media platform has been Twitter&lt;/strong&gt;. I joined over 14 years ago. And across &lt;a href=&#34;https://x.com/mkennedy/&#34;&gt;my personal account&lt;/a&gt; and the podcasts&amp;rsquo; accounts (&lt;a href=&#34;https://x.com/TalkPython&#34;&gt;1&lt;/a&gt;, &lt;a href=&#34;https://x.com/PythonBytes&#34;&gt;2&lt;/a&gt;) we have built an audience of over 100,000 members. It&amp;rsquo;s an incredible honor.&lt;/p&gt;
&lt;p&gt;But with more than a little sarcasm, sites like &lt;strong&gt;Twitter is Going Great! &amp;hellip; and definitely does not develop features primarily to stroke Elon Musk&amp;rsquo;s delicate ego&lt;/strong&gt; [defunct link: https://twitterisgoinggreat.com] have been documenting the wild and downward spiral Twitter has been on.&lt;/p&gt;
&lt;p&gt;So as you can tell from my other posts&amp;rsquo; titles, I&amp;rsquo;ve &lt;a href=&#34;https://fosstodon.org/@mkennedy&#34;&gt;moved to Mastodon&lt;/a&gt; (&lt;a href=&#34;https://fosstodon.org&#34;&gt;Fosstodon.org&lt;/a&gt; is my instance - aka home base).&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;My current attitude is &lt;em&gt;Mastodon First&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Let me elaborate. Now that I&amp;rsquo;ve spent some time over there, I see how closely Mastodon and open source philosophies align. This is not just because &lt;a href=&#34;https://github.com/mastodon/mastodon&#34;&gt;Mastodon is open source&lt;/a&gt;. Rather, the whole idea of controlled and owned by the community via federated instances resonates much better than big tech powered by &lt;a href=&#34;https://en.wikipedia.org/wiki/Surveillance_capitalism&#34;&gt;surveillance capitalism&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I will be most active over on Mastodon. But I don&amp;rsquo;t intend to abandon my beloved community on Twitter or other networks (unless Twitter goes so great as to crash into the ground).&lt;/p&gt;
&lt;p&gt;I encourage you to follow me over on Mastodon via &lt;strong&gt;&lt;a href=&#34;https://fosstodon.org/@mkennedy&#34;&gt;@mkennedy@fosstodon.org&lt;/a&gt;&lt;/strong&gt;. And if you&amp;rsquo;re new to Mastodon or just still holding out, I did an excellent panel discussion with many Python people entitled &lt;a href=&#34;https://talkpython.fm/episodes/show/390/mastodon-for-python-devs&#34;&gt;Mastodon for Python Devs&lt;/a&gt; over on Talk Python.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Installing Mastodon as a Progressive Web App (PWA)</title>
            <link>https://mkennedy.codes/posts/installing-mastodon-as-a-progressive-web-app-pwa/</link>
            <pubDate>Fri, 18 Nov 2022 00:00:00 +0000</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/installing-mastodon-as-a-progressive-web-app-pwa/</guid>
            <description>&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Open your Mastodon instance (e.g., fosstodon.org) in a Chrome-based browser, right-click, and select &amp;ldquo;Install app.&amp;rdquo; It works as a standalone desktop or mobile app with instant access to new features.&lt;/p&gt;
&lt;p&gt;When Mastodon 4.0 came out, the official iOS and Android apps were still 1.5 months old (it has since been updated). Exciting new features were added such as editing of posts. And we had to wait until the new version of that mobile app shipped separately to take advantage of them.&lt;/p&gt;
&lt;p&gt;The web app had the new features instantly (and seems like it always will). Moreover, the mobile app isn&amp;rsquo;t a desktop app, is it?&lt;/p&gt;
&lt;h2 id=&#34;how-to-install-mastodon-as-a-desktop-app&#34;&gt;How to Install Mastodon as a Desktop App&lt;/h2&gt;
&lt;p&gt;Luckily the Mastodon folks have done the hard work to make the web app (that&amp;rsquo;s &lt;em&gt;yourinstance&lt;/em&gt;.org, e.g. &lt;a href=&#34;https://fosstodon.org&#34;&gt;fosstodon.org&lt;/a&gt;) a Progressive Web App that can be installed with a single right-click and runs as a perfect app on your machine:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/installing-mastodon-as-a-progressive-web-app-pwa/mastodon-pwa.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;p&gt;This works for any Chrome-based browser (Chrome, Vivaldi, Brave, and others). Once you install it, you can add Mastodon to your dock/task bar and it is effectively a stand-alone app:&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/installing-mastodon-as-a-progressive-web-app-pwa/mastodon-as-pwa.png&#34; alt=&#34;&#34;&gt;&lt;/p&gt;
&lt;h2 id=&#34;how-to-install-mastodon-as-a-mobile-app&#34;&gt;How to Install Mastodon as a Mobile App&lt;/h2&gt;
&lt;p&gt;This works equally well on both iOS (iPad preferably) and Android tablets. I even made &lt;a href=&#34;https://www.youtube.com/watch?v=oNT2Sa_0YJU&#34;&gt;a YouTube video&lt;/a&gt; showing how this works.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re searching for a great Mastodon app, it might be hiding in your browser!&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>20% Faster Python with a Single GC Tweak</title>
            <link>https://mkennedy.codes/posts/python-gc-settings-change-this-and-make-your-app-go-20pc-faster/</link>
            <pubDate>Thu, 17 Nov 2022 00:00:00 +0000</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/python-gc-settings-change-this-and-make-your-app-go-20pc-faster/</guid>
            <description>&lt;p&gt;[Update Nov 23, 2022: Added perf graphs at the end of this article]&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;TL;DR:&lt;/strong&gt; Increase Python&amp;rsquo;s GC allocation threshold from 700 to 50,000 at app startup with &lt;code&gt;gc.set_threshold(50_000, ...)&lt;/code&gt; and use &lt;code&gt;gc.freeze()&lt;/code&gt; to exclude startup objects. We saw 20% faster responses at Talk Python with no increase in memory usage.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a &lt;strong&gt;bold statement&lt;/strong&gt; given that I know nothing about you or your app:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Your Python GC settings are wrong and they are hurting your performance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Let me elaborate. Python has two types of memory management:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Primarily &lt;strong&gt;reference counting&lt;/strong&gt;. Objects keep track of the number of variables pointing at them. If that number reaches 0, they are &lt;em&gt;instantly&lt;/em&gt; deleted, deterministically. Without stats to back this, I&amp;rsquo;d guess 99.9%+ of all memory is handled this way.&lt;/li&gt;
&lt;li&gt;The 0.1%: The one case that fails hard here is if you have a cycle (think Person object which has a spouse field). Enter &lt;strong&gt;garbage collection&lt;/strong&gt;. This runs occasionally looking for missed ref count clean-ups.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Ref counting is great but the GC, it runs too often.&lt;/p&gt;
&lt;p&gt;The trigger is when you allocate 700 or more container objects (classes, dicts, tuples, lists, etc) more than have been cleaned up, a GC cycle runs.&lt;/p&gt;
&lt;p&gt;Imaging you&amp;rsquo;re doing a query:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;recent&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;k&#34;&gt;await&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;PageHits&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;objects&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;filter&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;date&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;&amp;gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;today&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;to_list&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;If there are anywhere near 700 results in that query, you&amp;rsquo;re hitting GC cycles before you even get the full list back.&lt;/p&gt;
&lt;p&gt;Our sitemap at &lt;a href=&#34;https://training.talkpython.fm/&#34;&gt;Talk Python Training&lt;/a&gt; was resulting in 77 GC cycles to just load the page, 77!&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://training.talkpython.fm/sitemap.xml&#34;&gt;training.talkpython.fm/sitemap.xml&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Yikes.&lt;/p&gt;
&lt;p&gt;But &lt;strong&gt;you can change it&lt;/strong&gt;. Try this code at app startup:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre tabindex=&#34;0&#34; class=&#34;chroma&#34;&gt;&lt;code class=&#34;language-python&#34; data-lang=&#34;python&#34;&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Clean up what might be garbage so far.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;gc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;collect&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;c1&#34;&gt;# Exclude current items from future GC.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;gc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;freeze&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;allocs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;gen1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;gen2&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;gc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;get_threshold&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;()&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;allocs&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;50_000&lt;/span&gt;  &lt;span class=&#34;c1&#34;&gt;# Start the GC sequence every 50K not 700 allocations.&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;gen1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;gen1&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;gen2&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;=&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;gen2&lt;/span&gt; &lt;span class=&#34;o&#34;&gt;*&lt;/span&gt; &lt;span class=&#34;mi&#34;&gt;2&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;span class=&#34;line&#34;&gt;&lt;span class=&#34;cl&#34;&gt;&lt;span class=&#34;n&#34;&gt;gc&lt;/span&gt;&lt;span class=&#34;o&#34;&gt;.&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;set_threshold&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;(&lt;/span&gt;&lt;span class=&#34;n&#34;&gt;allocs&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;gen1&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;,&lt;/span&gt; &lt;span class=&#34;n&#34;&gt;gen2&lt;/span&gt;&lt;span class=&#34;p&#34;&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;p&gt;Of course, your mileage will vary. But at Talk Python, we saw a 20% overall speed up with no change in the memory usage.&lt;/p&gt;
&lt;p&gt;If you want to dive much deeper into this, check out &lt;strong&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=p4Sn6UcFTOU&#34;&gt;my recent video&lt;/a&gt;&lt;/strong&gt; showing this live where we got 2x perf with just this code for a simpler example:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.youtube.com/watch?v=p4Sn6UcFTOU&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-gc-settings-change-this-and-make-your-app-go-20pc-faster/thumb-gc-tweaks.jpg&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;dive-in-with-my-full-course&#34;&gt;Dive in with my Full Course&lt;/h2&gt;
&lt;p&gt;If this all sounds interesting and you want some practical, hands-on experience and a solid look behind the scenes about how Python memory works and what knobs you have to control it, check out my full course over at Talk Python Training: &lt;a href=&#34;https://training.talkpython.fm/courses/python-memory-management-and-tips&#34;&gt;&lt;strong&gt;Python Memory Management and Tips Course&lt;/strong&gt;&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&#34;real-world-performance-results&#34;&gt;Real-World Performance Results&lt;/h2&gt;
&lt;p&gt;There has been a bit of debate on how realistically this improvement is for &lt;em&gt;real&lt;/em&gt; workloads. I can&amp;rsquo;t test yours, but here are two production end-to-end requests from Talk Python Training.&lt;/p&gt;
&lt;p&gt;I used &lt;a href=&#34;https://locust.io&#34;&gt;locust&lt;/a&gt; to run some performance tests against two URLs on our site:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://training.talkpython.fm/sitemap.xml&#34;&gt;Sitemap&lt;/a&gt; (the &lt;strong&gt;most&lt;/strong&gt; expensive page on our site)&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://training.talkpython.fm/search/all/flask&#34;&gt;Search&lt;/a&gt; for a super common term&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These both showed clear &amp;gt; 20% improvements:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Search&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.mkennedy.codes/posts/python-gc-settings-change-this-and-make-your-app-go-20pc-faster/tp-search-gc-tweaks.png&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-gc-settings-change-this-and-make-your-app-go-20pc-faster/tp-search-gc-tweaks.png&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sitemap&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://cdn.mkennedy.codes/posts/python-gc-settings-change-this-and-make-your-app-go-20pc-faster/tp-sitemap-gc-tweaks.png&#34;&gt;&lt;img src=&#34;https://cdn.mkennedy.codes/posts/python-gc-settings-change-this-and-make-your-app-go-20pc-faster/tp-sitemap-gc-tweaks.png&#34; alt=&#34;&#34;&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;discussion&#34;&gt;Discussion&lt;/h2&gt;
&lt;p&gt;This post got a decent amount of discussion. You can see it over on &lt;a href=&#34;https://fosstodon.org/@mkennedy/109390374368256104&#34;&gt;the Mastodon thread&lt;/a&gt;.&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Hassling Spammers</title>
            <link>https://mkennedy.codes/posts/hassling-spammers/</link>
            <pubDate>Wed, 16 Nov 2022 00:00:00 +0000</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/hassling-spammers/</guid>
            <description>&lt;p&gt;Running a set of popular websites (e.g. &lt;a href=&#34;https://talkpython.fm&#34;&gt;talkpython.fm&lt;/a&gt;) means I get a lot of unsolicited emails. Some are genuine and very welcome. But others, clearly spam. One day, I decided rather than just deleting them, let&amp;rsquo;s just make them spin their wheels a bit and return a little frustration.&lt;/p&gt;
&lt;p&gt;I get daily requests to &amp;ldquo;guest post&amp;rdquo; on our corporate blog. Note that &lt;strong&gt;we do not have a company blog&lt;/strong&gt; at all.&lt;/p&gt;
&lt;p&gt;Here&amp;rsquo;s a real back and forth I had last week. It was beautiful.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Spammer&lt;/strong&gt;: I have just gone through your website and read some of the blogs; they are outstanding. I can get working on the article to send it over&amp;hellip;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Me&lt;/strong&gt;: Oh, you read our blog? What is the &lt;strong&gt;URL&lt;/strong&gt; of it?&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Spammer&lt;/strong&gt;: Excellent. The URL is &lt;a href=&#34;https://training.talkpython.fm/courses/up-and-running-with-git-a-pragmatic-ui-based-introduction&#34;&gt;https://training.talkpython.fm/courses/up-an&amp;hellip;&lt;/a&gt;&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Me&lt;/strong&gt;: Are you insane? That&amp;rsquo;s a course, not a blog. If you cannot tell the difference, I don&amp;rsquo;t think we should be working on technical items together.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;&lt;strong&gt;Spammer&lt;/strong&gt;: I have some good topics on python&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I bet they do. I bet they do have some &lt;em&gt;great&lt;/em&gt; topics. Probably stolen from legit authors with links back to whatever scam they are pushing.&lt;/p&gt;
&lt;p&gt;It was a fun conversation. ;)&lt;/p&gt;
</description>
        </item>
        
        
        
        <item>
            <title>Python&#39;s Entire Codebase Leaked</title>
            <link>https://mkennedy.codes/posts/python-source-code-has-been-leaked/</link>
            <pubDate>Sun, 13 Nov 2022 00:00:00 +0000</pubDate>
            <author>michael@mkennedy.tech (Michael Kennedy)</author>
            <guid>https://mkennedy.codes/posts/python-source-code-has-been-leaked/</guid>
            <description>&lt;p&gt;If you work in Python, heads up: The entire source code base for Python has been leaked.&lt;/p&gt;
&lt;p&gt;As many as 24,000 copies have been found on GitHub. 24,000&amp;hellip; let that number sink in for a minute.&lt;/p&gt;
&lt;p&gt;If you&amp;rsquo;re familiar with GitHub, they call these rogue copies &amp;ldquo;forks&amp;rdquo;.&lt;/p&gt;
&lt;p&gt;While this news has just broke publicly, after inspecting the records and historical Usenet postings, we believe this code has actually been accessible since 1991.&lt;/p&gt;
&lt;p&gt;See &lt;a href=&#34;https://fosstodon.org/@mkennedy/109356457625573700&#34;&gt;the original citation&lt;/a&gt; for further discussion and details.&lt;/p&gt;
</description>
        </item>
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
        
    </channel>
</rss>
