<?xml version='1.0' encoding='UTF-8'?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <id>https://death.andgravity.com/</id>
  <title>death and gravity</title>
  <updated>2026-04-16T09:00:00+00:00</updated>
  <author>
    <name>Adrian</name>
    <email>lemon@andgravity.com</email>
  </author>
  <link href="https://death.andgravity.com/" rel="alternate"/>
  <link href="https://death.andgravity.com/_feed/index.xml" rel="self"/>
  <entry xml:base="https://death.andgravity.com/hettinger">
    <id>https://death.andgravity.com/hettinger</id>
    <title>Learn Python object-oriented programming with Raymond Hettinger</title>
    <updated>2026-04-16T09:00:00+00:00</updated>
    <content type="html">&lt;blockquote&gt;
&lt;p&gt;💢 &lt;strong&gt;There must be a better way.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Raymond Hettinger&lt;/strong&gt; is a Python core developer.
Even if you haven't heard of him,
you've definitely used his work,
bangers such as
&lt;a class="external" href="https://docs.python.org/3/library/functions.html#sorted"&gt;sorted()&lt;/a&gt;, &lt;a class="external" href="https://docs.python.org/3/library/functions.html#enumerate"&gt;enumerate()&lt;/a&gt;, &lt;a class="external" href="https://docs.python.org/3/library/collections.html"&gt;collections&lt;/a&gt;, &lt;a class="external" href="https://docs.python.org/3/library/collections.html"&gt;itertools&lt;/a&gt;, &lt;a class="external" href="https://docs.python.org/3/library/functools.html#functools.lru_cache"&gt;@lru_cache&lt;/a&gt;,
and many others.
Over the years, he's held lots of great talks,
some of them on
&lt;strong&gt;effective object-oriented programming&lt;/strong&gt;
in Python.&lt;/p&gt;
&lt;p&gt;The talks in this article
had a huge impact on my development as a software engineer,
are some of the best I've heard,
and
are the single most important reason
&lt;strong&gt;you should not be afraid of inheritance&lt;/strong&gt; anymore;
don't trust me, look at the YouTube comments!&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;This list is only to whet your appetite –
to get the most out of it,
&lt;strong&gt;watch the talks in full&lt;/strong&gt;;
besides being a great teacher,
Raymond is quite the entertainer, too.&lt;/p&gt;
&lt;/section&gt;
&lt;details class="toc"&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#the-art-of-subclassing"&gt;The Art of Subclassing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#python-s-class-development-toolkit"&gt;Python's Class Development Toolkit&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#super-considered-super"&gt;Super considered super!&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#object-oriented-programming-from-scratch-four-times"&gt;Object Oriented Programming from scratch (four times)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#beyond-pep-8-best-practices-for-beautiful-intelligible-code"&gt;Beyond PEP 8 – Best practices for beautiful intelligible code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#bonus-the-mental-game-of-python"&gt;Bonus: The Mental Game of Python&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="the-art-of-subclassing"&gt;The Art of Subclassing&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-art-of-subclassing" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Subclassing can just be viewed as a technique for code reuse.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a class="external" href="https://www.youtube.com/watch?v=miGolgp9xq8"&gt;The Art of Subclassing&lt;/a&gt; (2012) is about
use cases, principles, and design patterns for inheritance,
with examples from the standard library.&lt;/p&gt;
&lt;p&gt;The point is to unlearn the animal examples –
instead of the classic &lt;em&gt;hierachical view&lt;/em&gt; where
subclasses are specializations of the parent,
there's an &lt;em&gt;operational view&lt;/em&gt; where:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;classes are &lt;strong&gt;dicts of functions&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;subclasses &lt;strong&gt;point to other dicts&lt;/strong&gt; to reuse their code&lt;/li&gt;
&lt;li&gt;subclasses &lt;strong&gt;decide when to delegate&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This view brings clarity to other related topics:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://en.wikipedia.org/wiki/Liskov_substitution_principle"&gt;Liskov substitution principle&lt;/a&gt;: allow existing code to work with your subclass&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://en.wikipedia.org/wiki/Circle%E2%80%93ellipse_problem"&gt;circle–ellipse problem&lt;/a&gt;: the class with the most reusable code should be the parent&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle"&gt;open–closed principle&lt;/a&gt;: subclasses should not break base class invariants&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Finally, a quote about the standard library:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The best way to become a better Python programmer is to spend some time
reading the source code written by great Python programmers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Sounds familiar?&lt;/p&gt;
&lt;p&gt;&lt;a class="internal" href="/stdlib"&gt;Learn by reading code: Python standard library design decisions explained&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="python-s-class-development-toolkit"&gt;Python's Class Development Toolkit&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#python-s-class-development-toolkit" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Each user will stretch your code in different ways.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a class="external" href="https://www.youtube.com/watch?v=HTLu2DFOdTg"&gt;Python's Class Development Toolkit&lt;/a&gt; (2013)
is a hands-on exercise:
build a single class,
encounter common problems users have with it,
come up with solutions, repeat.&lt;/p&gt;
&lt;p&gt;Use the lean startup methodology
to build an advanced circle analytic toolkit with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;instance and class variables&lt;/li&gt;
&lt;li&gt;instance methods (&lt;code&gt;self&lt;/code&gt; refers to &lt;strong&gt;you or your children&lt;/strong&gt;&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;)&lt;/li&gt;
&lt;li&gt;class methods (use &lt;code&gt;cls&lt;/code&gt; for &lt;strong&gt;alternative contructors&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;static methods (attach functions to classes for discoverability)&lt;/li&gt;
&lt;li&gt;properties (&lt;strong&gt;transparent getters and setters&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;slots (when you have many, many instances)&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="super-considered-super"&gt;Super considered super!&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#super-considered-super" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="external" href="https://www.youtube.com/watch?v=EiOglTERPEo"&gt;Super considered super!&lt;/a&gt; (2015)
goes deep into
&lt;strong&gt;cooperative multiple inheritance&lt;/strong&gt;,
problems you might encounter,
and how to fix them.&lt;/p&gt;
&lt;p&gt;The main point is that
just like how &lt;code&gt;self&lt;/code&gt; refers not to you, but to you &lt;em&gt;or your children&lt;/em&gt;,
&lt;a class="external" href="https://docs.python.org/3/library/functions.html#super"&gt;super()&lt;/a&gt; does not call your ancestors,
but &lt;strong&gt;your children's ancestors&lt;/strong&gt; –
it may even call a class that isn't defined yet.&lt;/p&gt;
&lt;p&gt;This allows you to &lt;strong&gt;change the inheritance chain&lt;/strong&gt; after the fact;
examples include
a form of dependency injection,
overriding parent behavior without changing its code,
and an OrderedCounter class
based on &lt;a class="external" href="https://docs.python.org/3/library/collections.html#collections.Counter"&gt;Counter&lt;/a&gt; and &lt;a class="external" href="https://docs.python.org/3/library/collections.html#collections.OrderedDict"&gt;OrderedDict&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;a class="external" href="https://rhettinger.wordpress.com/2011/05/26/super-considered-super/"&gt;article by the same name&lt;/a&gt; is worth a read too,
and has different examples.&lt;/p&gt;
&lt;h2 id="object-oriented-programming-from-scratch-four-times"&gt;Object Oriented Programming from scratch (four times)&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#object-oriented-programming-from-scratch-four-times" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="external" href="https://www.youtube.com/watch?v=8moWQ1561FY"&gt;Object Oriented Programming from scratch (four times)&lt;/a&gt; (2020)
does exactly that,
each time giving a new insight into
what, how, and why we use objects in Python.&lt;/p&gt;
&lt;p&gt;The first part shows OOP
&lt;strong&gt;emerge naturally from the need for more namespaces&lt;/strong&gt;,
by iteratively improving a script that emulates dictionaries.&lt;/p&gt;
&lt;p&gt;The second one covers the history of
moving from a huge pile of data and functions to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;data associated with functions (objects)&lt;/li&gt;
&lt;li&gt;groups of related functions (classes)&lt;/li&gt;
&lt;li&gt;related groups of functions (inheritance)&lt;/li&gt;
&lt;li&gt;using the same name for similar functions (polymorphism)&lt;/li&gt;
&lt;/ul&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Sounds familiar?&lt;/p&gt;
&lt;p&gt;&lt;a class="internal" href="/same-arguments"&gt;When to use classes in Python? When your functions take the same arguments&lt;/a&gt;
&lt;br&gt;
&lt;a class="internal" href="/same-functions"&gt;When to use classes in Python? When you repeat similar sets of functions&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;The third part explains the mechanics of objects via &lt;a class="external" href="https://docs.python.org/3/library/collections.html#collections.ChainMap"&gt;ChainMap&lt;/a&gt;, tl;dr:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;ChainMap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;instance_dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;class_dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parent_class_dict&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The fourth part highlights
how OOP naturally expresses &lt;strong&gt;entities and relationships&lt;/strong&gt;
by looking the data model of a Twitter clone
and the syntax tree of a compiler.&lt;/p&gt;
&lt;h2 id="beyond-pep-8-best-practices-for-beautiful-intelligible-code"&gt;Beyond PEP 8 – Best practices for beautiful intelligible code&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#beyond-pep-8-best-practices-for-beautiful-intelligible-code" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;Well factored code looks like business logic.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a class="external" href="https://www.youtube.com/watch?v=wf-BqAjZb8M"&gt;Beyond PEP 8 – Best practices for beautiful intelligible code&lt;/a&gt; (2015)
(&lt;a class="external" href="https://gist.github.com/sajalshres/8af0f85002c5b930236e67a5d7fb4c78"&gt;code&lt;/a&gt;)
is about how excessive focus on
following &lt;a class="external" href="https://peps.python.org/pep-0008/"&gt;PEP 8&lt;/a&gt; can lead to
&lt;strong&gt;code that is beautiful, but bad&lt;/strong&gt;,
since it distracts from the beauty that really matters:&lt;/p&gt;
&lt;blockquote&gt;

&lt;dl&gt;
&lt;dt&gt;Pythonic&lt;/dt&gt;
&lt;dd&gt;coding beautifully in harmony with the language
to get the maximum benefits from Python&lt;/dd&gt;
&lt;/dl&gt;
&lt;/blockquote&gt;

&lt;p&gt;Transform a bad API into a good one
using an adapter class and stuff like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;context managers for setup / teardown&lt;/li&gt;
&lt;li&gt;flat modules for simpler imports&lt;/li&gt;
&lt;li&gt;magic methods to make things iterable&lt;/li&gt;
&lt;li&gt;properties instead of getter methods&lt;/li&gt;
&lt;li&gt;custom exceptions for clearer business logic&lt;/li&gt;
&lt;li&gt;a good __repr__() for better debuggability&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;And remember:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If you don't transform bad APIs into Pythonic APIs, you're a fool!&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="bonus-the-mental-game-of-python"&gt;Bonus: The Mental Game of Python&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bonus-the-mental-game-of-python" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;The computer gives us words that do things;
what daddy does is make new words to make computers easier to use.&lt;sup class="footnote-ref" id="fnref-3"&gt;&lt;a href="#fn-3"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;a class="external" href="https://www.youtube.com/watch?v=Uwuv05aZ6ug"&gt;The Mental Game of Python&lt;/a&gt; (2019)
is not just about programming,
but about problem solving strategies.
The most relevant one for OOP is:
&lt;strong&gt;build classes independently and let inheritance discover itself&lt;/strong&gt;;
this is because:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A lot of real world problems aren't tic-tac-toe problems,
where you can see to the end;
they are chess problems, where you can't.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;For a more meta discussion of this idea,
check out
&lt;a class="external" href="https://programmingisterrible.com/post/176657481103/repeat-yourself-do-more-than-one-thing-and"&gt;Repeat yourself, do more than one thing, and rewrite everything&lt;/a&gt;
by &lt;a class="external" href="https://programmingisterrible.com/about"&gt;tef&lt;/a&gt;;
it should be considered a classic at this point.&lt;/p&gt;
&lt;p&gt;Parting Raymond Hettinger quote:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;I came to here you to show you how to chunk,
and how to stack one chunk on top of the other,
and this is a way to &lt;strong&gt;reduce your cognitive load&lt;/strong&gt; and &lt;strong&gt;manage complexity&lt;/strong&gt;;
it is the core of our craft; it's what we're here to do.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr /&gt;
&lt;p&gt;Anyway, that's it for now. :)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Who learned something new today?&lt;/strong&gt; Share it with others! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/hettinger&amp;t=Learn%20Python%20object-oriented%20programming%20with%20Raymond%20Hettinger"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Learn%20Python%20object-oriented%20programming%20with%20Raymond%20Hettinger%20https%3A//death.andgravity.com/hettinger"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/hettinger&amp;title=Learn%20Python%20object-oriented%20programming%20with%20Raymond%20Hettinger"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/hettinger"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Learn%20Python%20object-oriented%20programming%20with%20Raymond%20Hettinger&amp;url=https%3A//death.andgravity.com/hettinger&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;But remember it's a principle, not a law, violations are fine –
you may only want substitutability in some places
(e.g. contructors are often not substitutable). &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;...unless you use &lt;a class="external" href="https://docs.python.org/3/tutorial/classes.html#tut-private"&gt;double underscores&lt;/a&gt;. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-3"&gt;&lt;p&gt;In my opinion, this quote alone is reason enough to watch the talk. &lt;a href="#fnref-3" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/hettinger" rel="alternate"/>
    <summary>Even if you haven't heard of Raymond Hettinger, you've definitely used his work, bangers such as sorted(), collections, and many others. His talks had a huge impact on my development as a software engineer, are some of the best I've heard, and are the reason *you should not be afraid of inheritance* anymore.</summary>
    <published>2026-04-14T18:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/reader-3-22">
    <id>https://death.andgravity.com/reader-3-22</id>
    <title>reader 3.22 released – new web app</title>
    <updated>2026-04-02T18:00:00+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;I'm happy to announce version 3.22 of &lt;strong&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/strong&gt;, a Python feed reader library.&lt;/p&gt;
&lt;h2 id="what-s-new"&gt;What's new?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-new" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Here are the highlights since &lt;a class="internal" href="/reader-3-20"&gt;reader 3.20&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="new-feed-reader-web-app"&gt;New feed reader web app&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#new-feed-reader-web-app" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The new &lt;a class="external" href="https://reader.readthedocs.io/en/latest/app.html"&gt;web application&lt;/a&gt; is done!
Features include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;add / list / filter / delete feeds&lt;/li&gt;
&lt;li&gt;list / filter articles&lt;/li&gt;
&lt;li&gt;mark articles as read / (un)important&lt;/li&gt;
&lt;li&gt;article view&lt;/li&gt;
&lt;li&gt;custom feed titles&lt;/li&gt;
&lt;li&gt;dark mode&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the next releases,
I'll be adding back features already present in the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/app.html#legacy-web-app"&gt;legacy web app&lt;/a&gt;,
stuff like &lt;a class="external" href="https://reader.readthedocs.io/en/latest/guide.html#full-text-search"&gt;full-text search&lt;/a&gt;, &lt;a class="external" href="https://reader.readthedocs.io/en/latest/guide.html#resource-tags"&gt;tags&lt;/a&gt;, &lt;a class="external" href="https://reader.readthedocs.io/en/latest/plugins.html#readtime"&gt;read time&lt;/a&gt;, &lt;a class="external" href="https://reader.readthedocs.io/en/latest/plugins.html#enclosure-tags"&gt;MP3 tag fixing&lt;/a&gt;, and more.&lt;/p&gt;
&lt;p&gt;This is building towards
a &lt;strong&gt;hosted version of &lt;em&gt;reader&lt;/em&gt;&lt;/strong&gt;,
which should take the pain out of self-hosting,
while still leaving it as an option;
more to follow soon™.
(Meanwhile,
if this sounds like something you'd like to use,
&lt;a class="internal" href="/about#contact"&gt;get in touch&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;For now, here are some screenshots:&lt;/p&gt;
&lt;div class="columns"&gt;&lt;div class="column col-sm-6 col-xs-12"&gt;

&lt;figure class="figure"&gt;
&lt;img class="img-responsive" src="/_file/reader-3-22/entries.png" alt="main page (dark mode)" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
main page (dark mode)
&lt;/figcaption&gt;
&lt;/figure&gt;



&lt;/div&gt;
&lt;div class="column col-sm-6 col-xs-12"&gt;

&lt;figure class="figure"&gt;
&lt;img class="img-responsive" src="/_file/reader-3-22/entries-filters-light.png" alt="more filters (light mode)" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
more filters (light mode)
&lt;/figcaption&gt;
&lt;/figure&gt;



&lt;/div&gt;
&lt;div class="column col-sm-6 col-xs-12"&gt;

&lt;figure class="figure"&gt;
&lt;img class="img-responsive" src="/_file/reader-3-22/feed.png" alt="feed page (dark mode)" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
feed page (dark mode)
&lt;/figcaption&gt;
&lt;/figure&gt;



&lt;/div&gt;
&lt;/div&gt;

&lt;div class="columns"&gt;&lt;div class="column col-sm-6 col-xs-12"&gt;

&lt;figure class="figure"&gt;
&lt;img class="img-responsive" src="/_file/reader-3-22/feeds.png" alt="feeds page (dark mode)" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
feeds page (dark mode)
&lt;/figcaption&gt;
&lt;/figure&gt;



&lt;/div&gt;
&lt;div class="column col-sm-6 col-xs-12"&gt;

&lt;figure class="figure"&gt;
&lt;img class="img-responsive" src="/_file/reader-3-22/entry-image-light.png" alt="article view (light mode)" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
article view (light mode)
&lt;/figcaption&gt;
&lt;/figure&gt;



&lt;/div&gt;
&lt;div class="column col-sm-6 col-xs-12"&gt;

&lt;figure class="figure"&gt;
&lt;img class="img-responsive" src="/_file/reader-3-22/entry.png" alt="article view (dark mode)" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
article view (dark mode)
&lt;/figcaption&gt;
&lt;/figure&gt;



&lt;/div&gt;
&lt;/div&gt;

&lt;h3 id="config-and-plugin-loading"&gt;Config and plugin loading&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#config-and-plugin-loading" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Part of the hosted &lt;em&gt;reader&lt;/em&gt; work,
I've unified how &lt;a class="external" href="https://reader.readthedocs.io/en/latest/config.html"&gt;configuration&lt;/a&gt; and &lt;a class="external" href="https://reader.readthedocs.io/en/latest/plugins.html"&gt;plugins&lt;/a&gt; are loaded
across &lt;a class="external" href="https://reader.readthedocs.io/en/latest/api.html#reader.make_reader"&gt;make_reader()&lt;/a&gt;, the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/cli.html"&gt;command-line interface&lt;/a&gt;, and the web app,
by using &lt;a class="external" href="https://click.palletsprojects.com/"&gt;Click&lt;/a&gt; to parse and validate configuration
(it's not as wrong as it sounds, I promise).&lt;/p&gt;
&lt;p&gt;As a consequence,
the config file format changed from YAML to TOML
and follows the shape of the CLI,
and a few commands and environment variables were renamed;
no other breaking changes are expected in the foreseeable future.&lt;/p&gt;
&lt;h3 id="scheduled-updates-by-default"&gt;Scheduled updates by default&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#scheduled-updates-by-default" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Both &lt;a class="external" href="https://reader.readthedocs.io/en/latest/api.html#reader.Reader.update_feeds"&gt;update_feeds()&lt;/a&gt; and the CLI now limit
&lt;a class="external" href="https://reader.readthedocs.io/en/latest/guide.html#scheduled"&gt;how often feeds get updated&lt;/a&gt; by default;
while this is a minor compatibility break,
the previous behavior was arguably a bug –
doing the right thing should not be opt-in.&lt;/p&gt;
&lt;p&gt;Also, &lt;em&gt;reader&lt;/em&gt; now honors the Cache-Control max-age and Expires HTTP headers
when updating feeds,
in addition to Retry-After.&lt;/p&gt;
&lt;h3 id="ai-contributions"&gt;AI contributions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#ai-contributions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Finally, &lt;em&gt;reader&lt;/em&gt; now has an &lt;a class="external" href="https://reader.readthedocs.io/en/latest/contributing.html#ai-contributions"&gt;AI contributions&lt;/a&gt; policy;
tl;dr: they are banned, for now.&lt;/p&gt;
&lt;p&gt;The reasoning is two-fold.
First, after a few low-effort contributions,
I decided I don't have time for this.
Second, there are various issues
surrounding LLMs I don't want to bother with;
for more details,
see the &lt;a class="external" href="https://web.archive.org/web/20260307194456/https://book.servo.org/contributing/getting-started.html#ai-contributions"&gt;Servo&lt;/a&gt;, &lt;a class="external" href="https://web.archive.org/web/20260211125214/https://devguide.python.org/getting-started/generative-ai/"&gt;CPython&lt;/a&gt;, and &lt;a class="external" href="https://web.archive.org/web/20260302012029/https://www.llvm.org/docs/AIToolPolicy.html"&gt;LLVM&lt;/a&gt; policies.&lt;/p&gt;
&lt;p&gt;I am open to revisiting this later (I'll do so on my own, though, thank you).&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.
For more details, see the full &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-3-22"&gt;changelog&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Want to contribute?&lt;/strong&gt;
Check out the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/contributing.html"&gt;docs&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/dev.html#roadmap"&gt;roadmap&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/reader-3-22&amp;t=reader%203.22%20released%20%E2%80%93%20new%20web%20app"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=reader%203.22%20released%20%E2%80%93%20new%20web%20app%20https%3A//death.andgravity.com/reader-3-22"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/reader-3-22&amp;title=reader%203.22%20released%20%E2%80%93%20new%20web%20app"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/reader-3-22"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=reader%203.22%20released%20%E2%80%93%20new%20web%20app&amp;url=https%3A//death.andgravity.com/reader-3-22&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;h2 id="what-is-reader"&gt;What is &lt;em&gt;reader&lt;/em&gt;?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;reader&lt;/strong&gt; takes care
of the core functionality required by a feed reader,
so you can focus on what makes &lt;strong&gt;yours&lt;/strong&gt; different.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-2-0/reader.png" alt="reader in action" /&gt;
&lt;em&gt;reader&lt;/em&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retrieve, store, and manage &lt;strong&gt;Atom&lt;/strong&gt;, &lt;strong&gt;RSS&lt;/strong&gt;, and &lt;strong&gt;JSON&lt;/strong&gt; feeds&lt;/li&gt;
&lt;li&gt;mark articles as read or important&lt;/li&gt;
&lt;li&gt;add arbitrary tags/metadata to feeds and articles&lt;/li&gt;
&lt;li&gt;filter feeds and articles&lt;/li&gt;
&lt;li&gt;full-text search articles&lt;/li&gt;
&lt;li&gt;get statistics on feed and user activity&lt;/li&gt;
&lt;li&gt;write plugins to extend its functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...all these with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stable, clearly documented API&lt;/li&gt;
&lt;li&gt;excellent test coverage&lt;/li&gt;
&lt;li&gt;fully typed Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find out more, check out the &lt;a class="external" href="https://github.com/lemon24/reader"&gt;GitHub repo&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/"&gt;docs&lt;/a&gt;,
or give the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/tutorial.html"&gt;tutorial&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2 id="why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-use-a-feed-reader-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Have you been unhappy with existing feed readers and wanted to make your own, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never knew where to start?&lt;/li&gt;
&lt;li&gt;it seemed like too much work?&lt;/li&gt;
&lt;li&gt;you don't like writing backend code?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you already working with &lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/"&gt;feedparser&lt;/a&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;want an easier way to store, filter, sort and search feeds and entries?&lt;/li&gt;
&lt;li&gt;want to get back type-annotated objects instead of dicts?&lt;/li&gt;
&lt;li&gt;want to restrict or deny file-system access?&lt;/li&gt;
&lt;li&gt;want to change the way feeds are retrieved by using &lt;a class="external" href="https://requests.readthedocs.io"&gt;Requests&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to also support &lt;a class="external" href="https://jsonfeed.org/"&gt;JSON Feed&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to support custom information sources?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... while still supporting all the feed types feedparser does?&lt;/p&gt;
&lt;p&gt;If you answered yes to any of the above, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;h2 id="the-reader-philosophy"&gt;The &lt;em&gt;reader&lt;/em&gt; philosophy&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-reader-philosophy" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is a library&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is for the long term&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is extensible&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is stable (within reason)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is simple to use; API matters&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; features work well together&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is tested&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is documented&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; has minimal dependencies&lt;/li&gt;
&lt;/ul&gt;
</content>
    <link href="https://death.andgravity.com/reader-3-22" rel="alternate"/>
    <published>2026-04-02T18:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/dynamodb-model">
    <id>https://death.andgravity.com/dynamodb-model</id>
    <title>DynamoDB crash course: part 2 – data model</title>
    <updated>2026-02-10T10:35:00+00:00</updated>
    <content type="html">&lt;p&gt;This is part two of a series
covering core &lt;strong&gt;DynamoDB&lt;/strong&gt; concepts,
from &lt;a class="internal" href="/dynamodb"&gt;philosophy&lt;/a&gt;
all the way to &lt;strong&gt;single-table design&lt;/strong&gt;.
The goal is to get you
to understand &lt;strong&gt;idiomatic usage&lt;/strong&gt; and &lt;strong&gt;trade-offs&lt;/strong&gt;
in under an hour.&lt;/p&gt;
&lt;p&gt;Today, we're looking at the DynamoDB data model –
&lt;strong&gt;what the main abstractions are&lt;/strong&gt;,
&lt;strong&gt;what you can do with them&lt;/strong&gt;,
and &lt;strong&gt;how they scale&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;(While the AWS documentation is &lt;em&gt;mostly&lt;/em&gt; comprehensive,
it's also all over the place,
including some other places that aren't the documentation at all,
like the AWS blog.
This series brings the important stuff in one place,
so you can get a &lt;strong&gt;mental model&lt;/strong&gt; of how it all ties together
without having to read the entire documentation &lt;a class="internal" href="/output"&gt;twice&lt;/a&gt;).&lt;/p&gt;
&lt;details class="toc"&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#core-components"&gt;Core components&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#api-model-tables-items-attributes"&gt;API model: tables, items, attributes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#logical-model-hash-table-of-b-trees"&gt;Logical model: hash table of B‍-‍trees&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#physical-model-partitions"&gt;Physical model: partitions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#limits"&gt;Limits&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#partition-throughput"&gt;Partition throughput&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#item-size"&gt;Item size&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#page-size"&gt;Page size&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#indexes"&gt;Indexes&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#global-secondary-indexes"&gt;Global secondary indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#local-secondary-indexes"&gt;Local secondary indexes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#features"&gt;Features&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#eventual-consistency"&gt;Eventual consistency&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#conditional-writes"&gt;Conditional writes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#transactions"&gt;Transactions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#batch-operations"&gt;Batch operations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#streams"&gt;Streams&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="core-components"&gt;Core components&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#core-components" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;According to the &lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html"&gt;documentation&lt;/a&gt;,
the core components of DynamoDB are tables, items, and attributes.
This is accurate in the sense of &lt;strong&gt;what you can act on&lt;/strong&gt; through the API,
but can be deceptively simple,
and leaves out two other equally important aspects:
&lt;strong&gt;what you can do with it&lt;/strong&gt; (the &lt;em&gt;logical model&lt;/em&gt;)
and &lt;strong&gt;how it scales&lt;/strong&gt; (the &lt;em&gt;physical model&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;&lt;a id="diagram"&gt;&lt;/a&gt;
Let's put it all together, starting from the top.&lt;/p&gt;
&lt;figure class="figure"&gt;

&lt;svg xmlns='http://www.w3.org/2000/svg' style='font-size:initial;' class="pikchr" viewBox="0 0 655.2 371.098" data-pikchr-date="20260102012653"&gt;
&lt;path d="M3.6,329.76L651.6,329.76A1.44 1.44 0 0 0 653.04 328.32L653.04,3.6A1.44 1.44 0 0 0 651.6 2.16L3.6,2.16A1.44 1.44 0 0 0 2.16 3.6L2.16,328.32A1.44 1.44 0 0 0 3.6 329.76Z"  style="fill:rgb(230,230,250);stroke-width:2.16;stroke:rgb(102,51,153);" /&gt;
&lt;path d="M18,315.36L487.44,315.36A1.44 1.44 0 0 0 488.88 313.92L488.88,39.6A1.44 1.44 0 0 0 487.44 38.16L18,38.16A1.44 1.44 0 0 0 16.56 39.6L16.56,313.92A1.44 1.44 0 0 0 18 315.36Z"  style="fill:rgb(255,228,225);stroke-width:1.4472;stroke:rgb(255,0,0);stroke-dasharray:4.32,4.32;" /&gt;
&lt;path d="M32.4,300.96L323.28,300.96A1.44 1.44 0 0 0 324.72 299.52L324.72,97.2A1.44 1.44 0 0 0 323.28 95.76L32.4,95.76A1.44 1.44 0 0 0 30.96 97.2L30.96,299.52A1.44 1.44 0 0 0 32.4 300.96Z"  style="fill:rgb(230,230,250);stroke-width:2.16;stroke:rgb(102,51,153);" /&gt;
&lt;path d="M46.8,286.56L159.12,286.56A1.44 1.44 0 0 0 160.56 285.12L160.56,162A1.44 1.44 0 0 0 159.12 160.56L46.8,160.56A1.44 1.44 0 0 0 45.36 162L45.36,285.12A1.44 1.44 0 0 0 46.8 286.56Z"  style="fill:rgb(230,230,250);stroke-width:2.16;stroke:rgb(102,51,153);" /&gt;
&lt;path d="M61.2,221.76L144.72,221.76A1.44 1.44 0 0 0 146.16 220.32L146.16,198A1.44 1.44 0 0 0 144.72 196.56L61.2,196.56A1.44 1.44 0 0 0 59.76 198L59.76,220.32A1.44 1.44 0 0 0 61.2 221.76Z"  style="fill:rgb(255,255,255);stroke-width:0;stroke:rgb(30,144,255);" /&gt;
&lt;text x="102.96" y="209.16" text-anchor="middle" fill="rgb(30,144,255)" dominant-baseline="central"&gt;attribute&lt;/text&gt;
&lt;path d="M61.2,246.96L144.72,246.96A1.44 1.44 0 0 0 146.16 245.52L146.16,223.2A1.44 1.44 0 0 0 144.72 221.76L61.2,221.76A1.44 1.44 0 0 0 59.76 223.2L59.76,245.52A1.44 1.44 0 0 0 61.2 246.96Z"  style="fill:rgb(255,255,255);stroke-width:0;stroke:rgb(30,144,255);" /&gt;
&lt;text x="102.96" y="234.36" text-anchor="middle" fill="rgb(30,144,255)" dominant-baseline="central"&gt;attribute&lt;/text&gt;
&lt;path d="M61.2,272.16L144.72,272.16A1.44 1.44 0 0 0 146.16 270.72L146.16,248.4A1.44 1.44 0 0 0 144.72 246.96L61.2,246.96A1.44 1.44 0 0 0 59.76 248.4L59.76,270.72A1.44 1.44 0 0 0 61.2 272.16Z"  style="fill:rgb(255,255,255);stroke-width:0;stroke:rgb(30,144,255);" /&gt;
&lt;text x="102.96" y="259.56" text-anchor="middle" font-weight="bold" fill="rgb(30,144,255)" dominant-baseline="central"&gt;···&lt;/text&gt;
&lt;text x="51.12" y="175.68" text-anchor="start" font-weight="bold" fill="rgb(102,51,153)" dominant-baseline="central"&gt;item&lt;/text&gt;
&lt;path d="M176.4,286.56L259.92,286.56A1.44 1.44 0 0 0 261.36 285.12L261.36,162A1.44 1.44 0 0 0 259.92 160.56L176.4,160.56A1.44 1.44 0 0 0 174.96 162L174.96,285.12A1.44 1.44 0 0 0 176.4 286.56Z"  style="fill:rgb(230,230,250);stroke-width:2.16;stroke:rgb(102,51,153);" /&gt;
&lt;text x="218.16" y="223.56" text-anchor="middle" font-weight="bold" fill="rgb(102,51,153)" dominant-baseline="central"&gt;···&lt;/text&gt;
&lt;text x="180.72" y="175.68" text-anchor="start" font-weight="bold" fill="rgb(102,51,153)" dominant-baseline="central"&gt;item&lt;/text&gt;
&lt;text x="293.04" y="223.56" text-anchor="middle" font-weight="bold" fill="rgb(102,51,153)" dominant-baseline="central"&gt;···&lt;/text&gt;
&lt;text x="36.72" y="110.88" text-anchor="start" font-weight="bold" fill="rgb(102,51,153)" dominant-baseline="central"&gt;collection (B-tree)&lt;/text&gt;
&lt;path d="M340.56,300.96L424.08,300.96A1.44 1.44 0 0 0 425.52 299.52L425.52,97.2A1.44 1.44 0 0 0 424.08 95.76L340.56,95.76A1.44 1.44 0 0 0 339.12 97.2L339.12,299.52A1.44 1.44 0 0 0 340.56 300.96Z"  style="fill:rgb(230,230,250);stroke-width:2.16;stroke:rgb(102,51,153);" /&gt;
&lt;text x="382.32" y="198.36" text-anchor="middle" font-weight="bold" fill="rgb(102,51,153)" dominant-baseline="central"&gt;···&lt;/text&gt;
&lt;text x="344.88" y="110.88" text-anchor="start" font-weight="bold" fill="rgb(102,51,153)" dominant-baseline="central"&gt;collection&lt;/text&gt;
&lt;text x="457.2" y="198.36" text-anchor="middle" font-weight="bold" fill="rgb(102,51,153)" dominant-baseline="central"&gt;···&lt;/text&gt;
&lt;text x="22.32" y="53.28" text-anchor="start" fill="rgb(255,0,0)" dominant-baseline="central"&gt;partition&lt;/text&gt;
&lt;path d="M504.72,315.36L588.24,315.36A1.44 1.44 0 0 0 589.68 313.92L589.68,39.6A1.44 1.44 0 0 0 588.24 38.16L504.72,38.16A1.44 1.44 0 0 0 503.28 39.6L503.28,313.92A1.44 1.44 0 0 0 504.72 315.36Z"  style="fill:rgb(255,228,225);stroke-width:1.4472;stroke:rgb(255,0,0);stroke-dasharray:4.32,4.32;" /&gt;
&lt;text x="546.48" y="176.76" text-anchor="middle" font-weight="bold" fill="rgb(255,0,0)" dominant-baseline="central"&gt;···&lt;/text&gt;
&lt;text x="509.04" y="53.28" text-anchor="start" fill="rgb(255,0,0)" dominant-baseline="central"&gt;partition&lt;/text&gt;
&lt;text x="621.36" y="176.76" text-anchor="middle" font-weight="bold" fill="rgb(255,0,0)" dominant-baseline="central"&gt;···&lt;/text&gt;
&lt;text x="7.92" y="17.28" text-anchor="start" font-weight="bold" fill="rgb(102,51,153)" dominant-baseline="central"&gt;table (hash table)&lt;/text&gt;
&lt;polygon points="218.16,160.56 215.266,152.842 221.054,152.842" style="fill:rgb(102,51,153)"/&gt;
&lt;path d="M218.16,156.701L218.16,146.16"  style="fill:none;stroke-width:1.4472;stroke:rgb(102,51,153);" /&gt;
&lt;text x="218.16" y="133.56" text-anchor="middle" fill="rgb(102,51,153)" dominant-baseline="central"&gt;sort key&lt;/text&gt;
&lt;polygon points="102.96,160.56 100.066,152.842 105.854,152.842" style="fill:rgb(102,51,153)"/&gt;
&lt;path d="M102.96,156.701L102.96,146.16"  style="fill:none;stroke-width:1.4472;stroke:rgb(102,51,153);" /&gt;
&lt;text x="102.96" y="133.56" text-anchor="middle" fill="rgb(102,51,153)" dominant-baseline="central"&gt;sort key&lt;/text&gt;
&lt;text x="160.56" y="133.56" text-anchor="middle" fill="rgb(102,51,153)" dominant-baseline="central"&gt;&amp;lt;&lt;/text&gt;
&lt;polygon points="177.84,95.76 174.946,88.0416 180.734,88.0416" style="fill:rgb(102,51,153)"/&gt;
&lt;path d="M177.84,91.9008L177.84,81.36"  style="fill:none;stroke-width:1.4472;stroke:rgb(102,51,153);" /&gt;
&lt;text x="177.84" y="68.76" text-anchor="middle" fill="rgb(102,51,153)" dominant-baseline="central"&gt;partition key&lt;/text&gt;
&lt;polygon points="382.32,95.76 379.426,88.0416 385.214,88.0416" style="fill:rgb(102,51,153)"/&gt;
&lt;path d="M382.32,91.9008L382.32,81.36"  style="fill:none;stroke-width:1.4472;stroke:rgb(102,51,153);" /&gt;
&lt;text x="382.32" y="68.76" text-anchor="middle" fill="rgb(102,51,153)" dominant-baseline="central"&gt;partition key&lt;/text&gt;
&lt;path d="M4.02177,368.938L87.5418,368.938A1.44 1.44 0 0 0 88.9818 367.498L88.9818,345.178A1.44 1.44 0 0 0 87.5418 343.738L4.02177,343.738A1.44 1.44 0 0 0 2.58177 345.178L2.58177,367.498A1.44 1.44 0 0 0 4.02177 368.938Z"  style="fill:rgb(230,230,250);stroke-width:2.16;stroke:rgb(102,51,153);" /&gt;
&lt;text x="45.7818" y="356.338" text-anchor="middle" fill="rgb(102,51,153)" font-size="80%" dominant-baseline="central"&gt;logical&lt;/text&gt;
&lt;path d="M97.6218,368.938L181.142,368.938A1.44 1.44 0 0 0 182.582 367.498L182.582,345.178A1.44 1.44 0 0 0 181.142 343.738L97.6218,343.738A1.44 1.44 0 0 0 96.1818 345.178L96.1818,367.498A1.44 1.44 0 0 0 97.6218 368.938Z"  style="fill:rgb(255,228,225);stroke-width:1.4472;stroke:rgb(255,0,0);stroke-dasharray:4.32,4.32;" /&gt;
&lt;text x="139.382" y="356.338" text-anchor="middle" fill="rgb(255,0,0)" font-size="80%" dominant-baseline="central"&gt;physical only&lt;/text&gt;
&lt;path d="M191.222,368.938L274.742,368.938A1.44 1.44 0 0 0 276.182 367.498L276.182,345.178A1.44 1.44 0 0 0 274.742 343.738L191.222,343.738A1.44 1.44 0 0 0 189.782 345.178L189.782,367.498A1.44 1.44 0 0 0 191.222 368.938Z"  style="fill:rgb(255,255,255);stroke-width:0;stroke:rgb(30,144,255);" /&gt;
&lt;text x="232.982" y="356.338" text-anchor="middle" fill="rgb(30,144,255)" font-size="80%" dominant-baseline="central"&gt;API only&lt;/text&gt;
&lt;text x="162.298" y="217.109" text-anchor="middle" fill="rgb(255,228,225)" font-size="64%" transform="rotate(90 162.298,223.56)" dominant-baseline="central"&gt;death.andgravity.com&lt;/text&gt;
&lt;/svg&gt;


&lt;/figure&gt;

&lt;h3 id="api-model-tables-items-attributes"&gt;API model: tables, items, attributes&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#api-model-tables-items-attributes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;As far as the API is concerned,
&amp;quot;a &lt;strong&gt;table&lt;/strong&gt; is a collection of &lt;strong&gt;items&lt;/strong&gt;,
and each item is a collection of &lt;strong&gt;attributes&lt;/strong&gt;&amp;quot;.&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;An item is uniquely identified by two attributes,
the &lt;strong&gt;partition key&lt;/strong&gt; and the &lt;strong&gt;sort key&lt;/strong&gt;,&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;
which together compose its &lt;strong&gt;primary key&lt;/strong&gt;.&lt;sup class="footnote-ref" id="fnref-3"&gt;&lt;a href="#fn-3"&gt;3&lt;/a&gt;&lt;/sup&gt;
A group of items with the same partition key value
is called an &lt;strong&gt;item collection&lt;/strong&gt;,&lt;sup class="footnote-ref" id="fnref-4"&gt;&lt;a href="#fn-4"&gt;4&lt;/a&gt;&lt;/sup&gt;
but this is more of a logical grouping,
and does not exist as a distinct entity in the API.&lt;/p&gt;
&lt;p&gt;An attribute is a named data element,
with its value
either a scalar (number, string, binary, boolean, null),
a set of scalars,
or a document (a list or map of possibly nested attributes, similar to JSON).&lt;/p&gt;
&lt;p&gt;There are no limits on table size or number of items,
nor on those of an item collection.
Items do have a &lt;a class="anchor" href="#item-size"&gt;size limit&lt;/a&gt; of 400 KB / item,
which indirectly limits attribute size.&lt;/p&gt;
&lt;p&gt;As we've seen &lt;a class="internal" href="/dynamodb#python-mockup"&gt;in the previous article&lt;/a&gt;,
the core DynamoDB data operations are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;PutItem&lt;/code&gt;, &lt;code&gt;GetItem&lt;/code&gt;, &lt;code&gt;UpdateItem&lt;/code&gt;, &lt;code&gt;DeleteItem&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Query&lt;/code&gt; items with the same partition key, sorted by sort key,
and optionally narrowed down to a specific a range of sort keys&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Scan&lt;/code&gt; all the items in the table, possibly in parallel&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Besides whole items,
the API allows getting and updating specific attributes,
as well as filtering query and scan results by expressions using them.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html"&gt;Core components&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithItems.html"&gt;Working with items&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.NamingRulesDataTypes.html"&gt;Supported data types and naming rules&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;h3 id="logical-model-hash-table-of-b-trees"&gt;Logical model: hash table of B‍-‍trees&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#logical-model-hash-table-of-b-trees" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The operations above may seem arbitrarily restrictive
– for example, why can't I query items by sort key alone?
It might make more sense to think about it like this:&lt;/p&gt;
&lt;p&gt;Conceptually,
a DynamoDB table is &lt;strong&gt;a &lt;a class="external" href="https://en.wikipedia.org/wiki/Hash_table"&gt;hash table&lt;/a&gt; of &lt;a class="external" href="https://en.wikipedia.org/wiki/B-tree"&gt;B‍-‍trees&lt;/a&gt;&lt;/strong&gt;,
with partition keys being hash table keys,
and sort keys being B‍-‍tree keys
(making &lt;em&gt;item collections&lt;/em&gt; B‍-‍trees).
The hash table allows efficient &lt;em&gt;find collection by partition key&lt;/em&gt; operations;
within each collection,
the B‍-‍tree keeps the items sorted,
and allows efficient &lt;em&gt;find item by sort key&lt;/em&gt;
and &lt;em&gt;find items by sort key range&lt;/em&gt; operations.&lt;/p&gt;
&lt;p&gt;As a consequence,
&lt;strong&gt;any access not based on partition and sort key is expensive&lt;/strong&gt;,
since instead of taking advantage of the underlying data structure,
you have to go through &lt;em&gt;all the items&lt;/em&gt; in the table to find anything
(aka a &lt;em&gt;full table scan&lt;/em&gt;),
and at the scales you'd use DynamoDB at,
this can mean billions of items.&lt;sup class="footnote-ref" id="fnref-5"&gt;&lt;a href="#fn-5"&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Example&lt;/p&gt;
&lt;p&gt;&lt;small&gt;(from &lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#:~:text=another%20example%20table%20named%20Music"&gt;here&lt;/a&gt;)&lt;/small&gt;
Take a &lt;em&gt;Music&lt;/em&gt; table where items correspond to songs,
with &lt;em&gt;Artist&lt;/em&gt; as primary key and &lt;em&gt;Song&lt;/em&gt; as sort key:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="YAML"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# table Music (partition key: Artist, sort key: Song)&lt;/span&gt;
&lt;span class="nt"&gt;1000mods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!btree&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Claws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Album&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Vultures&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Vidage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;2011&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;Kyuss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!btree&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Space Cadet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can efficiently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;query songs by artist (sorted by song title)&lt;/li&gt;
&lt;li&gt;get the song by artist and song title&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...and that's it, anything else requires a full table scan.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;To a first approximation,
this is also a decent model of how DynamoDB scales
–
you could imagine that each collection has its own dedicated computer,
which in theory would account for the unlimited number of collections.&lt;/p&gt;
&lt;h3 id="physical-model-partitions"&gt;Physical model: partitions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#physical-model-partitions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Of course, there are &lt;em&gt;not&lt;/em&gt; infinitely many computers,
and that would be wildly inefficient anyway.
Instead, collections are packed together
into a smaller number of &lt;strong&gt;partitions&lt;/strong&gt;, each a few gigabytes in size.
To figure out which partition an item should go on,
DynamoDB hashes its &lt;em&gt;partition key&lt;/em&gt;
(also called a &lt;em&gt;hash key&lt;/em&gt;, for obvious reasons).&lt;/p&gt;
&lt;p&gt;This is similar to hash table buckets,&lt;sup class="footnote-ref" id="fnref-6"&gt;&lt;a href="#fn-6"&gt;6&lt;/a&gt;&lt;/sup&gt;
except there's one more level of indirection –
instead of mapping to a single number,
each partition maps to a range of numbers,
which allows splitting a partition into two new ones
by splitting its range.
Furthermore, an item collection
can be split on multiple partitions too,
by using the sort key.&lt;sup class="footnote-ref" id="fnref-7"&gt;&lt;a href="#fn-7"&gt;7&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;And &lt;em&gt;that&lt;/em&gt; is how the scaling magic happens:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;When you increase provisioned capacity,
partitions are split as needed.&lt;/li&gt;
&lt;li&gt;If a partition or collection becomes too big,
it gets split.&lt;/li&gt;
&lt;li&gt;If the &lt;em&gt;throughput&lt;/em&gt; to a partition or collection
is high enough for long enough,
it also gets split,&lt;sup class="footnote-ref" id="fnref-8"&gt;&lt;a href="#fn-8"&gt;8&lt;/a&gt;&lt;/sup&gt;
possibly with a bias towards keys with higher utilization;
this is a feature of &lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/burst-adaptive-capacity.html#isolate-frequent-access-items"&gt;adaptive capacity&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Partition management is handled entirely by DynamoDB
and is transparent to the user,
but it doesn't happen instantly –
it takes several minutes to allocate new partitions and shuffle things around.&lt;/p&gt;
&lt;p&gt;Since partitions are backed by real computers,
they do have a &lt;a class="anchor" href="#partition-throughput"&gt;throughput limit&lt;/a&gt;.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.Partitions.html"&gt;Partitions and data distribution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/burst-adaptive-capacity.html#isolate-frequent-access-items"&gt;Burst and adaptive capacity # Isolate frequently accessed items&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;small&gt;(blog)&lt;/small&gt; &lt;a class="external" href="https://aws.amazon.com/blogs/database/how-amazon-dynamodb-adaptive-capacity-accommodates-uneven-data-access-patterns-or-why-what-you-know-about-dynamodb-might-be-outdated/"&gt;How Amazon DynamoDB adaptive capacity accommodates uneven data access patterns&lt;/a&gt; (2018)&lt;/li&gt;
&lt;li&gt;&lt;small&gt;(blog)&lt;/small&gt; &lt;a class="external" href="https://aws.amazon.com/blogs/database/part-1-scaling-dynamodb-how-partitions-hot-keys-and-split-for-heat-impact-performance/"&gt;How partitions, hot keys, and split for heat impact performance&lt;/a&gt; (2023)&lt;/li&gt;
&lt;li&gt;&lt;small&gt;(unofficial)&lt;/small&gt; &lt;a class="external" href="https://www.alexdebrie.com/posts/dynamodb-partitions"&gt;Everything you need to know about DynamoDB Partitions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;h2 id="limits"&gt;Limits&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#limits" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Part of DynamoDB's appeal is that
it &lt;strong&gt;scales &amp;quot;infinitely&amp;quot;&lt;/strong&gt; for specific dimensions:
there are no limits on table size or number of items.
However, there are some hard, non-adjustable limits
you will have to take into account when designing your application.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/CheatSheet.html#CheatSheet.ServiceBasics"&gt;Cheat sheet # Service quota basics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/ServiceQuotas.html"&gt;Quotas&lt;/a&gt;
and &lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Constraints.html"&gt;Constraints&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;small&gt;(unofficial)&lt;/small&gt; &lt;a class="external" href="https://www.alexdebrie.com/posts/dynamodb-limits/"&gt;The Three DynamoDB Limits You Need to Know&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;h3 id="partition-throughput"&gt;Partition throughput&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#partition-throughput" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The most important limit is that
on partition throughput (aka &lt;em&gt;capacity&lt;/em&gt;) –
how much data DynamoDB can read from or write
to a &lt;a class="anchor" href="#physical-model-partitions"&gt;partition&lt;/a&gt;
in a given amount of time:&lt;sup class="footnote-ref" id="fnref-9"&gt;&lt;a href="#fn-9"&gt;9&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;1 MB/s for writes&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;24 MB/s for reads&lt;/strong&gt;, &lt;a class="anchor" href="#eventual-consistency"&gt;eventually consistent&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;12 MB/s for reads, strongly consistent&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Throughput measures
&lt;strong&gt;whole items DynamoDB has to access&lt;/strong&gt;,
not the data that goes through the API.
While you can touch single attributes and filter query results,
the consumed capacity is always that
of the &lt;em&gt;whole&lt;/em&gt; items DynamoDB had to read or write.&lt;sup class="footnote-ref" id="fnref-10"&gt;&lt;a href="#fn-10"&gt;10&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Once you reach the limit,
the operation is throttled,
and you can try again later,
ideally with exponential backoff
(the AWS SDK usually takes care of this for you).&lt;/p&gt;
&lt;p&gt;The best way to avoid throttling is
to &lt;strong&gt;distribute the load uniformly across partitions&lt;/strong&gt;
by using a &lt;strong&gt;high-cardinality partition key&lt;/strong&gt;.&lt;sup class="footnote-ref" id="fnref-11"&gt;&lt;a href="#fn-11"&gt;11&lt;/a&gt;&lt;/sup&gt;
Uneven key distribution can create &lt;em&gt;hot partitions&lt;/em&gt;
that suffer from persistent throttling.&lt;/p&gt;
&lt;p&gt;Nowadays, this is less of a problem.
For long-term imbalances,
&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/burst-adaptive-capacity.html#isolate-frequent-access-items"&gt;partition splitting&lt;/a&gt;
should rebalance things over time;
you &amp;quot;might&amp;quot; even end up with a single popular item per partition.
For short-term ones like traffic spikes,
&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/burst-adaptive-capacity.html#burst-capacity"&gt;burst&lt;/a&gt; and &lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/burst-adaptive-capacity.html#adaptive-capacity"&gt;adaptive capacity&lt;/a&gt;
will, &lt;em&gt;on a best effort basis&lt;/em&gt;,
&amp;quot;borrow&amp;quot; capacity above the table limit
and between partitions.
However,
AWS is very non-committal
about their behavior,
and there's nothing you can do
besides increasing traffic gradually,
so &lt;strong&gt;good partition key design&lt;/strong&gt; remains key.&lt;/p&gt;
&lt;p&gt;Of note, while the throughput is fixed,
the other dimensions are not;
this means that you have a trade-off between
&lt;em&gt;how often you access items&lt;/em&gt;,
the &lt;em&gt;number of items&lt;/em&gt;, and &lt;em&gt;item size&lt;/em&gt;;
for example, you can split items into smaller ones
based on how attributes are accessed, aka &lt;em&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/data-modeling-blocks.html#data-modeling-blocks-vertical-partitioning"&gt;vertical partitioning&lt;/a&gt;&lt;/em&gt;.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/read-write-operations.html"&gt;Read and write operations&lt;/a&gt; (capacity unit consumption)&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-design.html"&gt;Partition key design&lt;/a&gt; and
&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-partition-key-uniform-load.html"&gt;Distributing workloads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-sort-keys.html"&gt;Sort key design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;small&gt;(blog)&lt;/small&gt; &lt;a class="external" href="https://aws.amazon.com/blogs/database/choosing-the-right-dynamodb-partition-key/"&gt;Choosing the Right DynamoDB Partition Key&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;h3 id="item-size"&gt;Item size&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#item-size" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Second, &lt;strong&gt;the maximum item size is 400 KB&lt;/strong&gt;,
which ought to be enough for anybody.
You can work around this limit either by &lt;strong&gt;splitting items into parts&lt;/strong&gt;,
or by &lt;strong&gt;putting the data somewhere else&lt;/strong&gt; entirely, like S3,
and keeping only a reference in DynamoDB.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Constraints.html#limits-items"&gt;Constraints # Items&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-use-s3-too.html"&gt;Large items&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;h3 id="page-size"&gt;Page size&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#page-size" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Finally,
&lt;strong&gt;the maximum response size for query and scan operations is 1 MB&lt;/strong&gt;
(a &lt;em&gt;page&lt;/em&gt;).
You can continue from the end of the previous page
by passing the &lt;code&gt;LastEvaluatedKey&lt;/code&gt; response element to subsequent calls,
which is essentially
&lt;a class="internal" href="/query-builder-why#intermission-scrolling-window-queries"&gt;keyset pagination&lt;/a&gt;.
One consequence of this is that,
throughput limit aside,
there's an implicit limit
on how fast you can query the items in a collection,
since the calls are sequentiall.&lt;sup class="footnote-ref" id="fnref-12"&gt;&lt;a href="#fn-12"&gt;12&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Constraints.html#limits-api"&gt;Constraints # API-specific constraints&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Query.Pagination.html"&gt;Paginating query results&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;h2 id="indexes"&gt;Indexes&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#indexes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;As discussed in the &lt;a class="anchor" href="#logical-model-hash-table-of-b-trees"&gt;logical model&lt;/a&gt;,
access not based on primary key is very inefficient.&lt;/p&gt;
&lt;p&gt;Secondary indexes allow queries and scans that use &lt;strong&gt;alternative primary keys&lt;/strong&gt;,
ones composed of different attributes than that of the base table.
Unlike tables,
index sort keys do not have to be unique for a given partition key.
An item that is missing one of the index primary key attributes
will not appear in the index.&lt;/p&gt;
&lt;p&gt;Changes to the table are automatically propagated to any secondary indexes.
Aside from the index and table primary key attributes,
an index can include copies of other attributes
(aka &lt;em&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html#GSI.Projections"&gt;attribute projection&lt;/a&gt;&lt;/em&gt;),
which allows the index to answer queries alone,
without extra reads to the base table.&lt;sup class="footnote-ref" id="fnref-13"&gt;&lt;a href="#fn-13"&gt;13&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.SecondaryIndexes"&gt;Core components # Secondary indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/SecondaryIndexes.html"&gt;Working with indexes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;h3 id="global-secondary-indexes"&gt;Global secondary indexes&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#global-secondary-indexes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;A global secondary index allows using
&lt;strong&gt;different partition &lt;em&gt;and&lt;/em&gt; sort key attributes&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Conceptually,
a global secondary index is &lt;strong&gt;just a table&lt;/strong&gt;:
it has its own &lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html#GSI.ThroughputConsiderations"&gt;separate capacity&lt;/a&gt;,
no limits on size or number of items,
and the same &lt;a class="anchor" href="#partition-throughput"&gt;partition throughput&lt;/a&gt; limits apply.&lt;/p&gt;
&lt;p&gt;Despite &lt;abbr title="Global Secondary Index"&gt;GSI&lt;/abbr&gt;s being updated &lt;a class="anchor" href="#eventual-consistency"&gt;asynchronously&lt;/a&gt;,
an index without enough capacity to process the updates
will cause &lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/gsi-throttling.html"&gt;write throttling&lt;/a&gt;.
To retrieve attributes not in the index,
you have to get them yourself from the table
(&lt;a class="anchor" href="#batch-operations"&gt;batch operations&lt;/a&gt; can speed this up).&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Example&lt;/p&gt;
&lt;p&gt;&lt;small&gt;(from &lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#:~:text=query%20the%20data%20by%20Genre%20and%20Album"&gt;here&lt;/a&gt;)&lt;/small&gt;
Continuing with the music example,
a &lt;abbr title="Global Secondary Index"&gt;GSI&lt;/abbr&gt; with &lt;em&gt;Genre&lt;/em&gt; and &lt;em&gt;Album&lt;/em&gt; as partition and sort keys
would allow you to &lt;em&gt;also&lt;/em&gt; efficiently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;query songs by genre (and, with additional processing, albums by genre)&lt;/li&gt;
&lt;li&gt;query songs by genre and album
(but, since two albums can have the same genre and title,
you might want to group by artist in application code)&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="YAML"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# table Music (partition key: Artist, sort key: Song)&lt;/span&gt;
&lt;span class="nt"&gt;Kyuss&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!btree&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Demon Cleaner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Album&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Welcome To Sky Valley&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="nt"&gt; Genre&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Rock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Space Cadet&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Album&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Welcome To Sky Valley&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="nt"&gt; Genre&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Rock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;1000mods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!btree&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Claws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Genre&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Rock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# has no Album!&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Vidage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Album&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Super Van Vacation&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="nt"&gt; Genre&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Rock&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;Solar Fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!btree&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Air Song&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Album&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Leaving Home&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="nt"&gt; Genre&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Electronic&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="YAML"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# GSI Genres (partition key: Genre, sort key: Album)&lt;/span&gt;
&lt;span class="nt"&gt;Rock&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!btree&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Super Van Vacation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Artist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;1000mods&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="nt"&gt; Song&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Vidage&lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Welcome To Sky Valley&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Artist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Kyuss&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="nt"&gt; Song&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Space Cadet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Welcome To Sky Valley&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Artist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Kyuss&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="nt"&gt; Song&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Demon Cleaner&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;Electronic&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!btree&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Leaving Home&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Artist&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Solar Fields&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="nt"&gt; Song&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Air Song&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/GSI.html"&gt;Global secondary indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-indexes.html"&gt;Best practices for secondary indexes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;h3 id="local-secondary-indexes"&gt;Local secondary indexes&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#local-secondary-indexes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;A local secondary index allows using a &lt;strong&gt;different sort key attribute&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;abbr title="Local Secondary Index"&gt;LSI&lt;/abbr&gt; data is stored together with partition data
(the index is &lt;em&gt;local&lt;/em&gt; to the partition),
so besides the table B‍-‍tree,
each collection has one B‍-‍tree per &lt;abbr title="Local Secondary Index"&gt;LSI&lt;/abbr&gt;.&lt;/p&gt;
&lt;p&gt;This allows strongly consistent reads
and fetching non-projected attributes,
but also &lt;strong&gt;limits collection size to 10 GB&lt;/strong&gt;
and &lt;strong&gt;collection throughput to the &lt;a class="anchor" href="#partition-throughput"&gt;partition limit&lt;/a&gt;&lt;/strong&gt;,
since it prevents further partition splitting
(as each sort key would split the items in a different way).&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Example&lt;/p&gt;
&lt;p&gt;A &lt;abbr title="Local Secondary Index"&gt;LSI&lt;/abbr&gt; with &lt;em&gt;Year&lt;/em&gt; as sort key
would allow you to &lt;em&gt;also&lt;/em&gt; efficiently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;query songs by artist, in chronological order&lt;/li&gt;
&lt;li&gt;query songs by artist and year&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="YAML"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# table Music (partition key: Artist, sort key: Song)&lt;/span&gt;
&lt;span class="nt"&gt;1000mods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!btree&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Claws&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;2014&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Road To Burn&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;2011&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Vidage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;2011&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;Solar Fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!btree&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Sombrero&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Year&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;2011&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="YAML"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# table Music (partition key: Artist, LSI sort key: Year)&lt;/span&gt;
&lt;span class="nt"&gt;1000mods&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!btree&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;2011&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Song&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Vidage&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;2011&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Song&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Road To Burn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;2014&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Song&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Claws&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;span class="nt"&gt;Solar Fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!btree&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;2011&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; Song&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Sombrero&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/LSI.html"&gt;Local secondary indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-indexes.html"&gt;Best practices for secondary indexes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;h2 id="features"&gt;Features&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#features" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Let's look at some of the things DynamoDB can do
besides &lt;abbr title="create, read, update, and delete"&gt;CRUD&lt;/abbr&gt; operations.&lt;/p&gt;
&lt;h3 id="eventual-consistency"&gt;Eventual consistency&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#eventual-consistency" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;So, remember I said partitions are backed by real computers?
I didn't say &lt;em&gt;how many&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;To allow the high-availability magic to happen,
a partition is backed by &lt;a class="external" href="https://aws.amazon.com/blogs/database/part-2-scaling-dynamodb-how-partitions-hot-keys-and-split-for-heat-impact-performance/#:~:text=spread%20across%20three%20nodes%20for%20redundancy"&gt;three nodes&lt;/a&gt;
in separate data centers:&lt;sup class="footnote-ref" id="fnref-14"&gt;&lt;a href="#fn-14"&gt;14&lt;/a&gt;&lt;/sup&gt;
a leader that handles writes
and two asynchronous replicas.&lt;/p&gt;
&lt;p&gt;This explains why there are two kinds of reads:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;strongly consistent&lt;/strong&gt; reads go to the leader,
so you always get the latest data&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;eventually consistent&lt;/strong&gt; reads go to &lt;em&gt;any&lt;/em&gt; node,
so you may get slightly older data,
but if you repeat the read later,
you will &lt;em&gt;&lt;a class="external" href="https://en.wikipedia.org/wiki/Eventual_consistency"&gt;eventually&lt;/a&gt;&lt;/em&gt; get the latest data;
because they use all the available nodes,
they are more efficient, and thus cheaper&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that strongly consistent reads
&lt;strong&gt;do not replace synchronization primitives&lt;/strong&gt;
like &lt;a class="anchor" href="#conditional-writes"&gt;conditional writes&lt;/a&gt;
and &lt;a class="anchor" href="#transactions"&gt;transactions&lt;/a&gt;,
but they can be useful to lower the rate
at which
these operations fail
for highly-contended items.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.ReadConsistency.html"&gt;DynamoDB read consistency&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;h3 id="conditional-writes"&gt;Conditional writes&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#conditional-writes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Write operations can specify a &lt;strong&gt;condition expression&lt;/strong&gt;
that must be true for the write to happen
(e.g. an attribute has a specific value);
if the expression is false,
the write fails.
Condition expressions can refer only to the item being modified.&lt;/p&gt;
&lt;p&gt;Conditional writes are critical for data consistency
and avoiding concurrency &lt;a class="external" href="https://en.wikipedia.org/wiki/Time-of-check_to_time-of-use"&gt;bugs&lt;/a&gt;,
since they are &lt;strong&gt;only the way to run logic server-side&lt;/strong&gt;,
while the item is being modified.
You can use conditional writes
to build higher level abstractions like
optimistic locking, &lt;!-- TODO: link --&gt;
&lt;a class="external" href="https://github.com/awslabs/amazon-dynamodb-lock-client"&gt;distributed locks&lt;/a&gt;,
and atomic counters.&lt;sup class="footnote-ref" id="fnref-15"&gt;&lt;a href="#fn-15"&gt;15&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithItems.html#WorkingWithItems.ConditionalUpdate"&gt;Working with items # Conditional writes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html"&gt;Condition and filter expressions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.ConditionExpressions.html#Expressions.ConditionExpressions.ConditionalExamples"&gt;Condition expressions examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;small&gt;(unofficial)&lt;/small&gt; &lt;a class="external" href="https://www.alexdebrie.com/posts/dynamodb-condition-expressions/"&gt;Understanding DynamoDB Condition Expressions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;h3 id="transactions"&gt;Transactions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#transactions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Transactions allow performing &lt;strong&gt;multiple writes&lt;/strong&gt;
as &lt;strong&gt;a single atomic operation&lt;/strong&gt;,
isolated from other operations;
if two operations attempt to change an item at the same time,
one of them fails.
Transactions can target &lt;strong&gt;up to 100 distinct items&lt;/strong&gt;
in one or more tables in the same region,
and consume twice as much capacity.&lt;/p&gt;
&lt;p&gt;You can use transactions with &lt;a class="anchor" href="#conditional-writes"&gt;condition expressions&lt;/a&gt;
– if a condition fails for one item,
none of the items are modified;
you can also &lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_ConditionCheck.html"&gt;check an item&lt;/a&gt; without modifying it.
Like with single-item writes,
an expression can refer only an individual item
(you can't have a condition about another item in the transaction).&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/transactions.html"&gt;Working with transactions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;h3 id="batch-operations"&gt;Batch operations&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#batch-operations" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Batch operations allow you to
&lt;strong&gt;put/delete up to 25 items&lt;/strong&gt; or &lt;strong&gt;read up to 100 items&lt;/strong&gt;
in a &lt;strong&gt;single request&lt;/strong&gt;, up to 16 MB in total,
more efficiently than using single-item operations.
Batch writes don't support updates or condition expressions.&lt;/p&gt;
&lt;p&gt;The operations in a batch are &lt;strong&gt;independent from one another&lt;/strong&gt;
– some writes may fail, or only some of the read items may be returned
(e.g. if &lt;a class="anchor" href="#partition-throughput"&gt;throughput limits&lt;/a&gt; are reached).&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithItems.html#WorkingWithItems.BatchOperations"&gt;Working with items # Batch operations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchGetItem.html"&gt;BatchGetItem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html"&gt;BatchWriteItem&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;h3 id="streams"&gt;Streams&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#streams" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Streams allow you to &lt;a class="external" href="https://en.wikipedia.org/wiki/Change_data_capture"&gt;capture&lt;/a&gt;
changes to the items in a table in near-real time.
There are two flavors of streams,
&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.html"&gt;DynamoDB Streams&lt;/a&gt;
and &lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/kds.html"&gt;Kinesis Data Streams&lt;/a&gt;,
each with different features and integrations.&lt;/p&gt;
&lt;p&gt;Notable applications of streams are
&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Streams.Lambda.html"&gt;Lambda triggers&lt;/a&gt;
(similar to the ones in relational databases,
except they run &lt;em&gt;after&lt;/em&gt; the change),
replication to places like S3 or Redshift &lt;a class="external" href="https://docs.aws.amazon.com/firehose/latest/dev/basic-deliver.html"&gt;via Firehose&lt;/a&gt;,
and automatic &lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/data-modeling-blocks.html#data-modeling-blocks-ttl-archival"&gt;archival&lt;/a&gt;.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html#HowItWorks.CoreComponents.Streams"&gt;Core components # DynamoDB Streams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/streamsmain.html"&gt;Working with streams&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;small&gt;(unofficial)&lt;/small&gt; &lt;a class="external" href="https://www.alexdebrie.com/bites/dynamodb-streams/"&gt;What you should know about DynamoDB Streams&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;hr /&gt;
&lt;p&gt;Anyway, that's it for now.&lt;/p&gt;
&lt;p&gt;In the next article, we'll have a closer look at core DynamoDB &lt;strong&gt;design patterns&lt;/strong&gt;,
including the fundamental &lt;strong&gt;single-table design&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/dynamodb-model&amp;t=DynamoDB%20crash%20course%3A%20part%202%20%E2%80%93%20data%20model"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=DynamoDB%20crash%20course%3A%20part%202%20%E2%80%93%20data%20model%20https%3A//death.andgravity.com/dynamodb-model"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/dynamodb-model&amp;title=DynamoDB%20crash%20course%3A%20part%202%20%E2%80%93%20data%20model"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/dynamodb-model"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=DynamoDB%20crash%20course%3A%20part%202%20%E2%80%93%20data%20model&amp;url=https%3A//death.andgravity.com/dynamodb-model&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;!-- TODO: make this an aside or a bonus

Depending on how you structure your data,
a DynamoDB table can correspond to either a single table or a whole database
in a relational database system.

Partitions are somewhat analogous to [shards],
but unlike with most relational databases,
partitions are built *into* the database,
rather than *on top* of it.

A DynamoDB item usually corresponds to a row in a relational database system.

A DynamoDB attribute corresponds to a field in a relational database system.

[shards]: https://en.wikipedia.org/wiki/Shard_(database_architecture)

--&gt;

&lt;!-- TODO/IDEA: cheatsheet table a la big o cheatsheet

(one for table / partition / item )
(one for crud / batch / query/scan / transactions)

like so

| | read (ec) | read (sc) | write |
|-|-|-|-|
| units/s | 3,000 | 3,000 | 1,000 |
| unit size | 4 KB/unit | 4 KB/unit | 1 KB/unit |
| cost | 0.5 unit | 1 unit | 1 unit |
| total | 24 MB/s | 12 MB/s | 1 MB/s |

```
throughput = units/s * unit size * cost
```

--&gt;

&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;Here, &amp;quot;collection&amp;quot; just means a &amp;quot;group of things&amp;quot;. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;Ignore the names for now, it'll make sense in a bit. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-3"&gt;&lt;p&gt;It is also possible to have
a table with only a partition key and no sort key,
but you can think of that like
a degenerate case where the sort key is a constant value,
and thus each partition key can have only one item. &lt;a href="#fnref-3" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-4"&gt;&lt;p&gt;Yes, an &lt;em&gt;item collection&lt;/em&gt; is different from a &amp;quot;collection of items&amp;quot;.
Don't look at me, I didn't pick the names. ಠ_ಠ &lt;a href="#fnref-4" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-5"&gt;&lt;p&gt;&lt;a class="anchor" href="#indexes"&gt;Indexes&lt;/a&gt;, which we'll discuss later,
offer an escape hatch to this. &lt;a href="#fnref-5" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-6"&gt;&lt;p&gt;A quick rant on naming.
With a hash table,
you would say &amp;quot;hash table key&amp;quot;,
or maybe even &amp;quot;item key&amp;quot;;
you would &lt;em&gt;not&lt;/em&gt; say &amp;quot;bucket key&amp;quot;,
since that's a low level detail,
and also a bucket can have &lt;em&gt;multiple&lt;/em&gt; keys.
You know, like in DynamoDB.&lt;/p&gt;
&lt;p&gt;THEN WHY IS IT CALLED A PARTITION KEY &lt;!-- ...partition*ing* key, maybe? --&gt; &lt;a href="#fnref-6" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-7"&gt;&lt;p&gt;You have to admit this is a great explanation.
Surely you'd find it in the docs,
and not burried in a &lt;a class="external" href="https://aws.amazon.com/blogs/database/part-1-scaling-dynamodb-how-partitions-hot-keys-and-split-for-heat-impact-performance/"&gt;random blog post&lt;/a&gt;
published four years after the feature was announced,
&lt;a class="external" href="https://aws.amazon.com/blogs/database/how-amazon-dynamodb-adaptive-capacity-accommodates-uneven-data-access-patterns-or-why-what-you-know-about-dynamodb-might-be-outdated/"&gt;also in a blog post&lt;/a&gt;,
and which itself explains more
than the official documentation does &lt;a class="external" href="https://web.archive.org/web/20260210084635/https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/burst-adaptive-capacity.html#isolate-frequent-access-items"&gt;to this day&lt;/a&gt;,
seven years later! &lt;a href="#fnref-7" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-8"&gt;&lt;p&gt;Assuming the table has enough configured throughput. &lt;a href="#fnref-8" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-9"&gt;&lt;p&gt;Converted to normal people units for your convenience.
DynamoDB uses its own &lt;em&gt;capacity units&lt;/em&gt;
as a convoluted way of saying that for accounting purposes,
item size is rounded up to 4 KB (1 &lt;abbr title="Read Capacity Units"&gt;RCU&lt;/abbr&gt;) for reads
and 1 KB (1 &lt;abbr title="Write Capacity Units"&gt;WCU&lt;/abbr&gt;) for writes.
This is presumably because the size of a capacity unit
&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/DocumentHistory.html#:~:text=4%20KB%20read%20capacity%20unit%20size"&gt;can increase&lt;/a&gt; over time. &lt;a href="#fnref-9" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-10"&gt;&lt;p&gt;Yes, this includes just counting them. &lt;a href="#fnref-10" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-11"&gt;&lt;p&gt;If there is no good natural partition key,
you can make one by sharding a low-cardinality attribute, &lt;!-- TODO: link --&gt;
which we'll cover in the next article. &lt;a href="#fnref-11" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-12"&gt;&lt;p&gt;You could make it twice as fast by querying from both ends,
but probably no faster,
since for &lt;a class="internal" href="/pwned#binary-skipping"&gt;binary search&lt;/a&gt;
you'd need to jump in the middle of two sort keys.
Unsurprisingly,
we'll look at a potential solution in the next article. &lt;a href="#fnref-12" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-13"&gt;&lt;p&gt;This is the same as a &lt;a class="external" href="https://en.wikipedia.org/wiki/Database_index#Covering_index"&gt;covering index&lt;/a&gt; in relational databases. &lt;a href="#fnref-13" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-14"&gt;&lt;p&gt;Or better said, &lt;em&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html#ddb-intro-resilience"&gt;availability zones&lt;/a&gt;&lt;/em&gt;. &lt;a href="#fnref-14" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-15"&gt;&lt;p&gt;Although you can also use update expressions for &lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/WorkingWithItems.html#WorkingWithItems.AtomicCounters"&gt;atomic counters&lt;/a&gt;. &lt;a href="#fnref-15" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/dynamodb-model" rel="alternate"/>
    <summary>This is part two of a series covering core DynamoDB concepts, from philosophy all the way to single-table design. The goal is to get you to understand idiomatic usage and trade-offs in under an hour. Today, we're looking at the DynamoDB data model – what the main abstractions are, what you can do with them, and how they scale.</summary>
    <published>2026-02-10T10:35:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/dynamodb">
    <id>https://death.andgravity.com/dynamodb</id>
    <title>DynamoDB crash course: part 1 – philosophy</title>
    <updated>2026-01-27T17:00:00+00:00</updated>
    <content type="html">&lt;p&gt;This is part one of a series
covering core &lt;strong&gt;DynamoDB&lt;/strong&gt; concepts and patterns,
from the data model and features all the way up to &lt;strong&gt;single-table design&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;The goal is to get you
to understand what &lt;strong&gt;idiomatic usage&lt;/strong&gt; looks like and
what the &lt;strong&gt;trade-offs&lt;/strong&gt; are
in under an hour,
providing entry points to detailed documentation.&lt;/p&gt;
&lt;p&gt;(Don't get me wrong, the &lt;abbr title="Amazon Web Services"&gt;AWS&lt;/abbr&gt; documentation is comprehensive,
but can be quite complex,
and DynamoDB being a relatively low level product
with lots of features added over the years
doesn't really help with that.)&lt;/p&gt;
&lt;p&gt;Today, we're looking at
&lt;strong&gt;what DynamoDB is&lt;/strong&gt; and
&lt;strong&gt;why it is the way it is&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="what-is-dynamodb"&gt;What is DynamoDB?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-dynamodb" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Quoting &lt;a class="external" href="https://en.wikipedia.org/wiki/Amazon_DynamoDB"&gt;Wikipedia&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Amazon DynamoDB&lt;/strong&gt; is a managed NoSQL database service provided by &lt;abbr title="Amazon Web Services"&gt;AWS&lt;/abbr&gt;.
It supports key-value and document data structures and is designed to handle
a wide range of applications requiring scalability and performance.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;p&gt;This definition should suffice for now;
for a more detailed refresher, see:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Introduction.html"&gt;What is Amazon DynamoDB?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/HowItWorks.CoreComponents.html"&gt;Core components&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;p&gt;The DynamoDB data model can be summarized as follows:&lt;/p&gt;
&lt;p&gt;&lt;em&gt;A &lt;strong&gt;table&lt;/strong&gt; is a collection of items,
and an &lt;strong&gt;item&lt;/strong&gt; is a collection of attributes.
Items are uniquely identified by two attributes,
the &lt;strong&gt;partition key&lt;/strong&gt; and the &lt;strong&gt;sort key&lt;/strong&gt;.
The partition key determines where (i.e. on what computer) an item is stored.
The sort key is used to get ordered ranges of items from a specific partition.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;That's is, that's the whole data model.
Sure, there's indexes and transactions and other features,
but at its core, this is it.
Put another way:&lt;/p&gt;
&lt;p&gt;A DynamoDB table is &lt;strong&gt;a &lt;a class="external" href="https://en.wikipedia.org/wiki/Hash_table"&gt;hash table&lt;/a&gt; of &lt;a class="external" href="https://en.wikipedia.org/wiki/B-tree"&gt;B-trees&lt;/a&gt;&lt;/strong&gt;&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt; –
partition keys are hash table keys, and sort keys are B-tree keys.
Because of this,
&lt;strong&gt;any access not based on partition and sort key is expensive&lt;/strong&gt;,
since you end up doing a full table scan.&lt;/p&gt;
&lt;!-- TODO simple db footnote --&gt;

&lt;p&gt;&lt;a id="python-mockup"&gt;&lt;/a&gt;
If you were to implement this model in Python, it'd look something like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="IPython"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;collections&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sortedcontainers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;SortedDict&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pk_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sk_name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pk_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pk_name&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_sk_name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sk_name&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_partitions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SortedDict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pk_name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_sk_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;old_item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_partitions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="n"&gt;old_item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;old_item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_partitions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;min_sk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_sk&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inclusive&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# in the real DynamoDB, this operation is paginated&lt;/span&gt;
        &lt;span class="n"&gt;partition&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_partitions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;sk&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;irange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;min_sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inclusive&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# in the real DynamoDB, this operation is paginated&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;partition&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_partitions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;update_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sk&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_pk_name&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_sk_name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;old_item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_partitions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
        &lt;span class="n"&gt;old_item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;delete_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_partitions&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;pk&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;sk&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Table&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Artist&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Song&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Artist&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;1000mods&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Song&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Vidage&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Year&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2011&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Artist&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;1000mods&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Song&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Claws&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Album&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Vultures&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put_item&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Artist&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Kyuss&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Song&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Space Cadet&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;1000mods&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Claws&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;{&amp;#39;Artist&amp;#39;: &amp;#39;1000mods&amp;#39;, &amp;#39;Song&amp;#39;: &amp;#39;Claws&amp;#39;, &amp;#39;Album&amp;#39;: &amp;#39;Vultures&amp;#39;}&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Song&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;1000mods&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;Claws&amp;#39;, &amp;#39;Vidage&amp;#39;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Song&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;table&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;1000mods&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Loose&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;Vidage&amp;#39;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="philosophy"&gt;Philosophy&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#philosophy" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;One can't help but feel this kind of simplicity would be severely limiting.&lt;/p&gt;
&lt;p&gt;A consequence of DynamoDB being this low level is that,
unlike with most relational databases,
&lt;strong&gt;query planning&lt;/strong&gt; and sometimes &lt;strong&gt;index management&lt;/strong&gt;
happen at the &lt;strong&gt;application level&lt;/strong&gt;,
i.e. you have to do them yourself in code.
In turn, this means
you need to have a clear, &lt;strong&gt;upfront understanding&lt;/strong&gt;
of your application's &lt;strong&gt;access patterns&lt;/strong&gt;,
and accept that changes in access patterns
will require changes to the application.&lt;/p&gt;
&lt;p&gt;In return, you get
a &lt;strong&gt;fully managed&lt;/strong&gt;,
&lt;strong&gt;highly-available&lt;/strong&gt; database
that &lt;strong&gt;scales infinitely&lt;/strong&gt;:&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;
there are no servers to take care of,
there's almost no downtime, and
there are no limits on table size or the number of items in a table;
where limits do exist, they are clearly documented,
allowing for &lt;strong&gt;predictable performance&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This highlights an intentional design decision
that is essentially DynamoDB's main proposition to you as its user:
&lt;strong&gt;data modeling complexity
is always preferable to complexity coming from
infrastructure maintenance, availability, and scalability&lt;/strong&gt;
(what &lt;abbr title="Amazon Web Services"&gt;AWS&lt;/abbr&gt; marketing calls &amp;quot;undifferentiated heavy lifting&amp;quot;).&lt;/p&gt;
&lt;p&gt;To help manage this complexity,
a number of design patterns have arisen,
covered extensively by the official documentation,
and which we'll discuss in a future article.
Even so, the toll can be heavy –
by &lt;abbr title="Amazon Web Services"&gt;AWS&lt;/abbr&gt;'s &lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/data-modeling-foundations.html#data-modeling-foundations-single"&gt;own admission&lt;/a&gt;,
the prime disadvantage of single table design,
the fundamental design pattern, is that:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[the] learning curve can be steep due to paradoxical design compared to relational databases&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As &lt;a class="external" href="https://www.trek10.com/blog/dynamodb-single-table-relational-modeling/"&gt;this walkthrough&lt;/a&gt; puts it:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;a well-optimized single-table DynamoDB layout looks more like machine code than a simple spreadsheet&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;...which, admittedly, sounds pretty cool,
but also why would I want that?
After all, most useful programming most people do
is one or two abstraction levels above assembly,
itself one over machine code.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/bp-general-nosql-design.html"&gt;NoSQL design&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;small&gt;(unofficial)&lt;/small&gt; &lt;a class="external" href="https://www.alexdebrie.com/posts/dynamodb-limits/#the-dynamodb-philosophy-of-limits"&gt;# The DynamoDB philosophy of limits&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;h2 id="a-bit-of-history"&gt;A bit of history&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#a-bit-of-history" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Perhaps it's worth having a look at where DynamoDB comes from.&lt;/p&gt;
&lt;p&gt;Amazon.com used Oracle databases for a long time.
To cope with the increasing scale,
they first adopted a database-per-service model,
and then sharding,
with all the architectural and operational overhead
you would expect.
At its 2017 peak
(five years after DynamoDB was released in &lt;abbr title="Amazon Web Services"&gt;AWS&lt;/abbr&gt;,
and over ten years after some version of it was available internally),
they &lt;a class="external" href="https://youtu.be/vPF9l9OmiXE?t=449"&gt;still had&lt;/a&gt;
75 PB of data
in nearly 7500 Oracle databases,
owned by 100+ teams, with thousands of applications,
&lt;em&gt;for &lt;abbr title="Online Transaction Processing"&gt;OLTP&lt;/abbr&gt; workloads alone&lt;/em&gt;.
That sounds pretty traumatic
–
it was definitely bad enough to
&lt;a class="external" href="https://news.ycombinator.com/item?id=42677767"&gt;allegedly&lt;/a&gt; ban &lt;abbr title="Online Transaction Processing"&gt;OLTP&lt;/abbr&gt; relational databases internally,
and require that teams get VP approval to use one.&lt;/p&gt;
&lt;p&gt;Yeah, coming from &lt;em&gt;that&lt;/em&gt;,
it's hard to argue DynamoDB adds complexity.&lt;/p&gt;
&lt;p&gt;That is not to say relational databases
cannot be as scalable as DynamoDB,
just that Amazon doesn't belive in them
–
&lt;a class="external" href="https://en.wikipedia.org/wiki/Distributed_SQL"&gt;distributed SQL&lt;/a&gt; databases like Google's &lt;a class="external" href="https://en.wikipedia.org/wiki/Spanner_(database)"&gt;Spanner&lt;/a&gt; and &lt;a class="external" href="https://en.wikipedia.org/wiki/CockroachDB"&gt;CockroachDB&lt;/a&gt;
have existed for a while now,
and even &lt;abbr title="Amazon Web Services"&gt;AWS&lt;/abbr&gt; seems to be &lt;a class="external" href="https://aws.amazon.com/blogs/database/introducing-amazon-aurora-dsql/"&gt;warming up&lt;/a&gt; to the idea.&lt;/p&gt;
&lt;p&gt;This might also explain why the design patterns
are so slow to make their way into SDKs,
or even better, into DynamoDB itself;
when you have so many applications and so many experienced teams,
the cost of yet another bit of code to do partition key sharding
just isn't that great.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;See also&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;small&gt;(paper)&lt;/small&gt; &lt;a class="external" href="https://www.usenix.org/system/files/atc22-elhemali.pdf"&gt;Amazon DynamoDB: A Scalable, Predictably Performant, and Fully Managed NoSQL Database Service&lt;/a&gt; (2022)&lt;/li&gt;
&lt;li&gt;&lt;small&gt;(paper)&lt;/small&gt; &lt;a class="external" href="https://assets.amazon.science/ac/1d/eb50c4064c538c8ac440ce6a1d91/dynamo-amazons-highly-available-key-value-store.pdf"&gt;Dynamo: Amazon’s Highly Available Key-value Store&lt;/a&gt; (2007)&lt;/li&gt;
&lt;/ul&gt;
&lt;/section&gt;
&lt;hr /&gt;
&lt;p&gt;Anyway, that's it for now.&lt;/p&gt;
&lt;p&gt;In the next article, we'll have a closer look at the DynamoDB &lt;strong&gt;data model&lt;/strong&gt; and &lt;strong&gt;features&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/dynamodb&amp;t=DynamoDB%20crash%20course%3A%20part%201%20%E2%80%93%20philosophy"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=DynamoDB%20crash%20course%3A%20part%201%20%E2%80%93%20philosophy%20https%3A//death.andgravity.com/dynamodb"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/dynamodb&amp;title=DynamoDB%20crash%20course%3A%20part%201%20%E2%80%93%20philosophy"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/dynamodb"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=DynamoDB%20crash%20course%3A%20part%201%20%E2%80%93%20philosophy&amp;url=https%3A//death.andgravity.com/dynamodb&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;Or any other sorted data structure that allows
fast searches, sequential access, insertions, and deletions. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;As the saying goes, the cloud is just someone else's computers.
Here, &amp;quot;infinitely&amp;quot; means it scales horizontally,
and you'll run out of money before &lt;abbr title="Amazon Web Services"&gt;AWS&lt;/abbr&gt; runs out of computers. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/dynamodb" rel="alternate"/>
    <summary>This is part one of a series covering core DynamoDB concepts and patterns, all the way up to single-table design; the goal is to get you to understand idiomatic usage and trade-offs in under an hour. Today, we're looking at what DynamoDB is and why it is that way.</summary>
    <published>2026-01-23T08:40:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/reader-3-20">
    <id>https://death.andgravity.com/reader-3-20</id>
    <title>reader 3.20 released – we're so back</title>
    <updated>2025-12-01T18:00:00+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;I'm happy to announce version 3.20 of &lt;strong&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/strong&gt;, a Python feed reader library.&lt;/p&gt;
&lt;h2 id="what-s-new"&gt;What's new?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-new" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Here are the highlights since &lt;a class="internal" href="/reader-3-16"&gt;reader 3.16&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="web-app-re-design"&gt;Web app re-design&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#web-app-re-design" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Earlier this year,
I started a web app &lt;a class="external" href="https://github.com/lemon24/reader/issues/318"&gt;re-design&lt;/a&gt; based on &lt;a class="external" href="https://htmx.org/"&gt;htmx&lt;/a&gt; and &lt;a class="external" href="https://getbootstrap.com/"&gt;Bootstrap&lt;/a&gt;.
I only had time for the main page,
but it turned out pretty nice,
and I've been using it ever since.
What is &lt;em&gt;new&lt;/em&gt; is that I now have free time and some plans;
&lt;strong&gt;watch this space&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Here are some screenshots; it's not much, but it's honest work:&lt;/p&gt;
&lt;div class="columns"&gt;&lt;div class="column col-sm-6 col-xs-12"&gt;

&lt;figure class="figure"&gt;
&lt;img class="img-responsive" src="/_file/reader-3-20/entries-v2-dark.png" alt="main page (dark mode)" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
main page (dark mode)
&lt;/figcaption&gt;
&lt;/figure&gt;



&lt;/div&gt;
&lt;div class="column col-sm-6 col-xs-12"&gt;

&lt;figure class="figure"&gt;
&lt;img class="img-responsive" src="/_file/reader-3-20/entries-v2-filters-light.png" alt="main page – more filters (light mode)" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
main page – more filters (light mode)
&lt;/figcaption&gt;
&lt;/figure&gt;



&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;&lt;em&gt;htmx? ugh, how original&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Unsurprisingly, htmx is perfect for this kind of application.
As CEO of htmx, I can confirm that all the allegations are true –
it is indeed a pleasure to use,
not to mention a huge improvement
over my home-grown, half-assed JavaScript forms &lt;a class="external" href="https://github.com/lemon24/reader/blob/3.20/src/reader/_app/static/controls.js"&gt;framework&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;isn't Bootstrap from like 2010 or something? also, how original&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I've tried many modern popular CSS frameworks
(since forgotten, for my own sanity),
and it turns out tab-navigable form controls are a hard,
unsolved problem in 2025.
Thankfully, if you're willing to time travel (to the future, surely!),
Bootstrap comes with excellent documentation, built-in accessibility,
and a comprehensive component library.&lt;/p&gt;
&lt;h3 id="entry-deduplication"&gt;Entry deduplication&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#entry-deduplication" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;I may have accidentally rewritten the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/plugins.html#reader-entry-dedupe"&gt;entry_dedupe&lt;/a&gt; plugin. 😅&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Sometimes, the id of some or all the entries in a feed changes
(e.g. from &lt;code&gt;example.com/123&lt;/code&gt; to &lt;code&gt;example.com/entry-title&lt;/code&gt;),
causing each entry to appear twice.
&lt;em&gt;entry_dedupe&lt;/em&gt; fixes this
by copying user attributes to the new entry
and deleting the old one.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;What started as a quick and dirty plugin now has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lots of new, &lt;em&gt;data-driven&lt;/em&gt; heuristics to find potential duplicates&lt;ul&gt;
&lt;li&gt;by title (had this one already)&lt;/li&gt;
&lt;li&gt;by link&lt;/li&gt;
&lt;li&gt;by published timestamp&lt;/li&gt;
&lt;li&gt;by title with common prefixes removed&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;better approximate content matching&lt;/li&gt;
&lt;li&gt;an extensive test suite&lt;/li&gt;
&lt;li&gt;extensive internal documentation (&lt;a class="external" href="https://github.com/lemon24/reader/blob/3.20/src/reader/plugins/entry_dedupe.py#L232-L370"&gt;like this&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last one is pretty important:
the old logic was data-driven too,
but because I didn't document my &lt;em&gt;entire&lt;/em&gt; reasoning,
I wasted more time than I'd like to admit
on a useless title similarity heuristic.
It's a trope that you should write code such that others can understand it,
and &amp;quot;others&amp;quot; includes you six months from now;
I don't know about six months, but four years will definitely do it.
(If that sounds like an interesting story and you'd like to hear more,
&lt;a class="internal" href="/about#contact"&gt;drop me a line&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;It's not all bad, though –
a side-effect of better content matching
is that image &lt;code&gt;alt&lt;/code&gt; and &lt;code&gt;title&lt;/code&gt; attributes
are also included in the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/guide.html#fts"&gt;full-text search&lt;/a&gt; index now,
so you can search xkcd comics by title text.&lt;/p&gt;
&lt;h3 id="project-infrastructure"&gt;Project infrastructure&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#project-infrastructure" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;I've taken some time to modernize the project infrastructure; stuff like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Use GitHub Actions to publish releases to PyPI.&lt;/li&gt;
&lt;li&gt;Use dependency groups instead of extras for development dependencies.&lt;/li&gt;
&lt;li&gt;Rewrite &lt;a class="external" href="https://reader.readthedocs.io/en/latest/contributing.html"&gt;contributor documentation&lt;/a&gt; and &lt;a class="external" href="https://github.com/lemon24/reader/blob/3.20/run.sh"&gt;run.sh&lt;/a&gt; to optimize for readability.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(I'll admit the &lt;code&gt;uv&lt;/code&gt; hype is quite strong, but the vanilla tooling seems fine, for now.)&lt;/p&gt;
&lt;h3 id="read-only-reader"&gt;Read-only Reader&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#read-only-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;It's now possible to prevent write operations on a &lt;a class="external" href="https://github.com/lemon24/reader"&gt;Reader&lt;/a&gt; object
through the &lt;code&gt;read_only&lt;/code&gt; &lt;a class="external" href="https://reader.readthedocs.io/en/latest/api.html#reader.make_reader"&gt;make_reader()&lt;/a&gt;  argument.
Thanks to Roman Milko for the pull request!&lt;/p&gt;
&lt;h3 id="python-versions"&gt;Python versions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#python-versions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; 3.17 added support for PyPy 3.11.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; 3.18 dropped support for Python 3.10.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; 3.19 added support for Python 3.14.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.
For more details, see the full &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-3-20"&gt;changelog&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Want to contribute?&lt;/strong&gt;
Check out the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/contributing.html"&gt;docs&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/dev.html#roadmap"&gt;roadmap&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/reader-3-20&amp;t=reader%203.20%20released%20%E2%80%93%20we%27re%20so%20back"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=reader%203.20%20released%20%E2%80%93%20we%27re%20so%20back%20https%3A//death.andgravity.com/reader-3-20"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/reader-3-20&amp;title=reader%203.20%20released%20%E2%80%93%20we%27re%20so%20back"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/reader-3-20"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=reader%203.20%20released%20%E2%80%93%20we%27re%20so%20back&amp;url=https%3A//death.andgravity.com/reader-3-20&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;h2 id="what-is-reader"&gt;What is &lt;em&gt;reader&lt;/em&gt;?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;reader&lt;/strong&gt; takes care
of the core functionality required by a feed reader,
so you can focus on what makes &lt;strong&gt;yours&lt;/strong&gt; different.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-2-0/reader.png" alt="reader in action" /&gt;
&lt;em&gt;reader&lt;/em&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retrieve, store, and manage &lt;strong&gt;Atom&lt;/strong&gt;, &lt;strong&gt;RSS&lt;/strong&gt;, and &lt;strong&gt;JSON&lt;/strong&gt; feeds&lt;/li&gt;
&lt;li&gt;mark articles as read or important&lt;/li&gt;
&lt;li&gt;add arbitrary tags/metadata to feeds and articles&lt;/li&gt;
&lt;li&gt;filter feeds and articles&lt;/li&gt;
&lt;li&gt;full-text search articles&lt;/li&gt;
&lt;li&gt;get statistics on feed and user activity&lt;/li&gt;
&lt;li&gt;write plugins to extend its functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...all these with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stable, clearly documented API&lt;/li&gt;
&lt;li&gt;excellent test coverage&lt;/li&gt;
&lt;li&gt;fully typed Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find out more, check out the &lt;a class="external" href="https://github.com/lemon24/reader"&gt;GitHub repo&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/"&gt;docs&lt;/a&gt;,
or give the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/tutorial.html"&gt;tutorial&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2 id="why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-use-a-feed-reader-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Have you been unhappy with existing feed readers and wanted to make your own, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never knew where to start?&lt;/li&gt;
&lt;li&gt;it seemed like too much work?&lt;/li&gt;
&lt;li&gt;you don't like writing backend code?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you already working with &lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/"&gt;feedparser&lt;/a&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;want an easier way to store, filter, sort and search feeds and entries?&lt;/li&gt;
&lt;li&gt;want to get back type-annotated objects instead of dicts?&lt;/li&gt;
&lt;li&gt;want to restrict or deny file-system access?&lt;/li&gt;
&lt;li&gt;want to change the way feeds are retrieved by using &lt;a class="external" href="https://requests.readthedocs.io"&gt;Requests&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to also support &lt;a class="external" href="https://jsonfeed.org/"&gt;JSON Feed&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to support custom information sources?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... while still supporting all the feed types feedparser does?&lt;/p&gt;
&lt;p&gt;If you answered yes to any of the above, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;h2 id="the-reader-philosophy"&gt;The &lt;em&gt;reader&lt;/em&gt; philosophy&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-reader-philosophy" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is a library&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is for the long term&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is extensible&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is stable (within reason)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is simple to use; API matters&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; features work well together&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is tested&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is documented&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; has minimal dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="why-make-your-own-feed-reader"&gt;Why make your own feed reader?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-make-your-own-feed-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;have full control over your data&lt;/li&gt;
&lt;li&gt;control what features it has or doesn't have&lt;/li&gt;
&lt;li&gt;decide how much you pay for it&lt;/li&gt;
&lt;li&gt;make sure it doesn't get closed while you're still using it&lt;/li&gt;
&lt;li&gt;really, it's &lt;a class="external" href="https://rachelbythebay.com/w/2011/10/26/fred/"&gt;easier than you think&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously, this may not be your cup of tea, but if it is, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
</content>
    <link href="https://death.andgravity.com/reader-3-20" rel="alternate"/>
    <published>2025-11-28T18:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/asyncio-thread-runner">
    <id>https://death.andgravity.com/asyncio-thread-runner</id>
    <title>Announcing asyncio-thread-runner: you can have a little async (as a treat)</title>
    <updated>2025-08-22T12:00:00+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;Back in 2023, we made a thing for
&lt;a class="internal" href="/asyncio-bridge"&gt;running async code from sync code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I'm happy to announce that you can now install it from &lt;a class="external" href="https://pypi.org/project/asyncio-thread-runner"&gt;PyPI&lt;/a&gt;,
and read the
documented, tested, type-annotated code
on &lt;a class="external" href="https://github.com/lemon24/asyncio-thread-runner"&gt;GitHub&lt;/a&gt;! &lt;a class="external" href="https://github.com/lemon24/asyncio-thread-runner"&gt;⭐️&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Without further ado:&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;&lt;a class="external" href="https://pypi.org/project/asyncio-thread-runner"&gt;asyncio-thread-runner&lt;/a&gt;&lt;/strong&gt; allows you to run async code from sync code.&lt;/p&gt;
&lt;p&gt;This is useful when you're doing some sync stuff, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;you also need to do some async stuff, &lt;strong&gt;without&lt;/strong&gt; making &lt;strong&gt;everything async&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;maybe the sync stuff is an existing application&lt;/li&gt;
&lt;li&gt;maybe you still want to use your favorite sync library&lt;/li&gt;
&lt;li&gt;or maybe you need just a little async, without having to pay the full price&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Features:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;unlike &lt;a class="external" href="https://docs.python.org/3/library/asyncio-runner.html#asyncio.run"&gt;asyncio.run()&lt;/a&gt;, it provides a &lt;strong&gt;long-lived event loop&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;unlike &lt;a class="external" href="https://docs.python.org/3/library/asyncio-runner.html#asyncio.Runner"&gt;asyncio.Runner&lt;/a&gt;, you can use it from &lt;strong&gt;multiple threads&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;it allows you to use &lt;strong&gt;async context managers&lt;/strong&gt; and &lt;strong&gt;iterables&lt;/strong&gt; from sync code&lt;/li&gt;
&lt;li&gt;check out &lt;a class="external" href="https://death.andgravity.com/asyncio-bridge"&gt;this article&lt;/a&gt; for why these are useful&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Usage:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;asyncio-thread-runner
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;     &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;asyncio_thread_runner&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadRunner&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ThreadRunner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="mi"&gt;4&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Annotated example:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;aiohttp&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;asyncio_thread_runner&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;ThreadRunner&lt;/span&gt;

&lt;span class="c1"&gt;# you can use ThreadRunner as a context manager,&lt;/span&gt;
&lt;span class="c1"&gt;# or call runner.close() when you&amp;#39;re done with it&lt;/span&gt;
&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ThreadRunner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="c1"&gt;# aiohttp.ClientSession() should be used as an async context manager,&lt;/span&gt;
    &lt;span class="c1"&gt;# enter_context() will exit the context on runner shutdown;&lt;/span&gt;
    &lt;span class="c1"&gt;# because instantiating ClientSession requires a running event loop,&lt;/span&gt;
    &lt;span class="c1"&gt;# we pass it as a factory instead of calling it in the main thread&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enter_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# session.get() returns an async context manager...&lt;/span&gt;
    &lt;span class="n"&gt;request&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://death.andgravity.com/asyncio-bridge&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# which we turn into a normal one with wrap_context()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wrap_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

        &lt;span class="c1"&gt;# response.content is an async iterator;&lt;/span&gt;
        &lt;span class="c1"&gt;# we turn it into a normal iterator with wrap_iter()&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wrap_iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# &amp;quot;got 935 lines&amp;quot;&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;got&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;lines&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/asyncio-thread-runner&amp;t=Announcing%20asyncio-thread-runner%3A%20you%20can%20have%20a%20little%20async%20%28as%C2%A0a%C2%A0treat%29"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Announcing%20asyncio-thread-runner%3A%20you%20can%20have%20a%20little%20async%20%28as%C2%A0a%C2%A0treat%29%20https%3A//death.andgravity.com/asyncio-thread-runner"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/asyncio-thread-runner&amp;title=Announcing%20asyncio-thread-runner%3A%20you%20can%20have%20a%20little%20async%20%28as%C2%A0a%C2%A0treat%29"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/asyncio-thread-runner"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Announcing%20asyncio-thread-runner%3A%20you%20can%20have%20a%20little%20async%20%28as%C2%A0a%C2%A0treat%29&amp;url=https%3A//death.andgravity.com/asyncio-thread-runner&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

</content>
    <link href="https://death.andgravity.com/asyncio-thread-runner" rel="alternate"/>
    <summary>Back in 2023, we made a thing for running async code from sync code. I'm happy to announce that it is now available on PyPI as asyncio-thread-runner.</summary>
    <published>2025-08-22T12:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/over-composition">
    <id>https://death.andgravity.com/over-composition</id>
    <title>Inheritance over composition, sometimes</title>
    <updated>2025-07-15T20:00:00+00:00</updated>
    <content type="html">&lt;p&gt;In &lt;a class="internal" href="/ptpe"&gt;Process​Thread​Pool​Executor: when I‍/‍O becomes CPU-bound&lt;/a&gt;,
we built a hybrid &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html"&gt;concurrent.​futures&lt;/a&gt; executor
that runs tasks in multiple threads on all available CPUs,
bypassing Python's global interpreter lock.&lt;/p&gt;
&lt;p&gt;Here's some interesting reader feedback:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Currently, the &lt;strong&gt;code is complex due to subclassing&lt;/strong&gt;
and many layers of delegation.
Could this solution be implemented &lt;strong&gt;using only functions&lt;/strong&gt;, no classes?
Intuitively I feel &lt;strong&gt;classes would be hell to debug&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Since a lot of advanced beginners struggle with structuring code,
we'll implement the same executor
using &lt;strong&gt;inheritance&lt;/strong&gt;, &lt;strong&gt;composition&lt;/strong&gt;, and &lt;strong&gt;functions&lt;/strong&gt; only,
compare the solutions,
and reach some interesting conclusions.
Consider this a worked example.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;Today we're focusing on code &lt;em&gt;structure&lt;/em&gt;.
While not required,
reading the &lt;a class="internal" href="/ptpe"&gt;original article&lt;/a&gt; will give you a better idea
of &lt;em&gt;why&lt;/em&gt; the code does what it does.&lt;/p&gt;
&lt;/section&gt;
&lt;details class="toc" open&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#requirements"&gt;Requirements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#concurrent-futures"&gt;concurrent.futures&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#three-solutions"&gt;Three solutions&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#inheritance"&gt;Inheritance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#composition"&gt;Composition&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#functions"&gt;Functions&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#comparison"&gt;Comparison&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#composition-over-inheritance"&gt;Composition over inheritance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#forward-compatibility"&gt;Forward compatibility&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#global-state"&gt;Global state&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#complexity"&gt;Complexity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#debugging"&gt;Debugging&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#try-it-out"&gt;Try it out&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="requirements"&gt;Requirements&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#requirements" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Before we delve into the code,
we should have some understanding of what we're building.
The orginal article &lt;a class="internal" href="/ptpe#why-not-both"&gt;sets out&lt;/a&gt;
the following functional requirements:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Implement the &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor"&gt;Executor&lt;/a&gt; interface;
we want a drop-in replacement
for existing &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html"&gt;concurrent.​futures&lt;/a&gt; executors,
so that user code doesn't have to change.&lt;/li&gt;
&lt;li&gt;Spread the work to one worker process per CPU,
and then further to multiple threads inside each worker,
to work around CPU becoming a bottleneck for I‍/‍O.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Additionally, we have two implicit non-functional requirements:&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Use the existing executors where possible
(less code means fewer bugs).&lt;/li&gt;
&lt;li&gt;Only depend on stable, documented features;
we don't want our code to break
when &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html"&gt;concurrent.​futures&lt;/a&gt; internals change.&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="concurrent-futures"&gt;concurrent.futures&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#concurrent-futures" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Since we're building on top of &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html"&gt;concurrent.​futures&lt;/a&gt;,
we should also get familiar with it;
the docs already provide a great introduction:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html"&gt;concurrent.​futures&lt;/a&gt; module provides
a high-level interface for asynchronously executing callables.
[...this] can be performed with threads, using &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor"&gt;Thread​Pool​Executor&lt;/a&gt;,
or separate processes, using &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor"&gt;Process​Pool​Executor&lt;/a&gt;.
Both implement the same interface,
which is defined by the abstract &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor"&gt;Executor&lt;/a&gt; class.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Let's look at the classes in more detail.&lt;/p&gt;
&lt;!-- TODO: all links should be to code! --&gt;

&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor"&gt;Executor&lt;/a&gt; is an abstract base class&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt; defined in &lt;a class="external" href="https://github.com/python/cpython/blob/ebe54d7ab7ccafbd0a8a6036fd12de971dd2f55b/Lib/concurrent/futures/_base.py#L569"&gt;concurrent.​futures.​_base&lt;/a&gt;.
It provides dummy &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.submit"&gt;submit()&lt;/a&gt; and &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.shutdown"&gt;shutdown()&lt;/a&gt; methods,
a concrete &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.map"&gt;map()&lt;/a&gt; method implemented in terms of &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.submit"&gt;submit()&lt;/a&gt;,
and &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-context-management-protocol"&gt;context manager methods&lt;/a&gt; that &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.shutdown"&gt;shutdown()&lt;/a&gt; the executor on exit.
Notably, the documentation does not mention the concrete methods,
instead saying that the class
&amp;quot;should not be used directly, but through its concrete subclasses&amp;quot;.&lt;/p&gt;
&lt;p&gt;The first subclass, &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor"&gt;Thread​Pool​Executor&lt;/a&gt;, is defined in &lt;a class="external" href="https://github.com/python/cpython/blob/ebe54d7ab7ccafbd0a8a6036fd12de971dd2f55b/Lib/concurrent/futures/thread.py#L122"&gt;concurrent.​futures.​thread&lt;/a&gt;;
it implements &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.submit"&gt;submit()&lt;/a&gt; and &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.shutdown"&gt;shutdown()&lt;/a&gt;,
inheriting &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.map"&gt;map()&lt;/a&gt; unchanged.&lt;/p&gt;
&lt;p&gt;The second one, &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor"&gt;Process​Pool​Executor&lt;/a&gt;, is defined in &lt;a class="external" href="https://github.com/python/cpython/blob/ebe54d7ab7ccafbd0a8a6036fd12de971dd2f55b/Lib/concurrent/futures/process.py#L630"&gt;concurrent.​futures.​process&lt;/a&gt;;
as an optimization,
it &lt;a class="external" href="https://github.com/python/cpython/blob/ebe54d7ab7ccafbd0a8a6036fd12de971dd2f55b/Lib/concurrent/futures/process.py#L808"&gt;overrides map()&lt;/a&gt; to chop the input iterables
and pass the chunks to the superclass method with &lt;a class="external" href="https://docs.python.org/3/library/functions.html#super"&gt;super()&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="three-solutions"&gt;Three solutions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#three-solutions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Now we're ready for code.&lt;/p&gt;
&lt;h3 id="inheritance"&gt;Inheritance&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#inheritance" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;First, the original implementation,&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;
arguably a textbook example of inheritance.&lt;/p&gt;
&lt;p&gt;We override &lt;code&gt;__init__&lt;/code&gt;, &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.submit"&gt;submit()&lt;/a&gt;, and &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.shutdown"&gt;shutdown()&lt;/a&gt;,
and do some extra stuff on top of the inherited behavior,
which we access through &lt;a class="external" href="https://docs.python.org/3/library/functions.html#super"&gt;super()&lt;/a&gt;.
We inherit
the context manager methods,
&lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.map"&gt;map()&lt;/a&gt;,
and any public methods &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor"&gt;Process​Pool​Executor&lt;/a&gt; may get in the future,
assuming they use only other public methods
(more on this &lt;a class="anchor" href="#forward-compatibility"&gt;below&lt;/a&gt;).&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ProcessThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_init_process&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__handle_results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__tasks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;

        &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_running_or_notify_cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;__handle_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Because we're subclassing a class with private, undocumented attributes,
&lt;em&gt;our&lt;/em&gt; private attributes
have to start with &lt;a class="external" href="https://docs.python.org/3/tutorial/classes.html#tut-private"&gt;double underscores&lt;/a&gt;
to avoid clashes with superclass ones
(such as &lt;a class="external" href="https://github.com/python/cpython/blob/ebe54d7ab7ccafbd0a8a6036fd12de971dd2f55b/Lib/concurrent/futures/process.py#L736"&gt;_result_queue&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;In addition to the main class,
there are some global functions used in the worker processes
which remain unchanged
regardless of the solution:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# this code runs in each worker process&lt;/span&gt;

&lt;span class="n"&gt;_executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;_result_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_init_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_executor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_result_queue&lt;/span&gt;

    &lt;span class="n"&gt;_executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_result_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_done_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_put_result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_put_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;a class="attachment" href="/_file/ptpe/ptpelite.py"&gt;Download the entire file.&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="composition"&gt;Composition&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#composition" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;OK, now let's use &lt;a class="external" href="https://en.wikipedia.org/wiki/Composition_over_inheritance"&gt;composition&lt;/a&gt; –
instead of &lt;a class="external" href="https://en.wikipedia.org/wiki/Is-a"&gt;&lt;em&gt;being&lt;/em&gt;&lt;/a&gt; a &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor"&gt;Process​Pool​Executor&lt;/a&gt;,
our Process​Thread​Pool​Executor &lt;a class="external" href="https://en.wikipedia.org/wiki/Has-a"&gt;&lt;em&gt;has&lt;/em&gt;&lt;/a&gt; one.
At a first glance,
the result is the same as before,
with &lt;code&gt;super()&lt;/code&gt; changed to &lt;code&gt;self._inner&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ProcessThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_result_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;/span&gt;            &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_init_process&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_result_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_handle_results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_result_handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tasks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;

        &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_running_or_notify_cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="n"&gt;inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_handle_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_result_handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_result_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Except, we need to implement the context manager protocol ourselves:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__enter__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# concurrent.futures._base.Executor.__enter__&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__exit__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_tb&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# concurrent.futures._base.Executor.__exit__&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;...and we need to copy &lt;code&gt;map()&lt;/code&gt; from &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor"&gt;Executor&lt;/a&gt;,
since it should use &lt;em&gt;our&lt;/em&gt; &lt;code&gt;submit()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;iterables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunksize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# concurrent.futures._base.Executor.map&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="hll"&gt;        &lt;span class="n"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;iterables&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/span&gt;
        &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;result_iterator&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;_result_or_cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;_result_or_cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
            &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result_iterator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;...and the &lt;code&gt;chunksize&lt;/code&gt; optimization from its &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor"&gt;Process​Pool​Executor&lt;/a&gt; version:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;iterables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunksize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# concurrent.futures.process.ProcessPoolExecutor.map&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;chunksize&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;chunksize must be &amp;gt;= 1.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="hll"&gt;        &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_process_chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;                            &lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;iterables&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;chunksize&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                            &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_chain_from_iterable_of_lists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;details&gt;
&lt;summary&gt;...and a bunch of private functions they use.&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_result_or_cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fut&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# concurrent.futures._base._result_or_cancel&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;fut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;fut&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_process_chunk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# concurrent.futures.process._process_chunk&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;chunk&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_chain_from_iterable_of_lists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iterable&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# concurrent.futures.process._chain_from_iterable_of_lists&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;iterable&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;element&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;/details&gt;

&lt;p&gt;&lt;em&gt;And&lt;/em&gt;, when the &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor"&gt;Executor&lt;/a&gt; interface gets new methods,
we'll need to at least &lt;a class="external" href="https://en.wikipedia.org/wiki/Forwarding_(object-oriented_programming)"&gt;forward&lt;/a&gt; them to the inner executor,
although we may have to copy those too.&lt;/p&gt;
&lt;p&gt;On the upside,
no base class means
we can name attributes however we want.&lt;/p&gt;
&lt;p&gt;&lt;a class="attachment" href="/_file/ptpe/ptpelite_comp.py"&gt;Download the entire file.&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;But this is Python,
why do we need to copy stuff?
In Python,
methods are just functions,
so we could &lt;em&gt;almost&lt;/em&gt; get away with this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ProcessThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="c1"&gt;# __init__, submit(), and shutdown() just as before&lt;/span&gt;
    &lt;span class="fm"&gt;__enter__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__enter__&lt;/span&gt;
    &lt;span class="fm"&gt;__exit__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__exit__&lt;/span&gt;
    &lt;span class="nb"&gt;map&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Alas, it won't work –
Process​Pool​Executor &lt;a class="external" href="https://github.com/python/cpython/blob/ebe54d7ab7ccafbd0a8a6036fd12de971dd2f55b/Lib/concurrent/futures/process.py#L808"&gt;map()&lt;/a&gt;
calls &lt;code&gt;super().​map()&lt;/code&gt;,
and &lt;a class="external" href="https://docs.python.org/3/library/functions.html#object"&gt;object&lt;/a&gt;,
the superclass of our executor,
has no such method,
which is why we had to change it to &lt;code&gt;self.​_map()&lt;/code&gt;
in our copy in the first place.&lt;/p&gt;
&lt;h3 id="functions"&gt;Functions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#functions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Can this be done using only functions, though?&lt;/p&gt;
&lt;p&gt;Theoretically no,
since we need to implement the executor interface.
Practically yes,
since this is Python,
where
an &amp;quot;interface&amp;quot; just means
having &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-duck-typing"&gt;specific attributes&lt;/a&gt;,
usually functions with specific signatures.
For example, a &lt;a class="internal" href="/same-functions#counter-example-modules"&gt;module&lt;/a&gt; like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_tasks&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_result_handler&lt;/span&gt;
    &lt;span class="n"&gt;_result_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;_inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_init_process&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;_result_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_handle_results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_result_handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_tasks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;

    &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_running_or_notify_cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_handle_results&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_result_queue&lt;/span&gt;
    &lt;span class="n"&gt;_inner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;_result_handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;_result_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;details&gt;
&lt;summary&gt;
Like before, we need to copy &lt;code&gt;map()&lt;/code&gt; with minor tweaks.
&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;iterables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunksize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# concurrent.futures._base.Executor.map&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;fs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;iterables&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;result_iterator&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reverse&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;_result_or_cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;_result_or_cancel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;end_time&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;future&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;future&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;result_iterator&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;iterables&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunksize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# concurrent.futures.process.ProcessPoolExecutor.map&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;chunksize&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;chunksize must be &amp;gt;= 1.&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_process_chunk&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                   &lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;batched&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;iterables&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;chunksize&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                   &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_chain_from_iterable_of_lists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;/details&gt;

&lt;p&gt;Behold, we can use the module itself as an executor:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;ptpe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;ptpe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Of note,
everything that was an instance variable before
is now a global variable;
as a consequence,
only one executor can exist at any given time,
since there's only the one module.&lt;sup class="footnote-ref" id="fnref-3"&gt;&lt;a href="#fn-3"&gt;3&lt;/a&gt;&lt;/sup&gt;
But it gets worse – calling &lt;code&gt;init()&lt;/code&gt; a second time
will clobber the state of the first executor,
leading to all sorts of bugs;
if we were serious,
we'd prevent it somehow.&lt;/p&gt;
&lt;p&gt;Also, some interfaces are more complicated than having the right functions;
defining &lt;code&gt;__enter__&lt;/code&gt; and &lt;code&gt;__exit__&lt;/code&gt;
is not enough to use a module in a &lt;code&gt;with&lt;/code&gt; statement, since
the interpreter &lt;a class="external" href="https://snarky.ca/unravelling-the-with-statement/"&gt;looks them up on the class of the object&lt;/a&gt;,
not on the object itself.
We can work around this with
an alternate &amp;quot;constructor&amp;quot;
that returns a context manager:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;init_cm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;modules&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ptpe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;init_cm&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;ptpe&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;ptpe&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;2&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="go"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a class="attachment" href="/_file/ptpe/ptpelite_func.py"&gt;Download the entire file.&lt;/a&gt;&lt;/p&gt;
&lt;!--

# composition *and* inheritance

.. literalinclude:: //ptpe/ptpelite_both.py

Yet another option would be to use both inheritance *and* composition –
inherit the [Executor] base class directly for the [common methods]
(assuming they're defined there and not in subclasses),
and delegate to the inner executor only where needed
(likely just [map()] and [shutdown()]).
But, the only difference from the current code would be
that it'd say `self._inner` instead of `super()` in a few places,
so it's not really worth it, in my opinion.

--&gt;




&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        Liking this so far? Here&amp;#39;s another article you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/same-arguments"&gt;
            When to use classes in Python? When your functions take the same arguments
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="comparison"&gt;Comparison&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#comparison" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So, how do the solutions stack up? Here's a summary:&lt;/p&gt;
&lt;table class="table"&gt;
  &lt;thead&gt;
    &lt;tr&gt;
      &lt;th scope="col"&gt;&lt;/th&gt;
      &lt;th scope="col"&gt;pros&lt;/th&gt;
      &lt;th scope="col"&gt;cons&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td scope="row"&gt;&lt;a href="#inheritance"&gt;inheritance&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;
        &lt;ul&gt;
          &lt;li&gt;least amount of code
          &lt;li&gt;inherits new high level methods for free
        &lt;/ul&gt;
      &lt;/td&gt;
      &lt;td&gt;
        &lt;ul&gt;
          &lt;li&gt;assumes inherited high level methods use only the public API
          &lt;li&gt;attribute names have to start with double underscores (minor)
        &lt;/ul&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td scope="row"&gt;&lt;a href="#composition"&gt;composition&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;
        &lt;ul&gt;
          &lt;li&gt;attributes can have any name (minor)
        &lt;/ul&gt;
      &lt;/td&gt;
      &lt;td&gt;
        &lt;ul&gt;
          &lt;li&gt;copies lots of code
          &lt;li&gt;must be kept in sync with the interface
        &lt;/ul&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td scope="row"&gt;&lt;a href="#functions"&gt;functions&lt;/a&gt;&lt;/td&gt;
      &lt;td&gt;?&lt;/td&gt;
      &lt;td&gt;
        &lt;ul&gt;
          &lt;li&gt;copies lots of code
          &lt;li&gt;must be kept in sync with the interface
          &lt;li&gt;only one global executor at a time
          &lt;li&gt;state is harder to discover
          &lt;li&gt;alternate "constructor" to use as context manager (minor)
        &lt;/ul&gt;
      &lt;/td&gt;
    &lt;/tr&gt;
&lt;/table&gt;

&lt;p&gt;I may be a bit biased, but inheritance looks like a clear winner.&lt;/p&gt;
&lt;h3 id="composition-over-inheritance"&gt;Composition over inheritance&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#composition-over-inheritance" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Given that favoring &lt;a class="external" href="https://en.wikipedia.org/wiki/Composition_over_inheritance"&gt;composition over inheritance&lt;/a&gt;
is usually a good practice,
it's worth discussing why inheritance won this time.
I see three reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Composition helps most when
you have unrelated components
that need to be flexible
in response to an evolving business domain;
that's not the case here,
so we get all the &lt;a class="external" href="https://en.wikipedia.org/wiki/Composition_over_inheritance#Drawbacks"&gt;drawbacks&lt;/a&gt;
with none of the &lt;a class="external" href="https://en.wikipedia.org/wiki/Composition_over_inheritance#Benefits"&gt;benefits&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;The existing code
is &lt;a class="anchor" href="#concurrent-futures"&gt;designed for inheritance&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;We have a true &lt;em&gt;&lt;a class="external" href="https://en.wikipedia.org/wiki/Is-a"&gt;is-a&lt;/a&gt;&lt;/em&gt; relationship –
Process​Thread​Pool​Executor really is a &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor"&gt;Process​Pool​Executor&lt;/a&gt;
with extra behavior,
and not just part of an arbitrary hierarchy.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For a different line of reasoning involving subtyping,
check out &lt;a class="external" href="https://www.hillelwayne.com/"&gt;Hillel Wayne&lt;/a&gt;'s &lt;a class="external" href="https://buttondown.email/hillelwayne/archive/when-to-prefer-inheritance-to-composition/"&gt;When to prefer inheritance to composition&lt;/a&gt;;
he offers this rule of thumb:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;So, here's when you want to use inheritance:
&lt;strong&gt;when you need to instantiate both the parent and child classes
and pass them to the same functions&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="forward-compatibility"&gt;Forward compatibility&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#forward-compatibility" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;a class="anchor" href="#inheritance"&gt;inheritance&lt;/a&gt; solution
assumes &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.map"&gt;map()&lt;/a&gt; and
any future public &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor"&gt;Process​Pool​Executor&lt;/a&gt; methods
are implemented only in terms of other public methods.
This assumption introduces a risk that updates may break our executor;
this is lowered by two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html"&gt;concurrent.​futures&lt;/a&gt; is in the standard library,
which rarely does major rewrites of existing code,
and never within a minor (X.Y) version;
concurrent.​futures exists in its current form
&lt;a class="external" href="https://github.com/python/cpython/tree/v3.2/Lib/concurrent/futures"&gt;since Python 3.2&lt;/a&gt;, released in 2011.&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html"&gt;concurrent.​futures&lt;/a&gt; is clearly &lt;a class="anchor" href="#concurrent-futures"&gt;designed for inheritance&lt;/a&gt;,
even if mainly to enable internal reuse,
and not explicitly documented.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;As active mitigations,
we can add a basic test suite
(which we should do anyway),
and &lt;a class="external" href="https://packaging.python.org/en/latest/specifications/core-metadata/#core-metadata-classifier"&gt;document&lt;/a&gt; the &lt;a class="external" href="https://pypi.org/classifiers/#:~:text=Programming%20Language%20::%20Python%20::%203"&gt;supported Python versions&lt;/a&gt; explicitly
(which we should do anyway if we were to release this on PyPI).&lt;/p&gt;
&lt;p&gt;If &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html"&gt;concurrent.​futures&lt;/a&gt; were not in the standard library,
I'd probably go with the &lt;a class="anchor" href="#composition"&gt;composition&lt;/a&gt; version instead,
although as already mentioned,
this wouldn't be free from upkeep either.
Another option would be to
upstream Process​Thread​Pool​Executor,
so that it is maintained together with the code it depends on.&lt;/p&gt;
&lt;h3 id="global-state"&gt;Global state&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#global-state" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;a class="anchor" href="#functions"&gt;functions-only&lt;/a&gt; solution is probably the worst of the three,
since it has all the downsides of &lt;a class="anchor" href="#composition"&gt;composition&lt;/a&gt;,
&lt;em&gt;and&lt;/em&gt; significant limitations due to its use of global state.&lt;/p&gt;
&lt;p&gt;We could avoid using globals
by passing the state
(process pool executor instance, result queue, etc.)
as function arguments,
but this breaks the executor interface,
and makes for an awful user experience.
We could group common arguments into a single object
so there's only one argument to pass around;
if you call that argument &lt;code&gt;self&lt;/code&gt;,
&lt;a class="internal" href="/same-arguments"&gt;it becomes obvious&lt;/a&gt; that's just a class instance with extra steps.&lt;/p&gt;
&lt;p&gt;Having to keep track of a bunch of related globals has enough downsides
that even if you do want a module-level API,
it's still worth using a class to group them,
and exposing the methods of a global instance
at module-level (&lt;a class="external" href="https://github.com/python/cpython/blob/ebe54d7ab7ccafbd0a8a6036fd12de971dd2f55b/Lib/random.py#L917-L948"&gt;like so&lt;/a&gt;);
Brandon Rhodes discusses this at length in &lt;a class="external" href="https://python-patterns.guide/python/prebound-methods/"&gt;The Prebound Method Pattern&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="complexity"&gt;Complexity&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#complexity" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;While the code is somewhat complex,
that's mostly intrinsic to the problem itself
(what runs in the main vs. worker processes,
passing results around, error handling, and so on),
rather than due to our of use classes,
which only affects
how we refer to &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor"&gt;Process​Pool​Executor&lt;/a&gt; methods
and how we store state.&lt;/p&gt;
&lt;p&gt;One could argue that copying a bunch of code doesn't increase complexity,
but if you factor in keeping it up to date and tested,
it's not exactly free either.&lt;/p&gt;
&lt;p&gt;One could also argue that building our executor on top of &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor"&gt;Process​Pool​Executor&lt;/a&gt;
is increasing complexity,
and in a way that's true –
for example, we have
&lt;a class="internal" href="/ptpe#getting-results"&gt;two result queues&lt;/a&gt;
and had to
&lt;a class="internal" href="/ptpe#death-becomes-a-problem"&gt;deal with dead workers&lt;/a&gt; too,
which wouldn't be the case if we wrote it from scratch;
but in turn, that would come with having to
understand, maintain, and test
&lt;a class="external" href="https://github.com/python/cpython/blob/ebe54d7ab7ccafbd0a8a6036fd12de971dd2f55b/Lib/concurrent/futures/process.py#L630"&gt;800+ lines of code&lt;/a&gt;
of low level process management code.
Sometimes,
&lt;em&gt;complexity I have to care about&lt;/em&gt;
is more important that &lt;em&gt;total complexity&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id="debugging"&gt;Debugging&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#debugging" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;I have to come clean at this point –
I use &lt;a class="external" href="https://blog.startifact.com/posts/print-debugging/"&gt;print debugging&lt;/a&gt; &lt;em&gt;a lot&lt;/em&gt; 🙀
(especially if there are no tests yet,
and sometimes from tests too);
when that doesn't cut it,
IPython's &lt;a class="external" href="https://ipython.readthedocs.io/en/stable/api/generated/IPython.terminal.embed.html#IPython.terminal.embed.embed"&gt;embed()&lt;/a&gt; usually provides enough interactivity
to figure out what's going on.&lt;sup class="footnote-ref" id="fnref-4"&gt;&lt;a href="#fn-4"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;With the &lt;a class="internal" href="/ptpe#minimal-test"&gt;minimal test&lt;/a&gt; at the end of the file
driving the executor,
I used temporary &lt;a class="external" href="https://docs.python.org/3/library/functions.html#print"&gt;print()&lt;/a&gt; calls
in &lt;code&gt;_submit()&lt;/code&gt;, &lt;code&gt;_put_result()&lt;/code&gt;, and &lt;code&gt;__handle_results()&lt;/code&gt;
to check data is making its way through properly;
if I expected the code to change more often,
I'd replace them with permanent logging calls.&lt;/p&gt;
&lt;p&gt;In addition,
there were two debugging scripts
in the &lt;a class="attachment" href="/_file/ptpe/bench.py"&gt;benchmark&lt;/a&gt; file
that I didn't show,
one to automate &lt;a class="internal" href="/ptpe#death-becomes-a-problem"&gt;killing workers&lt;/a&gt; at the right time,
and one to make sure &lt;code&gt;shutdown()&lt;/code&gt; waits any pending tasks.&lt;/p&gt;
&lt;p&gt;So, does how we wrote the code change any of this?
Not really, no;
all the techniques above (and using a debugger too)
apply equally well.
If anything,
using classes makes interactive debugging easier,
since it's easier to discover state via autocomplete
(with functions only, you have to know to look it up on the module).&lt;/p&gt;



&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        If you&amp;#39;ve made it this far, you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/stdlib"&gt;
            Learn by reading code: Python standard library design decisions explained
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="try-it-out"&gt;Try it out&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#try-it-out" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;As I've said before, &lt;a class="internal" href="/same-functions#try-it-out"&gt;try it out&lt;/a&gt; –
it only took ~10 minutes to convert the initial solution to the other two.
In part,
the right code structure is a matter feeling and taste,
and both are educated by &lt;a class="internal" href="/stdlib"&gt;reading&lt;/a&gt; and &lt;strong&gt;writing&lt;/strong&gt; lots of code.
If you think there's a better way to do something,
do it and see how it looks;
it is a sort of deliberate practice.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/over-composition&amp;t=Inheritance%20over%20composition%2C%20sometimes"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Inheritance%20over%20composition%2C%20sometimes%20https%3A//death.andgravity.com/over-composition"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/over-composition&amp;title=Inheritance%20over%20composition%2C%20sometimes"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/over-composition"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Inheritance%20over%20composition%2C%20sometimes&amp;url=https%3A//death.andgravity.com/over-composition&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor"&gt;Executor&lt;/a&gt; is an &lt;em&gt;abstract base class&lt;/em&gt; only by convention:
it is a &lt;em&gt;base class&lt;/em&gt; (other classes are supposed to subclass it),
and it is &lt;em&gt;abstract&lt;/em&gt; (other classes are supposed to provide
concrete implementations for some methods).&lt;/p&gt;
&lt;p&gt;Python also allows formalizing &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-abstract-base-class"&gt;abstract base classes&lt;/a&gt; using the &lt;a class="external" href="https://docs.python.org/3/library/abc.html#module-abc"&gt;abc&lt;/a&gt; module;
see &lt;a class="internal" href="/same-functions#formalizing-this"&gt;When to use classes in Python? When you repeat similar sets of functions&lt;/a&gt;
for an example of this and other ways of achieving the same goal. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;For brevity, I'm using the version
before &lt;a class="internal" href="/ptpe#death-becomes-a-problem"&gt;dealing with dead workers&lt;/a&gt;;
the final code is similar,
but with a more involved &lt;code&gt;__handle_results&lt;/code&gt;. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-3"&gt;&lt;p&gt;This is &lt;em&gt;almost&lt;/em&gt; true –
we could &amp;quot;this is Python&amp;quot; our way deeper
and &lt;a class="external" href="https://docs.python.org/3/library/sys.html#sys.modules"&gt;reload the module&lt;/a&gt;
while still keeping a reference to the old one,
but that's just a round-about, unholy way
of emulating class instances. &lt;a href="#fnref-3" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-4"&gt;&lt;p&gt;Pro tip: you can use &lt;a class="external" href="https://ipython.readthedocs.io/en/stable/api/generated/IPython.terminal.embed.html#IPython.terminal.embed.embed"&gt;embed()&lt;/a&gt; as a &lt;a class="external" href="https://docs.python.org/3/library/functions.html#breakpoint"&gt;breakpoint()&lt;/a&gt; hook:
&lt;code&gt;PYTHONBREAKPOINT=IPython.embed python myscript.py&lt;/code&gt;. &lt;a href="#fnref-4" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/over-composition" rel="alternate"/>
    <summary>Last time, we built a hybrid concurrent.futures executor using inheritance. Today, we're building it again (twice!) using composition and functions only, to figure out which way is better and why. Consider this a worked example.</summary>
    <published>2025-07-15T07:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/ptpe">
    <id>https://death.andgravity.com/ptpe</id>
    <title>Process​Thread​Pool​Executor: when I‍/‍O becomes CPU-bound</title>
    <updated>2025-05-07T18:00:00+00:00</updated>
    <content type="html">&lt;p&gt;So, you're doing some I‍/‍O bound stuff, in parallel.&lt;/p&gt;
&lt;p&gt;Maybe you're scraping some websites – a &lt;em&gt;lot&lt;/em&gt; of websites.&lt;/p&gt;
&lt;p&gt;Maybe you're updating or deleting millions of DynamoDB items.&lt;/p&gt;
&lt;p&gt;You've got your &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor"&gt;ThreadPoolExecutor&lt;/a&gt;,
you've increased the number of threads and tuned connection limits...
but after some point, &lt;strong&gt;it's just not getting any faster&lt;/strong&gt;.
You look at your Python process,
and you see CPU utilization hovers above 100%.&lt;/p&gt;
&lt;p&gt;You &lt;em&gt;could&lt;/em&gt; split the work into batches
and have a &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor"&gt;ProcessPoolExecutor&lt;/a&gt;
run your original code in separate processes.
But that requires yet more code, and a bunch of changes, which is no fun.
And maybe your input is not that easy to split into batches.&lt;/p&gt;
&lt;p&gt;If only we had an executor that
&lt;strong&gt;worked seamlessly across processes and threads&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Well, you're in luck, since that's exactly what we're building today!&lt;/p&gt;
&lt;p&gt;And even better, in a couple years you won't even need it anymore.&lt;/p&gt;
&lt;details class="toc" open&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#establishing-a-baseline"&gt;Establishing a baseline&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#threads"&gt;Threads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#problem-cpu-becomes-a-bottleneck"&gt;Problem: CPU becomes a bottleneck&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#processes"&gt;Processes?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#problem-more-processes-more-memory"&gt;Problem: more processes, more memory&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#why-not-both"&gt;Why not both?&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#a-minimal-plausible-solution"&gt;A minimal plausible solution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#getting-results"&gt;Getting results&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#fine-we-ll-make-our-own-futures"&gt;Fine, we'll make our own futures&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#death-becomes-a-problem"&gt;Death becomes a problem&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#bonus-free-threading"&gt;Bonus: free threading&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="establishing-a-baseline"&gt;Establishing a baseline&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#establishing-a-baseline" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;To measure things,
we'll use a mock that pretends to do &lt;a class="external" href="https://en.wikipedia.org/wiki/I/O_bound"&gt;mostly I‍/‍O&lt;/a&gt;,
with a sprinkling of &lt;a class="external" href="https://en.wikipedia.org/wiki/CPU-bound"&gt;CPU-bound&lt;/a&gt; work thrown in
– a stand-in for something like a database connection,
a Requests &lt;a class="external" href="https://requests.readthedocs.io/en/latest/user/advanced/#session-objects"&gt;session&lt;/a&gt;, or a &lt;a class="external" href="https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/dynamodb.html#DynamoDB.Client"&gt;DynamoDB client&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;io_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.02&lt;/span&gt;
    &lt;span class="n"&gt;cpu_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0008&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# simulate I/O&lt;/span&gt;
        &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;io_time&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# simulate CPU-bound work&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cpu_time&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We &lt;a class="external" href="https://docs.python.org/3/library/time.html#time.sleep"&gt;sleep()&lt;/a&gt; for the I‍/‍O,
and do some math in a loop for the CPU stuff;
it doesn't matter exactly how long each takes,
as long I‍/‍O time dominates.&lt;/p&gt;
&lt;p&gt;&lt;a id="connection-pool"&gt;&lt;/a&gt;
Real multi-threaded clients are usually backed by a shared connection pool,
which allows for connection reuse
(so you don't pay the cost of a new connection on each request)
and multiplexing
(so you can use the same connection for multiple concurrent requests,
possible with protocols like HTTP/2 or newer).
We could simulate this with a &lt;a class="external" href="https://docs.python.org/3/library/threading.html#semaphore-objects"&gt;semaphore&lt;/a&gt;,
but limiting connections is not relevant here
– we're assuming the connection pool is effectively unbounded.&lt;/p&gt;
&lt;p&gt;Since we'll use our client from multiple processes,
we write an initializer function
to set up a global, per-process client instance
(remember, we want to share potential connection pools between threads);
we can then pass the initializer
to the &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor"&gt;executor&lt;/a&gt; constructor,
along with any arguments we want to pass to the client.
Similarly, we do the work through a function that uses this global client.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# this code runs in each worker process&lt;/span&gt;

&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;method&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, we make a simple timing context manager:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt;
    &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;elapsed: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;1.3f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;...and put everything together in a function
that measures how long it takes
to do a bunch of work
using a &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html"&gt;concurrent.futures&lt;/a&gt; executor:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;chunksize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="c1"&gt;# make sure all the workers are started,&lt;/span&gt;
        &lt;span class="c1"&gt;# so we don&amp;#39;t measure their startup time&lt;/span&gt;
        &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;chunksize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;chunksize&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="threads"&gt;Threads&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#threads" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- FIXME: note about python version and computer used --&gt;

&lt;p&gt;So, a &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor"&gt;ThreadPoolExecutor&lt;/a&gt; should suffice here,
since we're mostly doing I‍/‍O, right?&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;concurrent.futures&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;bench&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 24.693&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;More threads!&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 12.405&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Twice the threads, twice as fast. More!&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 8.718&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Good, it's still scaling linearly. MORE!&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 8.638&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/ptpe/confused.jpg" alt="confused cat with question marks around its head" /&gt;&lt;/p&gt;
&lt;p&gt;...more?&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;50&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 8.458&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 8.430&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 8.428&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;!-- ![confused cat with raised eyebrow collage](attachment:confused2.jpg) --&gt;

&lt;!-- !["what in tarnation" shiba inu dog wearing a cowboy hat](attachment:tarnation.jpg) --&gt;

&lt;!-- !["what in tarnation?" cat wearing a cowboy hat](attachment:tarnation2.jpg) --&gt;

&lt;!-- ![confused cat snarling](attachment:wtf.jpg) --&gt;

&lt;!-- ![slightly concerned cat looking at reader](attachment:concern.jpg) --&gt;

&lt;p&gt;&lt;img class="img-responsive" src="/_file/ptpe/confused3.jpg" alt="squinting confused cat" /&gt;&lt;/p&gt;
&lt;!-- ## Problem: CPU becomes a bottleneck when you do enough I&amp;zwj;/&amp;zwj;O --&gt;

&lt;h3 id="problem-cpu-becomes-a-bottleneck"&gt;Problem: CPU becomes a bottleneck&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-cpu-becomes-a-bottleneck" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;It's time we take a closer look at what our process is doing.
I'd normally use the &lt;a class="external" href="https://linux.die.net/man/1/top"&gt;top&lt;/a&gt; command for this,
but since the flags and output vary with the operating system,
we'll implement our own using the excellent &lt;a class="external" href="https://psutil.readthedocs.io/"&gt;psutil&lt;/a&gt; library.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;top&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Print information about current and child processes.&lt;/span&gt;

&lt;span class="sd"&gt;    RES is the resident set size. USS is the unique set size.&lt;/span&gt;
&lt;span class="sd"&gt;    %CPU is the CPU utilization. nTH is the number of threads.&lt;/span&gt;

&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;process&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;psutil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;processes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;process&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;processes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cpu_percent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;yield&lt;/span&gt;

    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;PID&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;7&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;RES&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;7&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;USS&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;7&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;%CPU&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;7&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;nTH&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;7&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;processes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_full_info&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;psutil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AccessDenied&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;memory_info&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;rss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rss&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;
        &lt;span class="n"&gt;uss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;uss&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;
        &lt;span class="n"&gt;cpu&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cpu_percent&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;nth&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;num_threads&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pid&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;7&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;rss&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;6.1f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;m &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;uss&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;6.1f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;m &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cpu&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;7.1f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;nth&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;&amp;gt;7&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And because it's a context manager, we can use it as a timer:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;    PID     RES     USS    %CPU     nTH&lt;/span&gt;
&lt;span class="go"&gt;  51395   35.2m   28.5m    38.7      11&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So, what happens if we increase the number of threads?&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;    PID     RES     USS    %CPU     nTH&lt;/span&gt;
&lt;span class="go"&gt;  13912   16.8m   13.2m    70.7      21&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;    PID     RES     USS    %CPU     nTH&lt;/span&gt;
&lt;span class="go"&gt;  13912   17.0m   13.4m    99.1      31&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;    PID     RES     USS    %CPU     nTH&lt;/span&gt;
&lt;span class="go"&gt;  13912   17.3m   13.7m   100.9      41&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With more threads, the compute part of our I‍/‍O bound workload increases,
eventually becoming high enough to saturate one CPU
– and due to the &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-global-interpreter-lock"&gt;global interpreter lock&lt;/a&gt;,
&lt;em&gt;one CPU is all we can use&lt;/em&gt;, regardless of the number of threads.&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 id="processes"&gt;Processes?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#processes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;I know, let's use a &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor"&gt;ProcessPoolExecutor&lt;/a&gt; instead!&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 12.374&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 8.330&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 6.273&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Hmmm... I guess it &lt;em&gt;is&lt;/em&gt; a little bit better.&lt;/p&gt;
&lt;p&gt;More? More!&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 4.751&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 3.785&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 3.824&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;OK, it's better, but with diminishing returns
– there's no improvement after 80 processes,
and even then, it's only &lt;strong&gt;2.2x&lt;/strong&gt; faster than the best time with threads,
when, in theory, it should be able to make full use of all 4 CPUs.&lt;/p&gt;
&lt;p&gt;Also, we're not making best use of
&lt;a class="anchor" href="#connection-pool"&gt;connection pools&lt;/a&gt;
(since we now have 80 of them, one per process),
nor multiplexing
(since we now have 80 connections, one per pool).&lt;/p&gt;
&lt;h3 id="problem-more-processes-more-memory"&gt;Problem: more processes, more memory&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-more-processes-more-memory" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;But it gets worse!&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;    PID     RES     USS    %CPU     nTH&lt;/span&gt;
&lt;span class="go"&gt;   2479   21.2m   15.4m    15.0       3&lt;/span&gt;
&lt;span class="go"&gt;   2480   11.2m    6.3m     0.0       1&lt;/span&gt;
&lt;span class="go"&gt;   2481   13.8m    8.5m     3.4       1&lt;/span&gt;
&lt;span class="go"&gt;  ... 78 more lines ...&lt;/span&gt;
&lt;span class="go"&gt;   2560   13.8m    8.5m     4.4       1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;13.8 MiB * 80 ~= 1 GiB ... that is &lt;em&gt;a lot&lt;/em&gt; of memory.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Now, there's some nuance to be had here.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;First, on most operating systems that have virtual memory,
&lt;a class="external" href="https://en.wikipedia.org/wiki/Code_segment"&gt;code segment&lt;/a&gt; pages are shared between processes
– there's no point in having 80 copies
of libc or the Python interpreter in memory.&lt;/p&gt;
&lt;p&gt;The &lt;a class="external" href="https://en.wikipedia.org/wiki/Unique_set_size"&gt;unique set size&lt;/a&gt; is probably a better measurement
than the &lt;a class="external" href="https://en.wikipedia.org/wiki/Resident_set_size"&gt;resident set size&lt;/a&gt;,
since it excludes memory shared between processes.&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;
So, for the macOS output above,&lt;sup class="footnote-ref" id="fnref-3"&gt;&lt;a href="#fn-3"&gt;3&lt;/a&gt;&lt;/sup&gt;
the actual usage is more like 8.5 MiB * 80 = 680 MiB.&lt;/p&gt;
&lt;p&gt;Second, if you use the &lt;em&gt;fork&lt;/em&gt; or &lt;em&gt;forkserver&lt;/em&gt; &lt;a class="external" href="https://docs.python.org/3.14/library/multiprocessing.html#contexts-and-start-methods"&gt;start methods&lt;/a&gt;,
processes also share memory allocated before the &lt;a class="external" href="https://en.wikipedia.org/wiki/Fork_(system_call)"&gt;fork()&lt;/a&gt; via &lt;a class="external" href="https://en.wikipedia.org/wiki/Copy-on-write#In_virtual_memory_management"&gt;copy-on-write&lt;/a&gt;;
for Python, this includes module code and variables.
On Linux, the actual usage is 1.7 MiB * 80 = 136 MiB:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;top&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;    PID     RES     USS    %CPU     nTH&lt;/span&gt;
&lt;span class="go"&gt; 329801   17.0m    6.6m     5.1       3&lt;/span&gt;
&lt;span class="go"&gt; 329802   13.3m    1.6m     2.1       1&lt;/span&gt;
&lt;span class="go"&gt;  ... 78 more lines ...&lt;/span&gt;
&lt;span class="go"&gt; 329881   13.3m    1.7m     2.0       1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;However, it's important to note that's just a lower bound;
memory allocated after &lt;a class="external" href="https://en.wikipedia.org/wiki/Fork_(system_call)"&gt;fork()&lt;/a&gt; is not shared,
and most real work will unavoidably allocate more memory.&lt;/p&gt;



&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        Liking this so far? Here&amp;#39;s another article you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/same-arguments"&gt;
            When to use classes in Python? When your functions take the same arguments
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="why-not-both"&gt;Why not both?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-not-both" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;One reasonable way of dealing with this
would be to split the input into batches,
one per CPU, and pass them to a &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor"&gt;ProcessPoolExecutor&lt;/a&gt;,
which in turn runs the batch items using a &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor"&gt;ThreadPoolExecutor&lt;/a&gt;.&lt;sup class="footnote-ref" id="fnref-4"&gt;&lt;a href="#fn-4"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;But that would mean we need to change our code, and that's no fun.&lt;/p&gt;
&lt;p&gt;If only we had an executor that
&lt;strong&gt;worked seamlessly across processes and threads&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="a-minimal-plausible-solution"&gt;A minimal plausible solution&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#a-minimal-plausible-solution" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;In keeping with what has
&lt;a class="internal" href="/pwned#a-minimal-plausible-solution"&gt;become&lt;/a&gt;
&lt;a class="internal" href="/lru-cache#a-minimal-plausible-solution"&gt;tradition&lt;/a&gt;
by now,
we'll take an iterative, &lt;a class="external" href="https://hintjens.gitbooks.io/scalable-c/content/chapter1.html#problem-what-do-we-do-next"&gt;problem-solution&lt;/a&gt; approach;
since we're &lt;em&gt;not sure what to do yet&lt;/em&gt;,
we start with &lt;a class="external" href="https://wiki.c2.com/?DoTheSimplestThingThatCouldPossiblyWork"&gt;the simplest thing that could possibly work&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We know we want a process pool executor
that starts one thread pool executor per process,
so let's deal with that first.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ProcessThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_init_process&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;By subclassing ProcessPoolExecutor,
we get the &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.map"&gt;map()&lt;/a&gt; implementation for free,
since the original is implemented in terms of &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.submit"&gt;submit()&lt;/a&gt;.&lt;sup class="footnote-ref" id="fnref-5"&gt;&lt;a href="#fn-5"&gt;5&lt;/a&gt;&lt;/sup&gt;
By going with the default &lt;code&gt;max_workers&lt;/code&gt;,
we get one process per CPU (which is what we want);
we can add more arguments later if needed.&lt;/p&gt;
&lt;p&gt;In a custom process initializer,
we set up a global thread pool executor,&lt;sup class="footnote-ref" id="fnref-6"&gt;&lt;a href="#fn-6"&gt;6&lt;/a&gt;&lt;/sup&gt;
and then call the process initializer provided by the user:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# this code runs in each worker process&lt;/span&gt;

&lt;span class="n"&gt;_executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_init_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_executor&lt;/span&gt;

    &lt;span class="n"&gt;_executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Likewise, &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.submit"&gt;submit()&lt;/a&gt; passes the work along
to the thread pool executor:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ProcessThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# this code runs in each worker process&lt;/span&gt;
&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;a name="minimal-test"&gt;&lt;/a&gt;
OK, that looks good enough;
let's use it and see if it works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_do_stuff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;doing: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;__main__&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ProcessThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_do_stuff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt; $ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;ptpe.py
&lt;span class="go"&gt;doing: 0&lt;/span&gt;
&lt;span class="go"&gt;doing: 1&lt;/span&gt;
&lt;span class="go"&gt;doing: 2&lt;/span&gt;
&lt;span class="go"&gt;[0, 1, 4]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Wait, we got it on the first try?!&lt;/p&gt;
&lt;p&gt;Let's measure that:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;bench&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;ptpe&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 6.161&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Hmmm... that's unexpectedly slow... almost as if:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;multiprocessing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cpu_count&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;4&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 6.067&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Ah, because &lt;code&gt;_submit()&lt;/code&gt; waits for the &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Future.result"&gt;result()&lt;/a&gt;
in the main thread of the worker process,
this is just a ProcessPoolExecutor with extra steps.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;But what if we send back the &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#future-objects"&gt;future&lt;/a&gt; object instead?&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;_executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Alas:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="go"&gt;$ python ptpe.py&lt;/span&gt;
&lt;span class="go"&gt;doing: 0&lt;/span&gt;
&lt;span class="go"&gt;doing: 1&lt;/span&gt;
&lt;span class="go"&gt;doing: 2&lt;/span&gt;
&lt;span class="go"&gt;concurrent.futures.process._RemoteTraceback:&lt;/span&gt;
&lt;span class="go"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;concurrent/futures/process.py&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;210&lt;/span&gt;, in &lt;span class="n"&gt;_sendback_result&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_ResultItem&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;work_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;multiprocessing/queues.py&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;391&lt;/span&gt;, in &lt;span class="n"&gt;put&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_ForkingPickler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;multiprocessing/reduction.py&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;51&lt;/span&gt;, in &lt;span class="n"&gt;dumps&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buf&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;obj&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;cannot pickle &amp;#39;_thread.RLock&amp;#39; object&lt;/span&gt;
&lt;span class="x"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="gt"&gt;The above exception was the direct cause of the following exception:&lt;/span&gt;

&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;ptpe.py&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;42&lt;/span&gt;, in &lt;span class="n"&gt;&amp;lt;module&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_do_stuff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])))&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;cannot pickle &amp;#39;_thread.RLock&amp;#39; object&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The immediate cause of the error is that
the future
&lt;a class="external" href="https://github.com/python/cpython/blob/3.13/Lib/concurrent/futures/_base.py#L330"&gt;has a condition&lt;/a&gt;
that &lt;a class="external" href="https://github.com/python/cpython/blob/3.13/Lib/threading.py#L284"&gt;has a lock&lt;/a&gt;
that can't be &lt;a class="external" href="https://docs.python.org/3/library/pickle.html#object.__reduce__"&gt;pickled&lt;/a&gt;,
because &lt;a class="external" href="https://docs.python.org/3/library/threading.html"&gt;threading&lt;/a&gt; locks only make sense within the same process.&lt;/p&gt;
&lt;p&gt;The deeper cause is that the &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#future-objects"&gt;future&lt;/a&gt; is not just data,
but encapsulates state owned by the thread pool executor,
and &lt;a class="external" href="https://docs.python.org/3/library/multiprocessing.html#sharing-state-between-processes"&gt;sharing state between processes&lt;/a&gt; requires extra work.&lt;/p&gt;
&lt;p&gt;It may not seem like it, but this is a partial success:
the work happens,
we just can't get the results back.
Not surprising, to be honest, it couldn't have been &lt;em&gt;that&lt;/em&gt; easy.&lt;/p&gt;
&lt;h3 id="getting-results"&gt;Getting results&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#getting-results" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;If you look carefully at the traceback,
you'll find a hint of how &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#processpoolexecutor"&gt;ProcessPoolExecutor&lt;/a&gt;
gets its own results back from workers
– a queue;
the &lt;a class="external" href="https://github.com/python/cpython/blob/3.13/Lib/concurrent/futures/process.py"&gt;module docstring&lt;/a&gt; even has a neat data-flow diagram:&lt;/p&gt;
&lt;p&gt;&lt;a name="data-flow"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;|======================= In-process =====================|== Out-of-process ==|

+----------+     +----------+       +--------+     +-----------+    +---------+
|          |  =&amp;gt; | Work Ids |       |        |     | Call Q    |    | Process |
|          |     +----------+       |        |     +-----------+    |  Pool   |
|          |     | ...      |       |        |     | ...       |    +---------+
|          |     | 6        |    =&amp;gt; |        |  =&amp;gt; | 5, call() | =&amp;gt; |         |
|          |     | 7        |       |        |     | ...       |    |         |
| Process  |     | ...      |       | Local  |     +-----------+    | Process |
|  Pool    |     +----------+       | Worker |                      |  #1..n  |
| Executor |                        | Thread |                      |         |
|          |     +----------- +     |        |     +-----------+    |         |
|          | &amp;lt;=&amp;gt; | Work Items | &amp;lt;=&amp;gt; |        | &amp;lt;=  | Result Q  | &amp;lt;= |         |
|          |     +------------+     |        |     +-----------+    |         |
|          |     | 6: call()  |     |        |     | ...       |    |         |
|          |     |    future  |     |        |     | 4, result |    |         |
|          |     | ...        |     |        |     | 3, except |    |         |
+----------+     +------------+     +--------+     +-----------+    +---------+
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now, we could probably use the same queue somehow,
but it would involve touching a lot of (private) internals.&lt;sup class="footnote-ref" id="fnref-7"&gt;&lt;a href="#fn-7"&gt;7&lt;/a&gt;&lt;/sup&gt;
Instead, let's use a separate queue:&lt;/p&gt;
&lt;!--
this is one of the rare cases where
we have to use [double underscores],
otherwise we'll clobber the original `_result_queue` attribute.
--&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_init_process&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;On the worker side, we make it globally accessible:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# this code runs in each worker process&lt;/span&gt;

&lt;span class="n"&gt;_executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;span class="n"&gt;_result_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_init_process&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;global&lt;/span&gt; &lt;span class="n"&gt;_executor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_result_queue&lt;/span&gt;

    &lt;span class="n"&gt;_executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;_result_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;...so we can use it from a task callback registered by &lt;code&gt;_submit()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_done_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_put_result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_put_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Back in the main process, we handle the results in a thread:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="c1"&gt;# ...&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_handler&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__handle_results&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;__handle_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ok&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;error&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, to stop the handler,
we use None as a &lt;a class="internal" href="/sentinels#what-s-a-sentinel-and-why-do-i-need-one"&gt;sentinel&lt;/a&gt;
on executor shutdown:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdown&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_handler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Let's see if it works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="go"&gt;$ python ptpe.py&lt;/span&gt;
&lt;span class="go"&gt;doing: 0&lt;/span&gt;
&lt;span class="go"&gt;ok: [0]&lt;/span&gt;
&lt;span class="go"&gt;doing: 1&lt;/span&gt;
&lt;span class="go"&gt;ok: [1]&lt;/span&gt;
&lt;span class="go"&gt;doing: 2&lt;/span&gt;
&lt;span class="go"&gt;ok: [4]&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;concurrent/futures/_base.py&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;317&lt;/span&gt;, in &lt;span class="n"&gt;_result_or_cancel&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fut&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gr"&gt;AttributeError&lt;/span&gt;: &lt;span class="n"&gt;&amp;#39;NoneType&amp;#39; object has no attribute &amp;#39;result&amp;#39;&lt;/span&gt;

&lt;span class="gt"&gt;During handling of the above exception, another exception occurred:&lt;/span&gt;

&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;AttributeError&lt;/span&gt;: &lt;span class="n"&gt;&amp;#39;NoneType&amp;#39; object has no attribute &amp;#39;cancel&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Yay, the results are making it to the handler!&lt;/p&gt;
&lt;p&gt;The error happens because instead of returning a &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#future-objects"&gt;Future&lt;/a&gt;,
our &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.submit"&gt;submit()&lt;/a&gt; returns the result of &lt;code&gt;_submit()&lt;/code&gt;, which is always None.&lt;/p&gt;
&lt;h3 id="fine-we-ll-make-our-own-futures"&gt;Fine, we'll make our own futures&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#fine-we-ll-make-our-own-futures" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;But &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.submit"&gt;submit()&lt;/a&gt; &lt;em&gt;must&lt;/em&gt; return a future, so we make our own:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_threads&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
        &lt;span class="c1"&gt;# ...&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Future&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__tasks&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;

        &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_running_or_notify_cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In order to map results to their futures,
we can use a unique identifier;
the &lt;a class="external" href="https://docs.python.org/3/library/functions.html#id"&gt;id()&lt;/a&gt; of the outer future should do,
since it is unique for the object's lifetime.&lt;/p&gt;
&lt;p&gt;We pass the id to &lt;code&gt;_submit()&lt;/code&gt;,
then to &lt;code&gt;_put_result()&lt;/code&gt; as an attribute on the future,
and finally back in the queue with the result:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;
    &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_done_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_put_result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_put_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;_result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Back in the result handler,
we find the maching future,
and set the result accordingly:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;__handle_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And it works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;ptpe.py
&lt;span class="go"&gt;doing: 0&lt;/span&gt;
&lt;span class="go"&gt;doing: 1&lt;/span&gt;
&lt;span class="go"&gt;doing: 2&lt;/span&gt;
&lt;span class="go"&gt;[0, 1, 4]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I mean, it &lt;em&gt;really&lt;/em&gt; works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 6.220&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 3.397&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 2.575&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ProcessThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 2.664&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;3.3x&lt;/strong&gt; is not &lt;em&gt;quite&lt;/em&gt; the 4 CPUs my laptop has,
but it's pretty close,
and much better than the 2.2x we got from processes alone.&lt;/p&gt;
&lt;p&gt;&lt;a class="attachment" href="/_file/ptpe/ptpelite.py"&gt;The executor code so far.&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="death-becomes-a-problem"&gt;Death becomes a problem&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#death-becomes-a-problem" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;I wonder what happens when a worker process dies.&lt;/p&gt;
&lt;p&gt;For example, the initializer can fail:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;divmod&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initargs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;Exception in initializer:&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;ZeroDivisionError&lt;/span&gt;: &lt;span class="n"&gt;integer division or modulo by zero&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;concurrent.futures.process.BrokenProcessPool&lt;/span&gt;: &lt;span class="n"&gt;A process in the process pool was terminated abruptly while the future was running or pending.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;...or a worker can die some time later,
which we can help along with a custom &lt;code&gt;timer&lt;/code&gt;:&lt;sup class="footnote-ref" id="fnref-8"&gt;&lt;a href="#fn-8"&gt;8&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@contextmanager&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;terminate_child&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Timer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;interval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;psutil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Process&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;children&lt;/span&gt;&lt;span class="p"&gt;()[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;terminate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;yield&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ProcessPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;terminate_child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[ one second later ]&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;concurrent.futures.process.BrokenProcessPool&lt;/span&gt;: &lt;span class="n"&gt;A process in the process pool was terminated abruptly while the future was running or pending.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now let's see &lt;em&gt;our&lt;/em&gt; executor:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ProcessThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;terminate_child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[ one second later ]&lt;/span&gt;
&lt;span class="go"&gt;[ ... ]&lt;/span&gt;
&lt;span class="go"&gt;[ still waiting ]&lt;/span&gt;
&lt;span class="go"&gt;[ ... ]&lt;/span&gt;
&lt;span class="go"&gt;[ hello? ]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If the dead worker is not around to send back results,
its futures never get completed,
and &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.map"&gt;map()&lt;/a&gt; keeps waiting until the end of time,
when the expected behavior is to detect when this happens, and
fail all pending tasks with &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.process.BrokenProcessPool"&gt;BrokenProcessPool&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a name="handle-inner"&gt;&lt;/a&gt;
Before we do that, though, let's address a more specific issue.&lt;/p&gt;
&lt;p&gt;If &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.map"&gt;map()&lt;/a&gt; hasn't finished submitting tasks when the worker dies,
&lt;code&gt;inner&lt;/code&gt; fails with &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.process.BrokenProcessPool"&gt;BrokenProcessPool&lt;/a&gt;,
which right now we're ignoring entirely.
While we don't need to do anything about it in particular
because it gets covered by handling the general case,
we should still propagate &lt;em&gt;all&lt;/em&gt; errors to the &lt;code&gt;outer&lt;/code&gt; task anyway.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# ...&lt;/span&gt;
        &lt;span class="n"&gt;inner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_submit&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;task_id&lt;/span&gt;
        &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_done_callback&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__handle_inner&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;__handle_inner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;task_id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exception&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="n"&gt;inner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exception&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This fixes the case where a worker dies almost instantly:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ProcessThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;terminate_child&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;concurrent.futures.process.BrokenProcessPool&lt;/span&gt;: &lt;span class="n"&gt;A process in the process pool was terminated abruptly while the future was running or pending.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;p&gt;For the general case,
we need to check if the executor is broken
– but how?
We've already decided we don't want to depend on internals,
so we can't use &lt;a class="external" href="https://github.com/python/cpython/blob/db7ad1c89f8b8f0319ec2f3a20f2f3c226a406ed/Lib/concurrent/futures/process.py#L706"&gt;Process​Pool​Executor.​​_broken&lt;/a&gt;.
Maybe we can submit a dummy task and see if it fails instead:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;__check_broken&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cancel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;concurrent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BrokenExecutor&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;shutdown&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Using it is a bit involved, but not completely awful:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;__handle_results&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;last_broken_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;last_broken_check&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;.1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__check_broken&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                    &lt;span class="k"&gt;break&lt;/span&gt;
                &lt;span class="n"&gt;last_broken_check&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;

            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__result_queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Empty&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;return&lt;/span&gt;

            &lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;:=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__tasks&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;outer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__tasks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;popitem&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="n"&gt;outer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_exception&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When there's a steady stream of results coming in,
we don't want to check too often,
so we enforce a minimum delay between checks.
When there are &lt;em&gt;no&lt;/em&gt; results coming in,
we want to check regularly,
so we use the &lt;a class="external" href="https://docs.python.org/3/library/multiprocessing.html#multiprocessing.Queue.get"&gt;Queue.get()&lt;/a&gt; timeout to avoid waiting forever.
If the check fails, we break out of the loop and fail the pending tasks.
Like so:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ProcessThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;initializer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;executor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timer&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;terminate_child&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;concurrent.futures.process.BrokenProcessPool&lt;/span&gt;: &lt;span class="n"&gt;A child process terminated abruptly, the process pool is not usable anymore&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/ptpe/cool.jpg" alt="cool smoking cat wearing denim jacket and sunglasses" /&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;So, yeah, I think we're done.
Here's the final &lt;a class="attachment" href="/_file/ptpe/ptpe.py"&gt;executor&lt;/a&gt; and &lt;a class="attachment" href="/_file/ptpe/bench.py"&gt;benchmark&lt;/a&gt; code.&lt;/p&gt;
&lt;p&gt;Some features left as an exercise for the reader:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;providing a &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#threadpoolexecutor"&gt;ThreadPoolExecutor&lt;/a&gt; initializer&lt;/li&gt;
&lt;li&gt;using other &lt;a class="external" href="https://docs.python.org/3.14/library/multiprocessing.html#contexts-and-start-methods"&gt;start methods&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.shutdown"&gt;shutdown()&lt;/a&gt;'s &lt;code&gt;cancel_futures&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/ptpe&amp;t=Process%E2%80%8BThread%E2%80%8BPool%E2%80%8BExecutor%3A%20when%20I%E2%80%8D/%E2%80%8DO%20becomes%20CPU-bound"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Process%E2%80%8BThread%E2%80%8BPool%E2%80%8BExecutor%3A%20when%20I%E2%80%8D/%E2%80%8DO%20becomes%20CPU-bound%20https%3A//death.andgravity.com/ptpe"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/ptpe&amp;title=Process%E2%80%8BThread%E2%80%8BPool%E2%80%8BExecutor%3A%20when%20I%E2%80%8D/%E2%80%8DO%20becomes%20CPU-bound"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/ptpe"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Process%E2%80%8BThread%E2%80%8BPool%E2%80%8BExecutor%3A%20when%20I%E2%80%8D/%E2%80%8DO%20becomes%20CPU-bound&amp;url=https%3A//death.andgravity.com/ptpe&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;h2 id="bonus-free-threading"&gt;Bonus: free threading&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bonus-free-threading" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;You may have heard people being excited
about the experimental &lt;a class="external" href="https://docs.python.org/3/whatsnew/3.13.html#free-threaded-cpython"&gt;free threading&lt;/a&gt; support added in Python 3.13,
which allows running Python code on multiple CPUs.&lt;/p&gt;
&lt;p&gt;And for good reason:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="go"&gt;$ python3.13t&lt;/span&gt;
&lt;span class="go"&gt;Python 3.13.2 experimental free-threading build&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;concurrent.futures&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;bench&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;init_client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 8.224&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 6.193&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;benchmark&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;120&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;elapsed: 2.323&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;3.6x&lt;/strong&gt; over to the &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-global-interpreter-lock"&gt;GIL&lt;/a&gt; version,
with none of the shenanigans in this article!&lt;/p&gt;
&lt;p&gt;Alas, packages with extensions need to be updated to support it:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;psutil&lt;/span&gt;
&lt;span class="go"&gt;zsh: segmentation fault  python3.13t&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;...but the ecosystem is &lt;a class="external" href="https://hugovk.github.io/free-threaded-wheels/"&gt;slowly catching up&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/ptpe/patient.jpg" alt="cat patiently waiting on balcony" /&gt;&lt;/p&gt;



&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        If you&amp;#39;ve made it this far, you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/lru-cache"&gt;
            This is not interview advice: a priority-expiry LRU cache in Python without heaps or trees
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;At least, all we can use for pure-Python code.
I‍/‍O always releases the &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-global-interpreter-lock"&gt;global interpreter lock&lt;/a&gt;,
and so do some extension modules. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;The psutil documentation for &lt;a class="external" href="https://psutil.readthedocs.io/en/latest/#psutil.Process.memory_full_info"&gt;memory_full_info()&lt;/a&gt;
explains the difference quite nicely and links to further resources,
because &lt;a class="internal" href="/output#good-libraries-educate"&gt;good libraries educate&lt;/a&gt;. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-3"&gt;&lt;p&gt;You may have to run Python as root to get the USS of child processes. &lt;a href="#fnref-3" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-4"&gt;&lt;p&gt;And no, &lt;a class="external" href="https://docs.python.org/3/library/asyncio.html"&gt;asyncio&lt;/a&gt; is not a solution,
since the event loop runs in a single thread,
so you'd still need to run one event loop per CPU in dedicated processes. &lt;a href="#fnref-4" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-5"&gt;&lt;p&gt;We could have used &lt;a class="external" href="https://en.wikipedia.org/wiki/Composition_over_inheritance"&gt;composition&lt;/a&gt; instead,
but then we'd have to implement the full &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor"&gt;Executor&lt;/a&gt; interface,
defining each method explicitly to delegate to the inner process pool executor,
and keep things up to date when the interface gets new methods
(and we'd have no way to trick the inner executor's &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.map"&gt;map()&lt;/a&gt; to use our &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.submit"&gt;submit()&lt;/a&gt;,
so we'd have to implement it from scratch).&lt;/p&gt;
&lt;p&gt;Yet another option would be to use both inheritance &lt;em&gt;and&lt;/em&gt; composition –
inherit the &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor"&gt;Executor&lt;/a&gt; base class directly for the &lt;a class="external" href="https://github.com/python/cpython/blob/3.13/Lib/concurrent/futures/_base.py#L583-L648"&gt;common methods&lt;/a&gt;
(assuming they're defined there and not in subclasses),
and delegate to the inner executor only where needed
(likely just &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.map"&gt;map()&lt;/a&gt; and &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.shutdown"&gt;shutdown()&lt;/a&gt;).
But, the only difference from the current code would be
that it'd say &lt;code&gt;self._inner&lt;/code&gt; instead of &lt;code&gt;super()&lt;/code&gt; in a few places,
so it's not really worth it, in my opinion. &lt;a href="#fnref-5" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-6"&gt;&lt;p&gt;A previous version of this code
attempted to &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.Executor.shutdown"&gt;shutdown()&lt;/a&gt; the thread pool executor
using &lt;a class="external" href="https://docs.python.org/3/library/atexit.html"&gt;atexit&lt;/a&gt;,
but since &lt;a class="external" href="https://docs.python.org/3/library/atexit.html"&gt;atexit&lt;/a&gt; functions run &lt;em&gt;after&lt;/em&gt; non-daemon threads finish,
it wasn't actually doing anything.
Not shutting it down seems to work for now,
but we may still need do it
to support &lt;code&gt;shutdown(​cancel_futures=​True)&lt;/code&gt; properly. &lt;a href="#fnref-6" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-7"&gt;&lt;p&gt;Check out
&lt;a class="external" href="https://github.com/nilp0inter/threadedprocess"&gt;nilp0inter/threadedprocess&lt;/a&gt;
for an idea of what that looks like. &lt;a href="#fnref-7" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-8"&gt;&lt;p&gt;&lt;code&gt;pkill -fn '[Pp]ython'&lt;/code&gt; would've done it too,
but it gets tedious if you do it a lot,
and it's a different command on Windows. &lt;a href="#fnref-8" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/ptpe" rel="alternate"/>
    <summary>...in which we build a hybrid concurrent.futures executor that runs I‍/‍O bound tasks on all available CPUs, thus evading the limitations imposed by the dreaded global interpreter lock on the humble ThreadPoolExecutor.</summary>
    <published>2025-04-18T18:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/reader-3-16">
    <id>https://death.andgravity.com/reader-3-16</id>
    <title>reader 3.16 released – Archived feed</title>
    <updated>2024-12-09T18:00:00+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;I'm happy to announce version 3.16 of &lt;strong&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/strong&gt;, a Python feed reader library.&lt;/p&gt;
&lt;h2 id="what-s-new"&gt;What's new?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-new" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Here are the highlights since &lt;a class="internal" href="/reader-3-15"&gt;reader 3.15&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="archived-feed"&gt;Archived feed&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#archived-feed" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;It is now possible to archive selected entries to a special &amp;quot;archived&amp;quot; feed,
so they can be preserved once the original feed is deleted;
this is similar to the Tiny Tiny RSS &lt;a class="external" href="https://tt-rss.org/wiki/ArchivedFeed/"&gt;feature of the same name&lt;/a&gt;
(which is where I got the idea in the first place).&lt;/p&gt;
&lt;p&gt;At high level,
this is available in the web app as an &amp;quot;archive all&amp;quot; button
that archives currently-visible entries for a feed;
in the &lt;a class="external" href="https://github.com/lemon24/reader/issues/318"&gt;re-design&lt;/a&gt;,
the plan is to archive important entries by default as part of
the &amp;quot;delete feed&amp;quot; confirmation page.
It may also be a good idea
to make this the default through a plugin before then,
so that user interaction is not required for it to happen.&lt;/p&gt;
&lt;p&gt;At low level, this is enabled by &lt;a class="external" href="https://reader.readthedocs.io/en/stable/api.html#reader.utils.archive_entries"&gt;archive_entries()&lt;/a&gt;
(I finally gave up and added a &lt;code&gt;utils&lt;/code&gt; module 😅),
and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/api.html#reader.Reader.copy_entry"&gt;copy_entry()&lt;/a&gt; method.&lt;/p&gt;
&lt;h3 id="entry-source"&gt;Entry source&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#entry-source" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; now parses and stores the entry &lt;a class="external" href="https://reader.readthedocs.io/en/stable/api.html#reader.Entry.source"&gt;source&lt;/a&gt;,
containing metadata about the source feed if the entry is a copy
– this can be the case when a feed aggregates articles from other feeds.
The source URL can be used for filtering entries,
and its title is part of the feed title during searches.&lt;/p&gt;
&lt;p&gt;This was a side-project for the &lt;a class="anchor" href="#archived-feed"&gt;archived feed&lt;/a&gt; functionality,
since the source of archived entries gets set to their original feed.&lt;/p&gt;
&lt;h3 id="bug-fixes"&gt;Bug fixes&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bug-fixes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Also during &lt;a class="anchor" href="#archived-feed"&gt;archived feed&lt;/a&gt; implementation,
I found out foreign keys were disabled in threads other
than the one that created the reader instance,
so e.g. deleting a feed from another thread would not delete its entries.
This is now fixed.&lt;/p&gt;
&lt;p&gt;The bug existed since &lt;a class="external" href="https://reader.readthedocs.io/en/stable/guide.html#threading"&gt;multi-threaded use&lt;/a&gt; became allowed in &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-2-15"&gt;version 2.15&lt;/a&gt;.
Serving the web application with the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/cli.html#reader-serve"&gt;serve&lt;/a&gt; command is known to be affected;
serving it without threads (e.g. the default uWSGI configuration)
should &lt;em&gt;not&lt;/em&gt; be affected.&lt;/p&gt;
&lt;section class="admonition attention"&gt;
&lt;p class="admonition-title"&gt;Attention&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Your database may be in an inconsistent state.&lt;/strong&gt;
The migration to 3.16 will &lt;a class="external" href="https://www.sqlite.org/pragma.html#pragma_foreign_key_check"&gt;check&lt;/a&gt; for this on first use;
issues will be reported as a &lt;code&gt;FOREIGN KEY constraint failed&lt;/code&gt; storage integrity error;
see the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-3-16"&gt;changelog&lt;/a&gt; for details.&lt;/p&gt;
&lt;/section&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.
For more details, see the full &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-3-16"&gt;changelog&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Want to contribute?&lt;/strong&gt;
Check out the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/contributing.html"&gt;docs&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/dev.html#roadmap"&gt;roadmap&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/reader-3-16&amp;t=reader%203.16%20released%20%E2%80%93%20Archived%20feed"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=reader%203.16%20released%20%E2%80%93%20Archived%20feed%20https%3A//death.andgravity.com/reader-3-16"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/reader-3-16&amp;title=reader%203.16%20released%20%E2%80%93%20Archived%20feed"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/reader-3-16"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=reader%203.16%20released%20%E2%80%93%20Archived%20feed&amp;url=https%3A//death.andgravity.com/reader-3-16&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;h2 id="what-is-reader"&gt;What is &lt;em&gt;reader&lt;/em&gt;?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;reader&lt;/strong&gt; takes care
of the core functionality required by a feed reader,
so you can focus on what makes &lt;strong&gt;yours&lt;/strong&gt; different.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-2-0/reader.png" alt="reader in action" /&gt;
&lt;em&gt;reader&lt;/em&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retrieve, store, and manage &lt;strong&gt;Atom&lt;/strong&gt;, &lt;strong&gt;RSS&lt;/strong&gt;, and &lt;strong&gt;JSON&lt;/strong&gt; feeds&lt;/li&gt;
&lt;li&gt;mark articles as read or important&lt;/li&gt;
&lt;li&gt;add arbitrary tags/metadata to feeds and articles&lt;/li&gt;
&lt;li&gt;filter feeds and articles&lt;/li&gt;
&lt;li&gt;full-text search articles&lt;/li&gt;
&lt;li&gt;get statistics on feed and user activity&lt;/li&gt;
&lt;li&gt;write plugins to extend its functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...all these with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stable, clearly documented API&lt;/li&gt;
&lt;li&gt;excellent test coverage&lt;/li&gt;
&lt;li&gt;fully typed Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find out more, check out the &lt;a class="external" href="https://github.com/lemon24/reader"&gt;GitHub repo&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/"&gt;docs&lt;/a&gt;,
or give the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/tutorial.html"&gt;tutorial&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2 id="why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-use-a-feed-reader-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Have you been unhappy with existing feed readers and wanted to make your own, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never knew where to start?&lt;/li&gt;
&lt;li&gt;it seemed like too much work?&lt;/li&gt;
&lt;li&gt;you don't like writing backend code?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you already working with &lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/"&gt;feedparser&lt;/a&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;want an easier way to store, filter, sort and search feeds and entries?&lt;/li&gt;
&lt;li&gt;want to get back type-annotated objects instead of dicts?&lt;/li&gt;
&lt;li&gt;want to restrict or deny file-system access?&lt;/li&gt;
&lt;li&gt;want to change the way feeds are retrieved by using &lt;a class="external" href="https://requests.readthedocs.io"&gt;Requests&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to also support &lt;a class="external" href="https://jsonfeed.org/"&gt;JSON Feed&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to support custom information sources?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... while still supporting all the feed types feedparser does?&lt;/p&gt;
&lt;p&gt;If you answered yes to any of the above, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;h2 id="the-reader-philosophy"&gt;The &lt;em&gt;reader&lt;/em&gt; philosophy&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-reader-philosophy" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is a library&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is for the long term&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is extensible&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is stable (within reason)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is simple to use; API matters&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; features work well together&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is tested&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is documented&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; has minimal dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="why-make-your-own-feed-reader"&gt;Why make your own feed reader?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-make-your-own-feed-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;have full control over your data&lt;/li&gt;
&lt;li&gt;control what features it has or doesn't have&lt;/li&gt;
&lt;li&gt;decide how much you pay for it&lt;/li&gt;
&lt;li&gt;make sure it doesn't get closed while you're still using it&lt;/li&gt;
&lt;li&gt;really, it's &lt;a class="external" href="https://rachelbythebay.com/w/2011/10/26/fred/"&gt;easier than you think&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously, this may not be your cup of tea, but if it is, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
</content>
    <link href="https://death.andgravity.com/reader-3-16" rel="alternate"/>
    <published>2024-12-09T18:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/reader-3-15">
    <id>https://death.andgravity.com/reader-3-15</id>
    <title>reader 3.15 released – Retry-After</title>
    <updated>2024-11-13T18:00:00+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;I'm happy to announce version 3.15 of &lt;strong&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/strong&gt;, a Python feed reader library.&lt;/p&gt;
&lt;h2 id="what-s-new"&gt;What's new?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-new" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Here are the highlights since &lt;a class="internal" href="/reader-3-13"&gt;reader 3.13&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="retry-after"&gt;Retry-After&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#retry-after" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Now that it supports &lt;a class="internal" href="/reader-3-13#scheduled-updates"&gt;scheduled updates&lt;/a&gt;,
&lt;em&gt;reader&lt;/em&gt; can honor the &lt;a class="external" href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After"&gt;Retry-After&lt;/a&gt; HTTP header sent with
429 Too Many Requests or 503 Service Unavailable responses.&lt;/p&gt;
&lt;p&gt;Adding this required an extensive rework of the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/internal.html#parser"&gt;parser internal API&lt;/a&gt;,
but I'd say it was worth it, since we're getting quite close to it
&lt;a class="internal" href="/reader-3-4#parser-internal-api"&gt;becoming stable&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next up in HTTP compliance is to do more on behalf of the user:
bump the update interval &lt;a class="external" href="https://github.com/lemon24/reader/issues/307"&gt;on repeated throttling&lt;/a&gt;,
and handle &lt;a class="external" href="https://github.com/lemon24/reader/issues/246"&gt;gone and redirected feeds&lt;/a&gt; accordingly.&lt;/p&gt;
&lt;h3 id="faster-tag-filters-feed-slugs"&gt;Faster tag filters, feed slugs&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#faster-tag-filters-feed-slugs" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;OR-only tag filters like &lt;code&gt;get_feeds(tags=[['one', 'two']])&lt;/code&gt; now use an index.&lt;/p&gt;
&lt;p&gt;This is useful for maintaining a reverse mapping to feeds/entries,
like the &lt;a class="external" href="https://reader.readthedocs.io/en/3.x/plugins.html#feed-slugs"&gt;feed slugs recipe&lt;/a&gt; does to add support for user-defined short URLs:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;https://death.andgravity.com/_feed/index.xml&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_feed_slug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;andgravity&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_feed_by_slug&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;andgravity&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;Feed(url=&amp;#39;https://death.andgravity.com/_feed/index.xml&amp;#39;, ...)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(Interested in adopting this recipe as a real plugin? &lt;a class="external" href="https://reader.readthedocs.io/en/latest/contributing.html#prs"&gt;Submit a pull request!&lt;/a&gt;)&lt;/p&gt;
&lt;h3 id="enclosure-tags-improvements"&gt;enclosure_tags improvements&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#enclosure-tags-improvements" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;a class="external" href="https://reader.readthedocs.io/en/3.x/plugins.html#module-reader._plugins.enclosure_tags"&gt;enclosure_tags&lt;/a&gt; plugin fixes &lt;a class="external" href="https://en.wikipedia.org/wiki/ID3"&gt;ID3 tags&lt;/a&gt; for MP3 enclosures like podcasts.&lt;/p&gt;
&lt;p&gt;I've changed the implementation to rewrite tags on the fly,
instead of downloading the entire file, rewriting tags,
and then sending it to the user;
this should allow browsers to display accurate download progress.&lt;/p&gt;
&lt;p&gt;Some other, smaller improvements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Set genre to &lt;em&gt;Podcast&lt;/em&gt; if the feed has any tag containing &amp;quot;podcast&amp;quot;.&lt;/li&gt;
&lt;li&gt;Prefer feed user title to feed title if available.&lt;/li&gt;
&lt;li&gt;Use feed title as artist, instead of author.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="using-the-installed-feedparser"&gt;Using the installed feedparser&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#using-the-installed-feedparser" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Because &lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/"&gt;feedparser&lt;/a&gt; makes PyPI releases at a lower cadence,
&lt;em&gt;reader&lt;/em&gt; has been using a vendored version of feedparser's &lt;a class="external" href="https://github.com/kurtmckee/feedparser"&gt;develop&lt;/a&gt; branch
&lt;a class="internal" href="/reader-2-11#memory-usage-improvements"&gt;for some time&lt;/a&gt;.
It is now possible to &lt;a class="external" href="https://reader.readthedocs.io/en/3.x/install.html#no-vendored-feedparser"&gt;opt out of this behavior&lt;/a&gt;
and make &lt;em&gt;reader&lt;/em&gt; use the installed &lt;code&gt;feedparser&lt;/code&gt; package.&lt;/p&gt;
&lt;h3 id="python-versions"&gt;Python versions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#python-versions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; 3.14 (released back in July) adds support for Python 3.13.&lt;/p&gt;
&lt;h2 id="upcoming-changes"&gt;Upcoming changes&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#upcoming-changes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id="replacing-requests-with-httpx"&gt;Replacing Requests with HTTPX&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#replacing-requests-with-httpx" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; relies on &lt;a class="external" href="https://requests.readthedocs.io/"&gt;Requests&lt;/a&gt; to retrieve feeds from the internet;
among others, it replaces feedparser's use of &lt;code&gt;urllib&lt;/code&gt;
to make it easier to &lt;a class="external" href="https://reader.readthedocs.io/en/3.x/internal.html#reader._parser.Parser.session_factory"&gt;write plugins&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;However, Requests has a few issues that may never get fixed
because it is in a feature-freeze –
mainly the lack of default timeouts,
underpowered response hooks, and no request hooks,
all of which I had to work around in &lt;em&gt;reader&lt;/em&gt; code.&lt;/p&gt;
&lt;p&gt;So, &lt;a class="external" href="https://github.com/lemon24/reader/issues/360"&gt;I've been looking&lt;/a&gt; into using &lt;a class="external" href="https://www.python-httpx.org/"&gt;HTTPX&lt;/a&gt; instead.&lt;/p&gt;
&lt;p&gt;Some reasons to use HTTPX:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;largely Requests-compatible API and feature set&lt;/li&gt;
&lt;li&gt;while the ecosystem is probably not comparable,
it is actively maintained, popular enough,
and the basics (mocking, auth) are there&lt;/li&gt;
&lt;li&gt;strict timeouts by default (and more kinds than Requests)&lt;/li&gt;
&lt;li&gt;request/response hooks&lt;/li&gt;
&lt;li&gt;URL normalization (needed by the parser)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Bad reasons to move away from Requests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;lack of async support –
I have no plan to use async in &lt;em&gt;reader&lt;/em&gt; at this point&lt;/li&gt;
&lt;li&gt;lack of HTTP/2 support –
coming soon in urllib3 (and by extension, Requests?);
also, &lt;em&gt;reader&lt;/em&gt; makes rare requests to many different hosts,
I'm not sure it would benefit all that much from HTTP/2&lt;/li&gt;
&lt;li&gt;lack of Brotli/Zstandard compresson support –
urllib3 already supports them&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Reasons to not move to HTTPX:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;not 1.0 yet (but coming soon)&lt;/li&gt;
&lt;li&gt;not as battle-tested as Requests (but can use urllib3 as transport)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, when is this happening?
Nothing's actually burning, so soon™, but not &lt;em&gt;that&lt;/em&gt; soon;
watch &lt;a class="external" href="https://github.com/lemon24/reader/issues/360"&gt;#360&lt;/a&gt; if you're interested in this.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.
For more details, see the full &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-3-15"&gt;changelog&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Want to contribute?&lt;/strong&gt;
Check out the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/contributing.html"&gt;docs&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/dev.html#roadmap"&gt;roadmap&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/reader-3-15&amp;t=reader%203.15%20released%20%E2%80%93%20Retry-After"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=reader%203.15%20released%20%E2%80%93%20Retry-After%20https%3A//death.andgravity.com/reader-3-15"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/reader-3-15&amp;title=reader%203.15%20released%20%E2%80%93%20Retry-After"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/reader-3-15"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=reader%203.15%20released%20%E2%80%93%20Retry-After&amp;url=https%3A//death.andgravity.com/reader-3-15&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;h2 id="what-is-reader"&gt;What is &lt;em&gt;reader&lt;/em&gt;?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;reader&lt;/strong&gt; takes care
of the core functionality required by a feed reader,
so you can focus on what makes &lt;strong&gt;yours&lt;/strong&gt; different.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-2-0/reader.png" alt="reader in action" /&gt;
&lt;em&gt;reader&lt;/em&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retrieve, store, and manage &lt;strong&gt;Atom&lt;/strong&gt;, &lt;strong&gt;RSS&lt;/strong&gt;, and &lt;strong&gt;JSON&lt;/strong&gt; feeds&lt;/li&gt;
&lt;li&gt;mark articles as read or important&lt;/li&gt;
&lt;li&gt;add arbitrary tags/metadata to feeds and articles&lt;/li&gt;
&lt;li&gt;filter feeds and articles&lt;/li&gt;
&lt;li&gt;full-text search articles&lt;/li&gt;
&lt;li&gt;get statistics on feed and user activity&lt;/li&gt;
&lt;li&gt;write plugins to extend its functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...all these with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stable, clearly documented API&lt;/li&gt;
&lt;li&gt;excellent test coverage&lt;/li&gt;
&lt;li&gt;fully typed Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find out more, check out the &lt;a class="external" href="https://github.com/lemon24/reader"&gt;GitHub repo&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/"&gt;docs&lt;/a&gt;,
or give the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/tutorial.html"&gt;tutorial&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2 id="why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-use-a-feed-reader-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Have you been unhappy with existing feed readers and wanted to make your own, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never knew where to start?&lt;/li&gt;
&lt;li&gt;it seemed like too much work?&lt;/li&gt;
&lt;li&gt;you don't like writing backend code?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you already working with &lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/"&gt;feedparser&lt;/a&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;want an easier way to store, filter, sort and search feeds and entries?&lt;/li&gt;
&lt;li&gt;want to get back type-annotated objects instead of dicts?&lt;/li&gt;
&lt;li&gt;want to restrict or deny file-system access?&lt;/li&gt;
&lt;li&gt;want to change the way feeds are retrieved by using &lt;a class="external" href="https://requests.readthedocs.io/"&gt;Requests&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to also support &lt;a class="external" href="https://jsonfeed.org/"&gt;JSON Feed&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to support custom information sources?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... while still supporting all the feed types feedparser does?&lt;/p&gt;
&lt;p&gt;If you answered yes to any of the above, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;h2 id="the-reader-philosophy"&gt;The &lt;em&gt;reader&lt;/em&gt; philosophy&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-reader-philosophy" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is a library&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is for the long term&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is extensible&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is stable (within reason)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is simple to use; API matters&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; features work well together&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is tested&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is documented&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; has minimal dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="why-make-your-own-feed-reader"&gt;Why make your own feed reader?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-make-your-own-feed-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;have full control over your data&lt;/li&gt;
&lt;li&gt;control what features it has or doesn't have&lt;/li&gt;
&lt;li&gt;decide how much you pay for it&lt;/li&gt;
&lt;li&gt;make sure it doesn't get closed while you're still using it&lt;/li&gt;
&lt;li&gt;really, it's &lt;a class="external" href="https://rachelbythebay.com/w/2011/10/26/fred/"&gt;easier than you think&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously, this may not be your cup of tea, but if it is, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
</content>
    <link href="https://death.andgravity.com/reader-3-15" rel="alternate"/>
    <published>2024-11-11T11:11:11+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/reader-3-13">
    <id>https://death.andgravity.com/reader-3-13</id>
    <title>reader 3.13 released – scheduled updates</title>
    <updated>2024-06-21T17:00:00+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;I'm happy to announce version 3.13 of &lt;strong&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/strong&gt;, a Python feed reader library.&lt;/p&gt;
&lt;h2 id="what-s-new"&gt;What's new?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-new" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Here are the highlights since &lt;a class="internal" href="/reader-3-12"&gt;reader 3.12&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="scheduled-updates"&gt;Scheduled updates&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#scheduled-updates" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; now allows updating feeds at different rates via &lt;a class="external" href="https://reader.readthedocs.io/en/latest/guide.html#scheduled-updates"&gt;scheduled updates&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The way it works is quite simple:
each feed has an update interval that
determines when the feed should be updated next;
calling &lt;a class="external" href="https://reader.readthedocs.io/en/latest/api.html#reader.Reader.update_feeds"&gt;&lt;code&gt;update_feeds(​scheduled​=True)&lt;/code&gt;&lt;/a&gt; updates only feeds that
should be updated at or before the current time.&lt;/p&gt;
&lt;p&gt;The interval can be configured by the user
globally or per-feed through the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/api.html#reader.types.UpdateConfig"&gt;&lt;code&gt;.reader​.update&lt;/code&gt;&lt;/a&gt; tag.
In addition, you can specify a jitter;
for an interval of 24 hours, a jitter of 0.25 means
the update will occur any time in the first 6 hours of the interval.&lt;/p&gt;
&lt;p&gt;In the future,
the same mechanism will be used to handle &lt;a class="external" href="https://github.com/lemon24/reader/issues/307"&gt;429 Too Many Requests&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="improved-documentation"&gt;Improved documentation&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#improved-documentation" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;As part of rewriting the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/guide.html#updating-feeds"&gt;Updating feeds&lt;/a&gt; user guide section
to talk about scheduled updates,
I've added a new section about &lt;a class="external" href="https://reader.readthedocs.io/en/latest/guide.html#saving-bandwidth"&gt;being polite to servers&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Also, we have a new recipe for &lt;a class="external" href="https://reader.readthedocs.io/en/latest/internal.html#adding-custom-headers-when-retrieving-feeds"&gt;adding custom headers when retrieving feeds&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="mark-as-read-reruns"&gt;mark_as_read reruns&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#mark-as-read-reruns" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;You can now re-run the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/plugins.html#reader-mark-as-read"&gt;mark_as_read&lt;/a&gt; plugin for existing entries
by adding the &lt;code&gt;.reader​.mark-as-read​.once&lt;/code&gt; tag to a feed.
Thanks to Michael Han for the pull request!&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.
For more details, see the full &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-3-13"&gt;changelog&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Want to contribute?&lt;/strong&gt;
Check out the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/contributing.html"&gt;docs&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/dev.html#roadmap"&gt;roadmap&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/reader-3-13&amp;t=reader%203.13%20released%20%E2%80%93%20scheduled%20updates"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=reader%203.13%20released%20%E2%80%93%20scheduled%20updates%20https%3A//death.andgravity.com/reader-3-13"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/reader-3-13&amp;title=reader%203.13%20released%20%E2%80%93%20scheduled%20updates"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/reader-3-13"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=reader%203.13%20released%20%E2%80%93%20scheduled%20updates&amp;url=https%3A//death.andgravity.com/reader-3-13&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;h2 id="what-is-reader"&gt;What is &lt;em&gt;reader&lt;/em&gt;?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;reader&lt;/strong&gt; takes care
of the core functionality required by a feed reader,
so you can focus on what makes &lt;strong&gt;yours&lt;/strong&gt; different.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-2-0/reader.png" alt="reader in action" /&gt;
&lt;em&gt;reader&lt;/em&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retrieve, store, and manage &lt;strong&gt;Atom&lt;/strong&gt;, &lt;strong&gt;RSS&lt;/strong&gt;, and &lt;strong&gt;JSON&lt;/strong&gt; feeds&lt;/li&gt;
&lt;li&gt;mark articles as read or important&lt;/li&gt;
&lt;li&gt;add arbitrary tags/metadata to feeds and articles&lt;/li&gt;
&lt;li&gt;filter feeds and articles&lt;/li&gt;
&lt;li&gt;full-text search articles&lt;/li&gt;
&lt;li&gt;get statistics on feed and user activity&lt;/li&gt;
&lt;li&gt;write plugins to extend its functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...all these with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stable, clearly documented API&lt;/li&gt;
&lt;li&gt;excellent test coverage&lt;/li&gt;
&lt;li&gt;fully typed Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find out more, check out the &lt;a class="external" href="https://github.com/lemon24/reader"&gt;GitHub repo&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/"&gt;docs&lt;/a&gt;,
or give the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/tutorial.html"&gt;tutorial&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2 id="why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-use-a-feed-reader-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Have you been unhappy with existing feed readers and wanted to make your own, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never knew where to start?&lt;/li&gt;
&lt;li&gt;it seemed like too much work?&lt;/li&gt;
&lt;li&gt;you don't like writing backend code?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you already working with &lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/"&gt;feedparser&lt;/a&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;want an easier way to store, filter, sort and search feeds and entries?&lt;/li&gt;
&lt;li&gt;want to get back type-annotated objects instead of dicts?&lt;/li&gt;
&lt;li&gt;want to restrict or deny file-system access?&lt;/li&gt;
&lt;li&gt;want to change the way feeds are retrieved by using &lt;a class="external" href="https://requests.readthedocs.io"&gt;Requests&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to also support &lt;a class="external" href="https://jsonfeed.org/"&gt;JSON Feed&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to support custom information sources?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... while still supporting all the feed types feedparser does?&lt;/p&gt;
&lt;p&gt;If you answered yes to any of the above, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;h2 id="the-reader-philosophy"&gt;The &lt;em&gt;reader&lt;/em&gt; philosophy&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-reader-philosophy" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is a library&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is for the long term&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is extensible&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is stable (within reason)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is simple to use; API matters&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; features work well together&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is tested&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is documented&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; has minimal dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="why-make-your-own-feed-reader"&gt;Why make your own feed reader?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-make-your-own-feed-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;have full control over your data&lt;/li&gt;
&lt;li&gt;control what features it has or doesn't have&lt;/li&gt;
&lt;li&gt;decide how much you pay for it&lt;/li&gt;
&lt;li&gt;make sure it doesn't get closed while you're still using it&lt;/li&gt;
&lt;li&gt;really, it's &lt;a class="external" href="https://rachelbythebay.com/w/2011/10/26/fred/"&gt;easier than you think&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously, this may not be your cup of tea, but if it is, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
</content>
    <link href="https://death.andgravity.com/reader-3-13" rel="alternate"/>
    <published>2024-06-21T17:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/reader-3-12">
    <id>https://death.andgravity.com/reader-3-12</id>
    <title>reader 3.12 released – split search index</title>
    <updated>2024-03-07T18:00:00+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;I'm happy to announce version 3.12 of &lt;strong&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/strong&gt;, a Python feed reader library.&lt;/p&gt;
&lt;h2 id="what-s-new"&gt;What's new?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-new" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Here are the highlights since &lt;a class="internal" href="/reader-3-10"&gt;reader 3.10&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="split-the-search-index-into-a-separate-database"&gt;Split the search index into a separate database&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#split-the-search-index-into-a-separate-database" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;a class="external" href="https://reader.readthedocs.io/en/latest/guide.html#fts"&gt;full-text search&lt;/a&gt; index can get almost as large as the actual data,
so I've split it into a separate, attached database,
which allows backing up only the main database.&lt;/p&gt;
&lt;p&gt;(I stole this idea from &lt;a class="external" href="https://crawshaw.io/blog/one-process-programming-notes"&gt;One process programming notes (with Go and SQLite)&lt;/a&gt;.)&lt;/p&gt;
&lt;h3 id="change-tracking-internal-api"&gt;Change tracking internal API&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#change-tracking-internal-api" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;To support the search index split,
Storage got a &lt;a class="external" href="https://reader.readthedocs.io/en/latest/internal.html#reader._types.ChangeTrackerType"&gt;change tracking API&lt;/a&gt;
that allows search implementations
to keep in sync with text content changes.&lt;/p&gt;
&lt;p&gt;This is a first step
towards &lt;strong&gt;search backends that aren't tightly-coupled&lt;/strong&gt; to a storage.
For example,
the SQLite storage uses its &lt;a class="external" href="https://www.sqlite.org/fts5.html"&gt;FTS5 extension&lt;/a&gt; for search,
and a PostgreSQL storage can use its own &lt;a class="external" href="https://www.postgresql.org/docs/current/textsearch.html"&gt;native support&lt;/a&gt;;
the new API allows either storage to use something like Elasticsearch.
(There's still no good way for search
to filter/sort results without storage cooperation,
so more work is needed here.)&lt;/p&gt;
&lt;p&gt;Also, it lays some of the groundwork for &lt;strong&gt;searchable tag values&lt;/strong&gt;
by having tag support already built into the API.&lt;/p&gt;
&lt;p&gt;Here's how change tracking works (&lt;a class="external" href="https://reader.readthedocs.io/en/latest/internal.html#reader._types.ChangeTrackerType"&gt;long version&lt;/a&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each entry has a 16 byte random sequence that changes when its text changes.&lt;/li&gt;
&lt;li&gt;Sequence changes get recorded and are available through the API.&lt;/li&gt;
&lt;li&gt;Search &lt;code&gt;update()&lt;/code&gt; processes pending changes and marks them as done.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;While simple on the surface,
this prevents a lot of potential concurrency issues
that needed special handling before.
For example, what if an entry changes during pre-processing,
before it is added to the search index?
You &lt;em&gt;could&lt;/em&gt; use a transaction,
but this may keep the database locked for too long.
Also, what about search backends
where you don't have transactions?&lt;/p&gt;
&lt;p&gt;I used &lt;a class="external" href="https://hypothesis.readthedocs.io/"&gt;Hypothesis&lt;/a&gt; and property-based testing
to &lt;a class="external" href="https://gist.github.com/lemon24/558955ad82ba2e4f50c0184c630c668c"&gt;validate the model&lt;/a&gt;,
so I'm ~99% sure it is correct.
A real model checker like TLA+ or Alloy may have been a better tool for it,
but I don't know how to use one at this point.&lt;/p&gt;
&lt;h3 id="filter-by-entry-tags"&gt;Filter by entry tags&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#filter-by-entry-tags" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;It is now possible to filter entries by &lt;a class="external" href="https://reader.readthedocs.io/en/latest/guide.html#resource-tags"&gt;entry tags&lt;/a&gt;:
&lt;code&gt;get_entries(tags=['tag'])&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I did this to see how it would look to implement
the &lt;em&gt;has_enclosures&lt;/em&gt; &lt;a class="external" href="https://reader.readthedocs.io/en/latest/api.html#reader.Reader.get_entries"&gt;get_entries()&lt;/a&gt; argument as a plugin
(it is possible, but &lt;a class="external" href="https://github.com/lemon24/reader/issues/327#issuecomment-1859147186"&gt;not really worth it&lt;/a&gt;).&lt;/p&gt;
&lt;h3 id="sqlite-storage-improvements"&gt;SQLite storage improvements&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#sqlite-storage-improvements" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;As part of a bigger storage refactoring,
I made a few small improvements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Enable &lt;a class="external" href="https://www.sqlite.org/wal.html"&gt;write-ahead logging&lt;/a&gt; only once, when the database is created.&lt;/li&gt;
&lt;li&gt;Vacuum the main database after migrations.&lt;/li&gt;
&lt;li&gt;Require at least SQLite 3.18, since it was required by &lt;a class="external" href="https://reader.readthedocs.io/en/latest/api.html#reader.Reader.update_search"&gt;update_search()&lt;/a&gt; anyway.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="python-versions"&gt;Python versions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#python-versions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; 3.11 (released back in December) adds support for Python 3.12.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.
For more details, see the full &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-3-12"&gt;changelog&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Want to contribute?&lt;/strong&gt;
Check out the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/contributing.html"&gt;docs&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/dev.html#roadmap"&gt;roadmap&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/reader-3-12&amp;t=reader%203.12%20released%20%E2%80%93%20split%20search%20index"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=reader%203.12%20released%20%E2%80%93%20split%20search%20index%20https%3A//death.andgravity.com/reader-3-12"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/reader-3-12&amp;title=reader%203.12%20released%20%E2%80%93%20split%20search%20index"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/reader-3-12"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=reader%203.12%20released%20%E2%80%93%20split%20search%20index&amp;url=https%3A//death.andgravity.com/reader-3-12&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;h2 id="what-is-reader"&gt;What is &lt;em&gt;reader&lt;/em&gt;?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;reader&lt;/strong&gt; takes care
of the core functionality required by a feed reader,
so you can focus on what makes &lt;strong&gt;yours&lt;/strong&gt; different.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-2-0/reader.png" alt="reader in action" /&gt;
&lt;em&gt;reader&lt;/em&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retrieve, store, and manage &lt;strong&gt;Atom&lt;/strong&gt;, &lt;strong&gt;RSS&lt;/strong&gt;, and &lt;strong&gt;JSON&lt;/strong&gt; feeds&lt;/li&gt;
&lt;li&gt;mark articles as read or important&lt;/li&gt;
&lt;li&gt;add arbitrary tags/metadata to feeds and articles&lt;/li&gt;
&lt;li&gt;filter feeds and articles&lt;/li&gt;
&lt;li&gt;full-text search articles&lt;/li&gt;
&lt;li&gt;get statistics on feed and user activity&lt;/li&gt;
&lt;li&gt;write plugins to extend its functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...all these with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stable, clearly documented API&lt;/li&gt;
&lt;li&gt;excellent test coverage&lt;/li&gt;
&lt;li&gt;fully typed Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find out more, check out the &lt;a class="external" href="https://github.com/lemon24/reader"&gt;GitHub repo&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/"&gt;docs&lt;/a&gt;,
or give the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/tutorial.html"&gt;tutorial&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2 id="why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-use-a-feed-reader-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Have you been unhappy with existing feed readers and wanted to make your own, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never knew where to start?&lt;/li&gt;
&lt;li&gt;it seemed like too much work?&lt;/li&gt;
&lt;li&gt;you don't like writing backend code?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you already working with &lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/"&gt;feedparser&lt;/a&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;want an easier way to store, filter, sort and search feeds and entries?&lt;/li&gt;
&lt;li&gt;want to get back type-annotated objects instead of dicts?&lt;/li&gt;
&lt;li&gt;want to restrict or deny file-system access?&lt;/li&gt;
&lt;li&gt;want to change the way feeds are retrieved by using &lt;a class="external" href="https://requests.readthedocs.io"&gt;Requests&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to also support &lt;a class="external" href="https://jsonfeed.org/"&gt;JSON Feed&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to support custom information sources?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... while still supporting all the feed types feedparser does?&lt;/p&gt;
&lt;p&gt;If you answered yes to any of the above, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;h2 id="the-reader-philosophy"&gt;The &lt;em&gt;reader&lt;/em&gt; philosophy&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-reader-philosophy" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is a library&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is for the long term&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is extensible&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is stable (within reason)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is simple to use; API matters&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; features work well together&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is tested&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is documented&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; has minimal dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="why-make-your-own-feed-reader"&gt;Why make your own feed reader?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-make-your-own-feed-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;have full control over your data&lt;/li&gt;
&lt;li&gt;control what features it has or doesn't have&lt;/li&gt;
&lt;li&gt;decide how much you pay for it&lt;/li&gt;
&lt;li&gt;make sure it doesn't get closed while you're still using it&lt;/li&gt;
&lt;li&gt;really, it's &lt;a class="external" href="https://rachelbythebay.com/w/2011/10/26/fred/"&gt;easier than you think&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously, this may not be your cup of tea, but if it is, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
</content>
    <link href="https://death.andgravity.com/reader-3-12" rel="alternate"/>
    <published>2024-03-07T07:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/lru-cache">
    <id>https://death.andgravity.com/lru-cache</id>
    <title>This is not interview advice: a priority-expiry LRU cache in Python without heaps or trees</title>
    <updated>2024-01-26T10:00:00+00:00</updated>
    <content type="html">&lt;p&gt;&lt;em&gt;It's not your fault I got nerdsniped,
but that doesn't matter.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Hi, I'm Adrian, and today we're implementing a
least recently used cache with priorities and expiry,
using &lt;strong&gt;only the Python standard library&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;This is a bIG TEch CoDINg InTerVIEW problem,
so we'll work hard to stay away from the correct™ data structures –
&lt;strong&gt;no heaps&lt;/strong&gt;, &lt;strong&gt;no binary search trees&lt;/strong&gt; –
but we'll end up with a decent solution anyway!&lt;/p&gt;
&lt;details class="toc" open&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#requirements"&gt;Requirements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#a-minimal-plausible-solution"&gt;A minimal plausible solution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#problem-expired-items-should-go-first"&gt;Problem: expired items should go first...&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#problem-name-priorityqueue-is-not-defined"&gt;Problem: name PriorityQueue is not defined&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#problem-low-priority-items-second"&gt;Problem: ...low priority items second&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#problem-we-re-deleting-items-in-three-places"&gt;Problem: we're deleting items in three places&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#problem-least-recently-used-items-last"&gt;Problem: ...least recently used items last&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#functools-lru-cache"&gt;functools.lru_cache()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#ordereddict"&gt;OrderedDict&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#problem-our-priority-queue-is-slow"&gt;Problem: our priority queue is slow&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#heapq"&gt;heapq&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#bisect"&gt;bisect&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#pop-optimization"&gt;pop() optimization&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#binary-search-trees"&gt;Binary search trees&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#sorted-containers"&gt;Sorted Containers&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#problem-sorted-containers-is-not-in-stdlib"&gt;Problem: Sorted Containers is not in stdlib&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#logarithmic-time"&gt;Logarithmic time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#bisect-redux"&gt;bisect, redux&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#conclusion"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="requirements"&gt;Requirements&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#requirements" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So you're at an interview and
have to implement a &lt;em&gt;priority-expiry LRU cache&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Maybe you get more details,
but maybe the problem is deliberately vague;
either way,
we can reconstruct the requirements from the name alone.&lt;/p&gt;
&lt;p&gt;A &lt;em&gt;cache&lt;/em&gt; is something that stores data
for future reuse,
usually the result of a slow computation or retrieval.
Each piece of data has an associated &lt;em&gt;key&lt;/em&gt;
used to retrieve it.
Most caches are &lt;em&gt;bounded&lt;/em&gt; in some way,
for example by limiting the number of items.&lt;/p&gt;
&lt;p&gt;The other words have to do with &lt;em&gt;eviction&lt;/em&gt;
– how and when items are removed.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Each item has a maximum age –
items that go past that are &lt;em&gt;expired&lt;/em&gt;,
so we don't return them.
It stands to reason this does not depend
on their priority or how full the cache is.&lt;/li&gt;
&lt;li&gt;Each item has a &lt;em&gt;priority&lt;/em&gt; –
when the cache fills up,
we evict items with lower priority
before those with higher priority.&lt;/li&gt;
&lt;li&gt;All other things being equal,
we evict items &lt;em&gt;least recently used&lt;/em&gt;
relative to others.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The problem may specify an API;
we can reconstruct that from first principles too.
Since the cache is basically a key-value store,
we can get away with two methods:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;set(key, value, maxage, priority)&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;get(key) -&amp;gt; value or None&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The problem may also suggest:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;delete(key)&lt;/code&gt; – allows users to invalidate items
for reasons external to the cache;
not strictly necessary,
but we'll end up with it as part of refactoring&lt;/li&gt;
&lt;li&gt;&lt;code&gt;evict(now)&lt;/code&gt; – not strictly necessary either,
but hints eviction is a separate bit of logic,
and may come in handy for testing&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Types deserve their own discussion:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;key&lt;/code&gt; – usually, the key is a string,
but we can relax this to any &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-hashable"&gt;hashable&lt;/a&gt; value&lt;/li&gt;
&lt;li&gt;&lt;code&gt;value&lt;/code&gt; – for an in-memory cache, any kind of object is fine&lt;/li&gt;
&lt;li&gt;&lt;code&gt;maxage&lt;/code&gt; and &lt;code&gt;priority&lt;/code&gt; – a number should do for these;
a float is more general, but an integer may allow a simpler solution;
limits on these are important too,
as we'll see soon enough&lt;/li&gt;
&lt;/ul&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;Your interviewer may be expecting you to uncover some of these details
through clarifying questions.
Be sure to think out loud and state your assumptions.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="a-minimal-plausible-solution"&gt;A minimal plausible solution&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#a-minimal-plausible-solution" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I'm sure there are a lot smart people out there
that can Think Really Hard™
and just come up with a solution,
but I'm not one of them,
so we'll take an iterative, &lt;a class="external" href="https://hintjens.gitbooks.io/scalable-c/content/chapter1.html#problem-what-do-we-do-next"&gt;problem-solution&lt;/a&gt; approach to this.&lt;/p&gt;
&lt;p&gt;Since right now we don't have &lt;em&gt;any&lt;/em&gt; solution,
we start with &lt;a class="external" href="https://wiki.c2.com/?DoTheSimplestThingThatCouldPossiblyWork"&gt;the simplest thing that could possibly work&lt;/a&gt;&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;
– a basic cache with no fancy eviction and no priorities;
we can then write some tests against that,
to know if we break anything going forward.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;If during an interview you don't know what to do
and choose to work up from the naive solution,
make it very clear that's what you're doing.
Your interviewer may give you hints to help you skip that.&lt;/p&gt;
&lt;/section&gt;
&lt;!-- ## setup --&gt;

&lt;p&gt;A class holds all the things we need
and gives us something to stick the API on:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxsize&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And this is our first algorithmic choice:
a &lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#dict"&gt;dict&lt;/a&gt; (backed by a &lt;a class="external" href="https://en.wikipedia.org/wiki/Hash_table"&gt;hash table&lt;/a&gt;)
provides average &lt;strong&gt;O(1)&lt;/strong&gt; search / insert / delete.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;Given the problem we're solving,
and the context we're solving it in,
we &lt;em&gt;have to&lt;/em&gt; talk about &lt;a class="external" href="https://en.wikipedia.org/wiki/Time_complexity"&gt;time complexity&lt;/a&gt;.
Ned Batchelder's
&lt;a class="external" href="https://nedbatchelder.com/text/bigo.html"&gt;Big-O: How Code Slows as Data Grows&lt;/a&gt;
provides an excellent introduction
(text and video available).&lt;/p&gt;
&lt;/section&gt;
&lt;!-- ## set --&gt;

&lt;p&gt;&lt;code&gt;set()&lt;/code&gt; leads to more choices:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;evict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;
        &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;First, we evict items only if there's no more room left.
(There are other ways of doing this;
for example,
evicting expired items periodically minimizes memory usage.)&lt;/p&gt;
&lt;p&gt;Second, if the key is already in the cache,
we remove and insert it again,
instead of updating things in place.
This way, there's only one code path for setting items,
which will make it a lot easier to
keep multiple data structures in sync later on.&lt;/p&gt;
&lt;p&gt;We use a named tuple to store the parameters associated with a key:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;
    &lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;!-- ## evict --&gt;

&lt;p&gt;For now, we just evict an arbitrary item;
in a happy little coincidence,
&lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#dict"&gt;dict&lt;/a&gt;s preserve insertion order,
so when iterating over the cache, the oldest key is first.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;evict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;!-- ## get --&gt;

&lt;p&gt;Finally, &lt;code&gt;get()&lt;/code&gt; is trivial:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;hr /&gt;
&lt;p&gt;With everything in place, here's the first test:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_basic&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FakeTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;A&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;A&amp;#39;&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;A&amp;#39;&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;C&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;C&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To make things predictable,
we inject a fake time implementation:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;FakeTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__call__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;!--

Also, we inject the time, so we can fake it during tests,
and we use [monotonic()] time
– [time()] can jump back and forth with system clock updates.

[time()]: https://docs.python.org/3/library/time.html#time.time
[monotonic()]: https://docs.python.org/3/library/time.html#time.monotonic

--&gt;

&lt;h2 id="problem-expired-items-should-go-first"&gt;Problem: expired items should go first...&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-expired-items-should-go-first" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Following from the &lt;a class="anchor" href="#requirements"&gt;requirements&lt;/a&gt;,
there's an order in which items get kicked out:
first expired (lowest expiry time),
then lowest priority,
and only then least recently used.
So, we need a data structure that
can efficiently remove the smallest element.&lt;/p&gt;
&lt;p&gt;Turns out, there's an &lt;a class="external" href="https://en.wikipedia.org/wiki/Abstract_data_type"&gt;abstract data type&lt;/a&gt; for that
called a &lt;a class="external" href="https://en.wikipedia.org/wiki/Priority_queue"&gt;priority queue&lt;/a&gt;;&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;
for now, we'll honor its abstract nature
and not bother with an implementation.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PriorityQueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;!-- ## set --&gt;

&lt;p&gt;Since we need the &lt;em&gt;item&lt;/em&gt; with the lowest expiry time,
we need a way to get back to the item;
an &lt;code&gt;(expires, key)&lt;/code&gt; tuple should do fine
– since tuples compare lexicographically,
it'll be like comparing by &lt;code&gt;expires&lt;/code&gt; alone,
but with &lt;code&gt;key&lt;/code&gt; along for the ride; in &lt;code&gt;set()&lt;/code&gt;, we add:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You may be tempted  (like I was) to say
&amp;quot;hey, the item's already a tuple,
if we make &lt;code&gt;expires&lt;/code&gt; the first field,
we can use the item itself&amp;quot;,
but let's delay optimizations
until we have and &lt;em&gt;understand&lt;/em&gt; a full solution –
&lt;a class="external" href="https://wiki.c2.com/?MakeItWorkMakeItRightMakeItFast"&gt;make it work, make it right, make it fast&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Still in &lt;code&gt;set()&lt;/code&gt;,
if the key is already in the cache,
we also remove and insert it from the expires queue,
so it's added back with the new expiry time.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;evict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;!-- ## evict --&gt;

&lt;p&gt;Moving on to evicting things;
for this, we need two operations:
first peek at the item that expires next
to see if it's expired, then, if it is, pop it from the queue.
(Another choice: we only have to evict one item, but evict all expired ones.)&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;evict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;initial_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;initial_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If there are no expired items,
we still have to make room for the one item;
since we're not handling priorities yet,
we'll evict the item that expires next a little early.&lt;/p&gt;
&lt;p&gt;&lt;a id="priority-queue"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="problem-name-priorityqueue-is-not-defined"&gt;Problem: name PriorityQueue is not defined&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-name-priorityqueue-is-not-defined" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;OK, to get the code working again, we need a PriorityQueue class.
It doesn't need to be fast,
we can deal with that after we finish everything else;
for now, let's just keep our elements in a plain list.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;PriorityQueue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The easiest way to get the smallest value is to keep the list sorted;
the downside is that &lt;code&gt;push()&lt;/code&gt; is now &lt;strong&gt;O(n log n)&lt;/strong&gt;
– although, because the list is always sorted,
it can be as good as &lt;strong&gt;O(n)&lt;/strong&gt; depending on the &lt;a class="external" href="https://en.wikipedia.org/wiki/Timsort"&gt;implementation&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This makes &lt;code&gt;peek()&lt;/code&gt; and &lt;code&gt;pop()&lt;/code&gt; trivial;
still, &lt;code&gt;pop()&lt;/code&gt; is &lt;strong&gt;O(n)&lt;/strong&gt;,
because it shifts all the items left by one position.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;rv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;remove()&lt;/code&gt; is just as simple, and just as &lt;strong&gt;O(N)&lt;/strong&gt;,
because it first needs to find the item,
and then shift the ones after it to cover the gap.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We didn't use the &lt;em&gt;is empty&lt;/em&gt; operation,
but it should be O(1) regardless of implementation,
so let's throw it in anyway:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__bool__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;OK, let's wrap up with a quick test:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_priority_queue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;pq&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PriorityQueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;pq&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ne"&gt;IndexError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ne"&gt;IndexError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;pq&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;!-- ## more tests --&gt;

&lt;hr /&gt;
&lt;p&gt;Now the existing tests pass,
and we can add more –
first, that expired items are evicted
(note how we're moving the time forward):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_expires&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FakeTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;A&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;A&amp;#39;&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;C&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;C&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Second, that setting an existing item changes its expire time:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_update_expires&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FakeTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;A&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;X&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;X&amp;#39;&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="problem-low-priority-items-second"&gt;Problem: ...low priority items second&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-low-priority-items-second" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Next up, kick out items by priority –
shouldn't be too hard, right?&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;__init__()&lt;/code&gt;, add another priority queue for priorities:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PriorityQueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priorities&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PriorityQueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In &lt;code&gt;set()&lt;/code&gt;, add new items to the priorities queue:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priorities&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;...and remove already-cached items:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priorities&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In &lt;code&gt;evict()&lt;/code&gt;, remove expired items from the priorities queue:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priorities&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;...and finally, if none are expired,
remove the one with the lowest priority:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;initial_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priorities&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;!-- ## tests --&gt;

&lt;p&gt;Add one test for eviction by priority:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_priorities&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FakeTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;A&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;A&amp;#39;&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;C&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;A&amp;#39;&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;C&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;...and one for updating the priority of existing items:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_update_priorities&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FakeTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;A&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;C&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Y&amp;#39;&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;C&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="problem-we-re-deleting-items-in-three-places"&gt;Problem: we're deleting items in three places&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-we-re-deleting-items-in-three-places" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;I said we'll postpone performance optimizations
until we have a complete solution,
but I have a different kind of optimization in mind – for readability.&lt;/p&gt;
&lt;p&gt;We're deleting items in three slightly different ways,
careful to keep three data structures in sync each time;
it would be nice to do it only once.
While a bit premature,
through the magic of having written the article already,
I'm sure it will pay off.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priorities&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Sure, eviction is twice as slow,
but the complexity stays the the same –
the constant factor in &lt;strong&gt;O(2n)&lt;/strong&gt; gets removed, leaving us with &lt;strong&gt;O(n)&lt;/strong&gt;.
If needed, we can go back to the unpacked version
once we have a reasonably efficient implementation
(that's what tests are for).&lt;/p&gt;
&lt;p&gt;Deleting already-cached items is shortened to:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Idem for the core eviction logic:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;initial_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priorities&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Neat!&lt;/p&gt;
&lt;h2 id="problem-least-recently-used-items-last"&gt;Problem: ...least recently used items last&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-least-recently-used-items-last" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So, how does one implement a least recently used cache?&lt;/p&gt;
&lt;p&gt;We could google it...
or, we could look at an existing implementation.&lt;/p&gt;
&lt;h3 id="functools-lru-cache"&gt;functools.lru_cache()&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#functools-lru-cache" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Standard library &lt;a class="external" href="https://docs.python.org/3/library/functools.html#functools.lru_cache"&gt;functools.lru_cache()&lt;/a&gt; comes to mind first;
let's have a look.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;You can read the code of stdlib modules
by following the &lt;strong&gt;Source code:&lt;/strong&gt; link
at the top of each documentation page.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;&lt;a class="external" href="https://github.com/python/cpython/blob/v3.12.0/Lib/functools.py#L479-L523"&gt;lru_cache()&lt;/a&gt; delegates to &lt;a class="external" href="https://github.com/python/cpython/blob/v3.12.0/Lib/functools.py#L525-L639"&gt;_lru_cache_wrapper()&lt;/a&gt;,
which sets up a bunch of variables to be used by nested functions.&lt;sup class="footnote-ref" id="fnref-3"&gt;&lt;a href="#fn-3"&gt;3&lt;/a&gt;&lt;/sup&gt;
Among the variables is
a &lt;code&gt;cache&lt;/code&gt; dict
and a doubly linked list
where nodes are [prev, next, key, value] lists.&lt;sup class="footnote-ref" id="fnref-4"&gt;&lt;a href="#fn-4"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;&lt;a id="dll"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;And that's the answer –
a doubly linked list allows tracking item use in &lt;strong&gt;O(1)&lt;/strong&gt;:
each time a node is used,
remove it from its current position and plop it at the &amp;quot;recently used&amp;quot; end;
whatever's at the other end will be the least recently used item.&lt;/p&gt;
&lt;p&gt;Note that, unlike &lt;a class="external" href="https://docs.python.org/3/library/functools.html#functools.lru_cache"&gt;lru_cache()&lt;/a&gt;,
we need one doubly linked list &lt;em&gt;for each priority&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;But, before making &lt;code&gt;Item&lt;/code&gt; mutable and giving it prev/next links,
let's dive deeper.&lt;/p&gt;
&lt;h3 id="ordereddict"&gt;OrderedDict&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#ordereddict" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;If you &lt;a class="external" href="https://docs.python.org/3.12/search.html?q=LRU"&gt;search the docs for &amp;quot;LRU&amp;quot;&lt;/a&gt;,
the next result after lru_cache()
is &lt;a class="external" href="https://docs.python.org/3/library/collections.html#ordereddict-objects"&gt;OrderedDict&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Some differences from &lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#dict"&gt;dict&lt;/a&gt; still remain:
[...]
The &lt;a class="external" href="https://docs.python.org/3/library/collections.html#collections.OrderedDict"&gt;OrderedDict&lt;/a&gt; algorithm can handle frequent reordering operations better than dict.
[...] this makes it suitable for implementing various kinds of LRU caches.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Specifically:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/library/collections.html#collections.OrderedDict"&gt;OrderedDict&lt;/a&gt; has a &lt;code&gt;move_to_end()&lt;/code&gt; method to efficiently reposition an element to an endpoint.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Since &lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#dict"&gt;dict&lt;/a&gt;s preserve insertion order,
you can use &lt;code&gt;d[k] = d.pop(k)&lt;/code&gt; to move items to the end...
What makes &lt;a class="external" href="https://docs.python.org/3/library/collections.html#collections.OrderedDict.move_to_end"&gt;move_to_end()&lt;/a&gt; better, then?
&lt;a class="external" href="https://github.com/python/cpython/blob/v3.12.0/Lib/collections/__init__.py#L90"&gt;This comment&lt;/a&gt; may shed some light:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;90&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="c1"&gt;# The internal self.__map dict maps keys to links in a doubly linked list.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Indeed, move_to_end() &lt;a class="external" href="https://github.com/python/cpython/blob/v3.12.0/Lib/collections/__init__.py#L188-L211"&gt;does exactly&lt;/a&gt; what we described &lt;a class="anchor" href="#dll"&gt;above&lt;/a&gt; –
this is good news, it means we don't have to do it ourselves!&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a id="priority-buckets"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;So, we need one OrderedDict (read: doubly linked list) for each priority,
but still need to keep track the lowest priority:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PriorityQueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PriorityQueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Handling priorities in &lt;code&gt;set()&lt;/code&gt; gets a bit more complicated:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="n"&gt;priority_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_buckets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;priority_bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;priority_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderedDict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;priority_bucket&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But now we can finally evict the least recently used item:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;initial_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;priority_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_buckets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priority_bucket&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In &lt;code&gt;delete()&lt;/code&gt;, we're careful to get rid of empty buckets:&lt;sup class="footnote-ref" id="fnref-5"&gt;&lt;a href="#fn-5"&gt;5&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="n"&gt;priority_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;priority_bucket&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;priority_bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Existing tests pass again, and we can add a new (still failing) one:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_lru&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;FakeTime&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;A&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;B&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;A&amp;#39;&lt;/span&gt;

    &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;C&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;A&amp;#39;&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;C&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;All that's needed to make it pass is to call &lt;a class="external" href="https://docs.python.org/3/library/collections.html#collections.OrderedDict.move_to_end"&gt;move_to_end()&lt;/a&gt; in &lt;code&gt;get()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;move_to_end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;




&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        Liking this so far? Here&amp;#39;s another article you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/stdlib"&gt;
            Learn by reading code: Python standard library design decisions explained
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="problem-our-priority-queue-is-slow"&gt;Problem: our priority queue is slow&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-our-priority-queue-is-slow" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;OK, we have a complete solution,
it's time to deal with the priority queue implementation.
Let's do a quick recap of the methods we need and why:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;push()&lt;/code&gt; – to add items&lt;/li&gt;
&lt;li&gt;&lt;code&gt;peek()&lt;/code&gt; – to get items / buckets with the lowest expiry time / priority&lt;/li&gt;
&lt;li&gt;&lt;code&gt;remove()&lt;/code&gt; – to delete items&lt;/li&gt;
&lt;li&gt;&lt;code&gt;pop()&lt;/code&gt; – not used, but would be without the &lt;a class="anchor" href="#problem-we-re-deleting-items-in-three-places"&gt;&lt;code&gt;delete()&lt;/code&gt; refactoring&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We make two related observations:
first, there's no &lt;em&gt;remove&lt;/em&gt; operation on the  &lt;a class="external" href="https://en.wikipedia.org/wiki/Priority_queue"&gt;priority queue&lt;/a&gt; Wikipedia page;
second, even if we unpack &lt;code&gt;delete()&lt;/code&gt;,
we only get to &lt;code&gt;pop()&lt;/code&gt; an item/bucket from one of the queues,
and still have to &lt;code&gt;remove()&lt;/code&gt; it from the other.&lt;/p&gt;
&lt;p&gt;And &lt;strong&gt;this is what makes the problem tricky&lt;/strong&gt; –
we need to maintain not one, but &lt;em&gt;two&lt;/em&gt; independent priority queues.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;We'll now go through a few data structures in quick succession,
which may be a bit overwhelming without preparation.
Keep in mind we don't care how they work (not yet, at least),
we're just shopping around based on specs.&lt;/p&gt;
&lt;/section&gt;
&lt;h3 id="heapq"&gt;heapq&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#heapq" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;If you &lt;a class="external" href="https://docs.python.org/3.12/search.html?q=priority+queue"&gt;search the docs for &amp;quot;priority queue&amp;quot;&lt;/a&gt;,
you'll find &lt;a class="external" href="https://docs.python.org/3/library/heapq.html"&gt;heapq&lt;/a&gt;, which:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[...] provides an implementation of the heap queue algorithm,
&lt;strong&gt;also known as the priority queue algorithm&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;!--

[Wikipedia][priority queue] mostly agrees:

&gt; While priority queues are often implemented using [heaps][heap],
&gt; they are conceptually distinct from heaps. [...]

--&gt;

&lt;p&gt;Reading on,
we find extensive &lt;a class="external" href="https://docs.python.org/3/library/heapq.html#priority-queue-implementation-notes"&gt;notes on implementing priority queues&lt;/a&gt;;
particularly interesting are
using &lt;em&gt;(priority, item)&lt;/em&gt; tuples (already doing this!)
and removing entries:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Removing the entry or changing its priority is more difficult
because it would break the heap structure invariants.
So, a possible solution is to mark the entry as removed
and add a new entry with the revised priority.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This workaround is needed because while
removing the i-th element &lt;a class="external" href="https://stackoverflow.com/questions/10162679/python-delete-element-from-heap/10163422#10163422"&gt;can be done in &lt;strong&gt;O(log n)&lt;/strong&gt;&lt;/a&gt;,
finding its index is &lt;strong&gt;O(n)&lt;/strong&gt;.
To summarize, we have:&lt;/p&gt;
&lt;table class="table"&gt;
&lt;thead&gt;
&lt;tr&gt;
  &lt;th&gt;&lt;/th&gt;
  &lt;th&gt;sort&lt;/th&gt;
  &lt;th&gt;heapq&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
  &lt;td&gt;push()&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;peek()&lt;/td&gt;
  &lt;td&gt;O(1)&lt;/td&gt;
  &lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;pop()&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;remove()&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Still, with a few mitigating assumptions, it could work:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;we can assume priorities are static, so buckets never get removed&lt;/li&gt;
&lt;li&gt;to-be-removed expiry times will get popped sooner or later anyway;
we can assume that most evictions are due to expired items,
and that items being evicted due to low priority (i.e. when the cache is full)
and item updates are rare
(both cause to-be-removed entries to accumulate in the expiry queue)&lt;/li&gt;
&lt;/ul&gt;
&lt;!-- ...but, that's maybe too many assumptions for my liking. --&gt;

&lt;h3 id="bisect"&gt;bisect&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bisect" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;One way of finding an element in better than O(n) is &lt;a class="external" href="https://docs.python.org/3/library/bisect.html"&gt;bisect&lt;/a&gt;, which:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[...] provides support for maintaining a list in sorted order
without having to sort the list after each insertion.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This may provide an improvement
to our &lt;a class="anchor" href="#priority-queue"&gt;naive implementation&lt;/a&gt;;
sadly, reading further to &lt;a class="external" href="https://docs.python.org/3/library/bisect.html#performance-notes"&gt;Performance Notes&lt;/a&gt;
we find that:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The insort() functions are &lt;strong&gt;O(n)&lt;/strong&gt; because the logarithmic search step
is dominated by the linear time insertion step.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;While in general that's better than just about any sort,
we happen to be hitting the best case of our sort &lt;a class="external" href="https://en.wikipedia.org/wiki/Timsort"&gt;implementation&lt;/a&gt;,
which has the same complexity.
(Nevertheless, shifting elements is likely cheaper than the same number of comparisons.)&lt;/p&gt;
&lt;table class="table"&gt;
&lt;thead&gt;
&lt;tr&gt;
  &lt;th&gt;&lt;/th&gt;
  &lt;th&gt;sort&lt;/th&gt;
  &lt;th&gt;heapq&lt;/th&gt;
  &lt;th&gt;&lt;mark&gt;bisect&lt;/mark&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
  &lt;td&gt;push()&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;peek()&lt;/td&gt;
  &lt;td&gt;O(1)&lt;/td&gt;
  &lt;td&gt;O(1)&lt;/td&gt;
  &lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;pop()&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;remove()&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Further down in the docs, there's a &lt;em&gt;see also&lt;/em&gt; box:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;a class="external" href="https://grantjenks.com/docs/sortedcollections/"&gt;Sorted Collections&lt;/a&gt; is a high performance module
that uses &lt;em&gt;bisect&lt;/em&gt; to managed sorted collections of data.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Not in stdlib, moving along... ¯\_(ツ)_/¯&lt;/p&gt;
&lt;h3 id="pop-optimization"&gt;pop() optimization&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#pop-optimization" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;There's an unrelated improvement that applies to both
the &lt;a class="anchor" href="#priority-queue"&gt;naive solution&lt;/a&gt; and &lt;a class="anchor" href="#bisect"&gt;bisect&lt;/a&gt;.
With a sorted list, pop() is &lt;strong&gt;O(n)&lt;/strong&gt; because
it shifts all elements after the first;
if the order was reversed,
we'd pop() &lt;em&gt;from the end&lt;/em&gt;,
&lt;a class="external" href="https://wiki.python.org/moin/TimeComplexity"&gt;which is &lt;strong&gt;O(1)&lt;/strong&gt;&lt;/a&gt;. So:&lt;/p&gt;
&lt;table class="table"&gt;
&lt;thead&gt;
&lt;tr&gt;
  &lt;th&gt;&lt;/th&gt;
  &lt;th&gt;sort&lt;/th&gt;
  &lt;th&gt;heapq&lt;/th&gt;
  &lt;th&gt;bisect&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
  &lt;td&gt;push()&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;peek()&lt;/td&gt;
  &lt;td&gt;O(1)&lt;/td&gt;
  &lt;td&gt;O(1)&lt;/td&gt;
  &lt;td&gt;O(1)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;pop()&lt;/td&gt;
  &lt;td&gt;&lt;mark&gt;O(1)&lt;a class="anchor" href="#pop-optimization"&gt;*&lt;/a&gt;&lt;/mark&gt;&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
  &lt;td&gt;&lt;mark&gt;O(1)&lt;a class="anchor" href="#pop-optimization"&gt;*&lt;/a&gt;&lt;/marr&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;remove()&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h3 id="binary-search-trees"&gt;Binary search trees&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#binary-search-trees" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;OK, I'm out of ideas,
and there's nothing else &lt;a class="external" href="https://docs.python.org/3/library/datatypes.html"&gt;in stdlib&lt;/a&gt; that can help.&lt;/p&gt;
&lt;p&gt;We can restate the problem as follows:
we need a sorted data structure
that can do better than &lt;strong&gt;O(n)&lt;/strong&gt; for push() / remove().&lt;/p&gt;
&lt;p&gt;We've already peeked at the Wikipedia &lt;a class="external" href="https://en.wikipedia.org/wiki/Priority_queue"&gt;priority queue&lt;/a&gt; page,
so let's keep reading –
skipping past the &lt;a class="external" href="https://en.wikipedia.org/wiki/Priority_queue#Naive_implementations"&gt;naive implementations&lt;/a&gt;,
to the &lt;a class="external" href="https://en.wikipedia.org/wiki/Priority_queue#Usual_implementation"&gt;usual implementation&lt;/a&gt;, we find that:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To improve performance, priority queues are typically based on a &lt;a class="external" href="https://en.wikipedia.org/wiki/Heap_(data_structure)"&gt;heap&lt;/a&gt;, [...]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Looked into that, didn't work; next:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Alternatively, when a &lt;a class="external" href="https://en.wikipedia.org/wiki/Self-balancing_binary_search_tree"&gt;self-balancing binary search tree&lt;/a&gt; is used,
insertion and removal also take O(log n) time [...]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;em&gt;There&lt;/em&gt;, that's what we're looking for!
(And likely what your interviewer is, too.)&lt;/p&gt;
&lt;table class="table"&gt;
&lt;thead&gt;
&lt;tr&gt;
  &lt;th&gt;&lt;/th&gt;
  &lt;th&gt;sort&lt;/th&gt;
  &lt;th&gt;heapq&lt;/th&gt;
  &lt;th&gt;bisect&lt;/th&gt;
  &lt;th&gt;&lt;mark&gt;BST&lt;/mark&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
  &lt;td&gt;push()&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;peek()&lt;/td&gt;
  &lt;td&gt;O(1)&lt;/td&gt;
  &lt;td&gt;O(1)&lt;/td&gt;
  &lt;td&gt;O(1)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;pop()&lt;/td&gt;
  &lt;td&gt;O(1)&lt;a class="anchor" href="#pop-optimization"&gt;*&lt;/a&gt;&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
  &lt;td&gt;O(1)&lt;a class="anchor" href="#pop-optimization"&gt;*&lt;/a&gt;&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;remove()&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;But there's no self-balancing BST in the standard library,
and I sure as hell am not implementing one right now
– I still have flashbacks from when I tried to do a red-black tree
and two hours later it still had bugs
(I mean, &lt;a class="external" href="https://en.wikipedia.org/wiki/Red%E2%80%93black_tree#Operations"&gt;look at the length of this explanation!&lt;/a&gt;).&lt;/p&gt;
&lt;!-- and there's other, more complicated data structures that mix trees with something else --&gt;

&lt;p&gt;After a bit of googling we find, among others, &lt;a class="external" href="https://pypi.org/project/bintrees/"&gt;bintrees&lt;/a&gt;,
a mature library that provides all sorts of binary search trees... except:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Bintrees Development Stopped&lt;/p&gt;
&lt;p&gt;Use &lt;em&gt;sortedcontainers&lt;/em&gt; instead: &lt;a class="external" href="https://pypi.python.org/pypi/sortedcontainers"&gt;https://pypi.python.org/pypi/sortedcontainers&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Sounds familiar, doesn't it?&lt;/p&gt;
&lt;h3 id="sorted-containers"&gt;Sorted Containers&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#sorted-containers" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Let's go back to that &lt;a class="external" href="https://grantjenks.com/docs/sortedcollections/"&gt;Sorted Collections&lt;/a&gt; library &lt;a class="external" href="https://docs.python.org/3/library/bisect.html"&gt;bisect&lt;/a&gt; was recommending:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Depends on the &lt;a class="external" href="https://grantjenks.com/docs/sortedcontainers/"&gt;Sorted Containers&lt;/a&gt; module.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(￢‿￢ )&lt;/p&gt;
&lt;p&gt;I remember, I remember now...
I'm not salty because the red-black tree took two hours to implement.
I'm salty because after all that time,
I found &lt;a class="external" href="https://grantjenks.com/docs/sortedcontainers/"&gt;Sorted Containers&lt;/a&gt;,
a pure Python library
that is &lt;em&gt;faster&lt;/em&gt; in practice than
fancy self-balancing binary search trees implemented in C!&lt;/p&gt;
&lt;p&gt;It has &lt;a class="external" href="https://grantjenks.com/docs/sortedcontainers/performance.html"&gt;extensive benchmarks&lt;/a&gt; to prove it,
and simulated workload benchmarks for our own use case,
&lt;a class="external" href="https://grantjenks.com/docs/sortedcontainers/performance-workload.html#priority-queue"&gt;priority queues&lt;/a&gt;
– so yeah, while the interview answer is &amp;quot;self-balancing BSTs&amp;quot;,
the &lt;em&gt;actual&lt;/em&gt; answer is &lt;a class="external" href="https://grantjenks.com/docs/sortedcontainers/"&gt;Sorted Containers&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;How does it work? There's an &lt;a class="external" href="https://grantjenks.com/docs/sortedcontainers/implementation.html"&gt;extensive explanation&lt;/a&gt; too:&lt;sup class="footnote-ref" id="fnref-6"&gt;&lt;a href="#fn-6"&gt;6&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;a class="external" href="https://grantjenks.com/docs/sortedcontainers/"&gt;Sorted Containers&lt;/a&gt; internal implementation is based on a couple observations. The first is that Python’s &lt;em&gt;list&lt;/em&gt; is fast, &lt;em&gt;really fast&lt;/em&gt;. [...] The second is that &lt;em&gt;bisect.insort&lt;/em&gt;&lt;sup class="footnote-ref" id="fnref-7"&gt;&lt;a href="#fn-7"&gt;7&lt;/a&gt;&lt;/sup&gt; is fast. This is somewhat counter-intuitive since it involves shifting a series of items in a list. But modern processors do this really well. A lot of time has been spent optimizing mem-copy/mem-move-like operations both in hardware and software.&lt;/p&gt;
&lt;p&gt;But using only one list and &lt;em&gt;bisect.insort&lt;/em&gt; would produce sluggish behavior for lengths exceeding ten thousand. So the implementation of &lt;a class="external" href="https://grantjenks.com/docs/sortedcontainers/sortedlist.html"&gt;Sorted List&lt;/a&gt; uses a list of lists to store elements. [...]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;There's also a comparison with trees, which I'll summarize:
fewer memory allocations,
better cache locality,
lower memory overhead,
and faster iteration.&lt;/p&gt;
&lt;p&gt;I think that gives you a decent idea of how and why it works,
enough that with a bit of tinkering
you might be able to implement it yourself.&lt;sup class="footnote-ref" id="fnref-8"&gt;&lt;a href="#fn-8"&gt;8&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2 id="problem-sorted-containers-is-not-in-stdlib"&gt;Problem: Sorted Containers is not in stdlib&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-sorted-containers-is-not-in-stdlib" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;But &lt;a class="external" href="https://grantjenks.com/docs/sortedcontainers/"&gt;Sorted Containers&lt;/a&gt; is not in the standard library either,
and we don't want to implement it ourselves.
We did learn something from it, though:&lt;/p&gt;
&lt;table class="table"&gt;
&lt;thead&gt;
&lt;tr&gt;
  &lt;th&gt;&lt;/th&gt;
  &lt;th&gt;sort&lt;/th&gt;
  &lt;th&gt;heapq&lt;/th&gt;
  &lt;th&gt;bisect&lt;/th&gt;
  &lt;th&gt;&lt;mark&gt;bisect &amp;lt;10k&lt;/mark&gt;&lt;/th&gt;
  &lt;th&gt;BST&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
  &lt;td&gt;push()&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;peek()&lt;/td&gt;
  &lt;td&gt;O(1)&lt;/td&gt;
  &lt;td&gt;O(1)&lt;/td&gt;
  &lt;td&gt;O(1)&lt;/td&gt;
  &lt;td&gt;O(1)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;pop()&lt;/td&gt;
  &lt;td&gt;O(1)&lt;a class="anchor" href="#pop-optimization"&gt;*&lt;/a&gt;&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
  &lt;td&gt;O(1)&lt;a class="anchor" href="#pop-optimization"&gt;*&lt;/a&gt;&lt;/td&gt;
  &lt;td&gt;O(1)&lt;a class="anchor" href="#pop-optimization"&gt;*&lt;/a&gt;&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;remove()&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
  &lt;td&gt;O(n)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
  &lt;td&gt;O(log n)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;We still need to make some assumptions, though:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Do we really need more than 10k priorities?
&lt;strong&gt;Likely no&lt;/strong&gt;, let's just cap them at 10k.&lt;/li&gt;
&lt;li&gt;Do we really need more than 10k expiry times?
&lt;strong&gt;Maybe?&lt;/strong&gt; –
with 1 second granularity
we can represent only up to 2.7 hours;
10 seconds takes us up to 27 hours,
which may just work.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;OK, one more and we're done.
The other issue,
aside from the maximum time,
is that the granularity is too low,
especially for short times –
rounding 1 to 10 seconds is much worse than
rounding 2001 to 2010 seconds.
Which begs the question –&lt;/p&gt;
&lt;ol start="3"&gt;
&lt;li&gt;Does it really matter if items expire in 2010 seconds instead of 2001?
&lt;strong&gt;Likely no&lt;/strong&gt;,
but we need a way to round
small values with higher granularity than big ones.&lt;/li&gt;
&lt;/ol&gt;



&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        If you&amp;#39;ve made it this far, you will definitely like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/pwned"&gt;
            Has your password been pwned? Or, how I almost failed to search a 37 GB text file in under 1 millisecond (in Python)
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="logarithmic-time"&gt;Logarithmic time&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#logarithmic-time" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;How about 1, 2, 4, 8, ...?
Rounding up to powers of 2 gets us decreasing granularity,
but time doesn't actually start at zero.
We fix this by rounding up to &lt;em&gt;multiples&lt;/em&gt; of powers of 2 instead;
let's get an intuition of how it works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2001&lt;/span&gt;
&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2002&lt;/span&gt;
&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2004&lt;/span&gt;
&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2004&lt;/span&gt;
&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2016&lt;/span&gt;
&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2016&lt;/span&gt;
&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;So far so good, how about after some time has passed?&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2013&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;  &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;  &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2014&lt;/span&gt;
&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2013&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;  &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2016&lt;/span&gt;
&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2013&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2016&lt;/span&gt;
&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2013&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;  &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2020&lt;/span&gt;
&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2013&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2032&lt;/span&gt;
&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2013&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2032&lt;/span&gt;
&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;2013&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2048&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The beauty of aligned powers is that
for a relatively constant number of expiry times,
the number of buckets remains roughly the same over time –
as closely packed buckets are removed from the beginning,
new ones fill the gaps between the sparser ones towards the end.&lt;/p&gt;
&lt;p&gt;OK, let's put it into code:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;next_power&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;
    &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;next_power&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;next_power&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="go"&gt;[1, 2, 4, 4, 16, 16, 32]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="go"&gt;[2001, 2002, 2004, 2004, 2016, 2016, 2048]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2013&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="go"&gt;[2014, 2016, 2016, 2020, 2032, 2032, 2048]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Looking good!&lt;/p&gt;
&lt;p&gt;There are two sources of error –
first from rounding &lt;code&gt;maxage&lt;/code&gt;,
worst when it's one more than a power of 2,
and second from rounding the expiry time,
also worst when it's one more than a power of two.
Together, they approach 200% of &lt;code&gt;maxage&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# (32 - 17) / 17 ~= 88%&lt;/span&gt;
&lt;span class="go"&gt;32&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# (64 - 33) / 33 ~= 94%&lt;/span&gt;
&lt;span class="go"&gt;64&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# (64 - 31) / 17 ~= 182%&lt;/span&gt;
&lt;span class="go"&gt;64&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;33&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# (128 - 64) / 33 ~= 191%&lt;/span&gt;
&lt;span class="go"&gt;128&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;200% error is quite a lot;
before we set to fix it,
let's confirm our reasoning.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;log_bucket() error.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;max_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_maxage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Worst log_bucket() error for all maxages up to max_maxage.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_maxage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;max_error_random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Worst log_bucket() error for random inputs, out of n tries.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;max_now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;max_maxage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;
    &lt;span class="n"&gt;rand&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;randint&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_now&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rand&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_maxage&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;max_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;0.9997558891736849&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;max_error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;1.9527896995708156&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;max_error_random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10_000_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;1.9995498725910554&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Looks confirmed enough to me.&lt;/p&gt;
&lt;p&gt;So, how do we make the error smaller?
Instead of rounding to the next power of 2,
we round to the next half of a power of 2,
or next quarter, or next eighth...&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;next_power&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;
    &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;next_power&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;next_power&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It seems to be working:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="go"&gt;[1, 2, 4, 4, 16, 16, 32]&lt;/span&gt;
&lt;span class="go"&gt;[1, 2, 4, 4, 16, 16, 32]&lt;/span&gt;
&lt;span class="go"&gt;[1, 2, 3, 4, 16, 16, 24]&lt;/span&gt;
&lt;span class="go"&gt;[1, 2, 3, 4, 16, 16, 20]&lt;/span&gt;
&lt;span class="go"&gt;[1, 2, 3, 4, 15, 16, 18]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;e&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;max_error_random&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1_000_000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s1"&gt;6.1%&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="go"&gt;0 199.8%&lt;/span&gt;
&lt;span class="go"&gt;1  99.9%&lt;/span&gt;
&lt;span class="go"&gt;2  50.0%&lt;/span&gt;
&lt;span class="go"&gt;3  25.0%&lt;/span&gt;
&lt;span class="go"&gt;4  12.5%&lt;/span&gt;
&lt;span class="go"&gt;5   6.2%&lt;/span&gt;
&lt;span class="go"&gt;6   3.1%&lt;/span&gt;
&lt;span class="go"&gt;7   1.6%&lt;/span&gt;
&lt;span class="go"&gt;8   0.8%&lt;/span&gt;
&lt;span class="go"&gt;9   0.4%&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With &lt;code&gt;shift=7&lt;/code&gt;, the error is less that two percent;
I wonder how many buckets that is...&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;max_buckets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_maxage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Number of buckets to cover all maxages up to max_maxage.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="n"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_maxage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buckets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;max_buckets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;729&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;max_buckets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;31&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;1047&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;max_buckets&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3600&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;24&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;365&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;1279&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A bit over a thousand buckets for the whole year, not bad!&lt;/p&gt;
&lt;!-- ## expires buckets --&gt;

&lt;hr /&gt;
&lt;details&gt;
&lt;summary&gt;
Before we can use any of that,
we need to convert expiry times to buckets;
that looks a lot like the &lt;a href="#priority-buckets"&gt;priority buckets&lt;/a&gt; code,
the only notable part being eviction.

&lt;/summary&gt;

&lt;p&gt;&lt;code&gt;__init__()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PriorityQueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PriorityQueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;set()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="n"&gt;expires_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_buckets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;expires_bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;expires_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;expires_bucket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;delete()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="n"&gt;expires_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;expires_bucket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;expires_bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;evict()&lt;/code&gt;:&lt;/p&gt;
&lt;/details&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="n"&gt;expires_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
                &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;hr /&gt;
&lt;p&gt;And now we use &lt;code&gt;log_bucket()&lt;/code&gt;.
Since we're at it,
why not have unlimited priorities too?
A hammer is a hammer and everything is a nail, after all.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="bisect-redux"&gt;bisect, redux&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bisect-redux" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Time to fix that priority queue.&lt;/p&gt;
&lt;p&gt;We use &lt;a class="external" href="https://docs.python.org/3/library/bisect.html#bisect.insort"&gt;insort()&lt;/a&gt; to add priorities
and &lt;a class="external" href="https://docs.python.org/3/library/operator.html#operator.neg"&gt;operator.neg()&lt;/a&gt; to keep the list &lt;a class="anchor" href="#pop-optimization"&gt;reversed&lt;/a&gt;:&lt;sup class="footnote-ref" id="fnref-9"&gt;&lt;a href="#fn-9"&gt;9&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;bisect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We update &lt;code&gt;peek()&lt;/code&gt; and &lt;code&gt;pop()&lt;/code&gt; to handle the reverse order:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, for &lt;code&gt;remove()&lt;/code&gt; we adapt the &lt;code&gt;index()&lt;/code&gt; recipe
from &lt;a class="external" href="https://docs.python.org/3/library/bisect.html#searching-sorted-lists"&gt;Searching Sorted Lists&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bisect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bisect_left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And that's it, we're done!&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;
Here's the cache in its full glory (click to expand):

&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;maxsize&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;

        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PriorityQueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_buckets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_order&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;PriorityQueue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;move_to_end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;evict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;

        &lt;span class="n"&gt;expires_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_buckets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;expires_bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;expires_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;expires_bucket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;priority_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_buckets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;priority_bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;priority_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;OrderedDict&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;priority_bucket&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;evict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;initial_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
            &lt;span class="n"&gt;expires_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt;
                &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;initial_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;priority_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_buckets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priority_bucket&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;priority&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cache&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;expires_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;expires_bucket&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;expires_bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;expires_order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;priority_bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;priority_bucket&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;priority_bucket&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_buckets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;priority_order&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Item&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;
    &lt;span class="n"&gt;expires&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;priority&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;PriorityQueue&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;push&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;bisect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;insort&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;peek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;remove&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bisect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bisect_left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;operator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;neg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__bool__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;log_bucket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;now&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;next_power&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;log2&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxage&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;shift&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;now&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;maxage&lt;/span&gt;
    &lt;span class="n"&gt;bucket&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expires&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;next_power&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;next_power&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;bucket&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;/details&gt;

&lt;p&gt;(&lt;a class="attachment" href="/_file/lru-cache/50-bisect.py"&gt;The entire file, with tests and everything.&lt;/a&gt;)&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#conclusion" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Anyone expecting you to &lt;em&gt;implement&lt;/em&gt; this in under an hour is delusional.
Explaining &lt;strong&gt;what you would use and why&lt;/strong&gt; should be enough
for reasonable interviewers,
although that may prove difficult
if you haven't solved this kind of problem before.&lt;/p&gt;
&lt;p&gt;Bullshit interviews aside,
it is useful to &lt;strong&gt;have basic knowledge of time complexity&lt;/strong&gt;.
Again, can't recommend &lt;a class="external" href="https://nedbatchelder.com/text/bigo.html"&gt;Big-O: How Code Slows as Data Grows&lt;/a&gt; enough.&lt;/p&gt;
&lt;p&gt;But, what big O notation says and what happens in practice can differ quite a lot.
Be sure to &lt;strong&gt;&lt;a class="internal" href="/fast-conway-cubes#conclusion"&gt;measure&lt;/a&gt;&lt;/strong&gt;,
and be sure to think of limits –
sometimes, the &lt;strong&gt;n&lt;/strong&gt; in &lt;strong&gt;O(n)&lt;/strong&gt;
is or can be made &lt;strong&gt;small enough&lt;/strong&gt;
you don't have to do the theoretically correct thing.&lt;/p&gt;
&lt;p&gt;You don't need to know how to implement all the data structures,
that's what (software) libraries and Wikipedia are for
(and for that matter, book libraries too).
However, it is useful to have an idea of
&lt;strong&gt;what's available and when to use it&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;a class="internal" href="/output#good-libraries-educate"&gt;Good libraries educate&lt;/a&gt;&lt;/strong&gt; –
the Python standard library docs
already cover a lot of the practical knowledge we needed,
and so did Sorted Containers.
But, that won't show up in the API reference you see in your IDE,
you have read the actual documentation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/lru-cache&amp;t=This%20is%20not%20interview%20advice%3A%20a%20priority-expiry%20LRU%20cache%20in%20Python%20without%20heaps%20or%20trees"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=This%20is%20not%20interview%20advice%3A%20a%20priority-expiry%20LRU%20cache%20in%20Python%20without%20heaps%20or%20trees%20https%3A//death.andgravity.com/lru-cache"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/lru-cache&amp;title=This%20is%20not%20interview%20advice%3A%20a%20priority-expiry%20LRU%20cache%20in%20Python%20without%20heaps%20or%20trees"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/lru-cache"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=This%20is%20not%20interview%20advice%3A%20a%20priority-expiry%20LRU%20cache%20in%20Python%20without%20heaps%20or%20trees&amp;url=https%3A//death.andgravity.com/lru-cache&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;!--

# Bonus: this is not production code

# Bonus: is bisect really O(log n) for &lt;10k elements?

--&gt;

&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;Note the subtitle: &lt;em&gt;if you're not sure what to do yet&lt;/em&gt;. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;This early on, the name doesn't really matter,
but we'll go with the correct, descriptive one;
in the first draft of the code, it was called MagicDS. ✨ &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-3"&gt;&lt;p&gt;You have to admit this is at least a bit weird;
what you're looking at is an object in a trench coat,
at least if you think &lt;a class="external" href="https://web.archive.org/web/20170512101417/http://wiki.c2.com/?ClosuresAndObjectsAreEquivalent"&gt;closures and objects are equivalent&lt;/a&gt;. &lt;a href="#fnref-3" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-4"&gt;&lt;p&gt;Another way of getting an &amp;quot;object&amp;quot; on the cheap. &lt;a href="#fnref-4" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-5"&gt;&lt;p&gt;If we assume a relatively small number of buckets
that will be reused soon enough,
this isn't strictly necessary.
I'm partly doing it to release the memory held by the dict,
since &lt;a class="external" href="https://github.com/python/cpython/blob/v3.12.0/Objects/dictnotes.txt#L93-L100"&gt;dicts are resized only when items are added&lt;/a&gt;. &lt;a href="#fnref-5" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-6"&gt;&lt;p&gt;There's even a &lt;a class="external" href="https://www.youtube.com/watch?v=7z2Ki44Vs4E&amp;amp;t=922s"&gt;PyCon talk&lt;/a&gt;
with the same explanation, if you prefer that. &lt;a href="#fnref-6" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-7"&gt;&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/library/bisect.html"&gt;bisect&lt;/a&gt; itself has
&lt;a class="external" href="https://github.com/python/cpython/blob/v3.12.0/Lib/bisect.py#L110-L114"&gt;a fast C implementation&lt;/a&gt;,
so I guess &lt;em&gt;technically&lt;/em&gt; it's not pure Python.
But given that stdlib is already there, does that count? &lt;a href="#fnref-7" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-8"&gt;&lt;p&gt;&lt;a class="external" href="https://peps.python.org/pep-0020/"&gt;If the implementation is easy to explain, it may be a good idea.&lt;/a&gt; &lt;a href="#fnref-8" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-9"&gt;&lt;p&gt;This limits priorities to values that can be negated,
so tuples won't work anymore.
We could use a &lt;a class="external" href="https://stackoverflow.com/a/24414594"&gt;&amp;quot;reversed view&amp;quot; wrapper&lt;/a&gt;
if we really cared about that. &lt;a href="#fnref-9" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/lru-cache" rel="alternate"/>
    <summary>Today we're implementing a least recently used cache with priorities and expiry, using only the Python standard library.
This is a bIG TEch CoDINg InTerVIEW problem, so we'll work hard to stay away from the correct™ data structures, but we'll end up with a decent solution anyway!</summary>
    <published>2024-01-20T12:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/reader-3-10">
    <id>https://death.andgravity.com/reader-3-10</id>
    <title>reader 3.10 released – storage internal API</title>
    <updated>2023-11-15T22:00:00+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;I'm happy to announce version 3.10 of &lt;strong&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/strong&gt;, a Python feed reader library.&lt;/p&gt;
&lt;h2 id="what-s-new"&gt;What's new?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-new" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Here are the highlights since &lt;a class="internal" href="/reader-3-9"&gt;reader 3.9&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="storage-internal-api"&gt;Storage internal API&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#storage-internal-api" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;a class="external" href="https://reader.readthedocs.io/en/latest/internal.html#storage"&gt;storage internal API&lt;/a&gt; is now documented!&lt;/p&gt;
&lt;p&gt;This is important because
it &lt;strong&gt;opens up &lt;em&gt;reader&lt;/em&gt; to using other databases&lt;/strong&gt; than SQLite.&lt;/p&gt;
&lt;p&gt;The protocols are &lt;em&gt;mostly&lt;/em&gt; stable,
but some changes are still expected.
The long term goal is full stabilization,
but at least one other implementation needs to exists before that,
to work out any remaining kinks.&lt;/p&gt;
&lt;p&gt;A SQLAlchemy backend would be especially useful,
since it would provide access to a variety of database engines
mostly out of the box.
(Alas, I do not have time nor a need for this at the moment.
Interested on working on it? &lt;a class="external" href="https://reader.readthedocs.io/en/latest/contributing.html"&gt;Let me know!&lt;/a&gt;)&lt;/p&gt;
&lt;h4 id="why-not-use-sqlalchemy-from-the-start"&gt;Why not use SQLAlchemy from the start?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-not-use-sqlalchemy-from-the-start" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h4&gt;
&lt;p&gt;In the beginning:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I wanted to keep things as simple as possible,
so I stay motivated for the long term.
I also wanted to follow a &lt;a class="external" href="https://hintjens.gitbooks.io/scalable-c/content/chapter1.html#problem-what-do-we-do-next"&gt;problem-solution&lt;/a&gt; approach,
which cautions against solving problems you don't have.
(Details on both &lt;a class="internal" href="/own-query-builder#background"&gt;here&lt;/a&gt;
and &lt;a class="internal" href="/reader-3-4#5-years-2000-commits"&gt;here&lt;/a&gt;.)&lt;/li&gt;
&lt;li&gt;By that time, I was already a SQLite fan,
and due to the single-user nature of &lt;em&gt;reader&lt;/em&gt;,
I was relatively confident concurrency won't be an issue.&lt;/li&gt;
&lt;li&gt;I didn't know exactly where and how I would deploy the web app;
&lt;a class="external" href="https://docs.python.org/3/library/sqlite3.html"&gt;sqlite3&lt;/a&gt; being in the standard library made it very appealing.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Since then,
I did come up with some of my own complexity –
&lt;em&gt;reader&lt;/em&gt; has a &lt;a class="internal" href="/query-builder"&gt;query builder&lt;/a&gt; and a migration system
(albeit both of them tiny),
and there were &lt;em&gt;some&lt;/em&gt; concurrency issues.
SQLAlchemy would have likely helped with the first two,
but not with the last.
Overall, I still think plain SQLite was the right choice at the time.&lt;/p&gt;
&lt;h3 id="deprecated-sqlite3-datetime-support"&gt;Deprecated sqlite3 datetime support&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#deprecated-sqlite3-datetime-support" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The default &lt;a class="external" href="https://docs.python.org/3/library/sqlite3.html"&gt;sqlite3&lt;/a&gt; datetime adapters/converters were &lt;a class="external" href="https://docs.python.org/3.12/library/sqlite3.html#default-adapters-and-converters-deprecated"&gt;deprecated&lt;/a&gt; in Python 3.12.
Since adapters/converters apply to &lt;em&gt;all&lt;/em&gt; database connections,
&lt;em&gt;reader&lt;/em&gt; does not have the option of registering its own
(as a library, it should not change global stuff),
so datetime conversions now happen in the storage.
As an upside,
this provided an opportunity to change the storage
to use timezone-aware datetimes.&lt;/p&gt;
&lt;h3 id="share-experimental-plugin"&gt;Share experimental plugin&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#share-experimental-plugin" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;There's a new &lt;a class="external" href="https://reader.readthedocs.io/en/latest/plugins.html#share"&gt;share&lt;/a&gt; web app plugin
to add social sharing links to the entry page.&lt;/p&gt;
&lt;p&gt;Ideally, this functionality should end up in a plugin
that adds them to &lt;code&gt;Entry.links&lt;/code&gt;
(to be exposed in &lt;a class="external" href="https://github.com/lemon24/reader/issues/320"&gt;#320&lt;/a&gt;),
so all &lt;em&gt;reader&lt;/em&gt; users can benefit from it.&lt;/p&gt;
&lt;h3 id="python-versions"&gt;Python versions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#python-versions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;None this time, but Python 3.12 support is coming soon!&lt;/p&gt;
&lt;p&gt;For more details, see the full &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-3-10"&gt;changelog&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Want to contribute?&lt;/strong&gt;
Check out the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/contributing.html"&gt;docs&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/dev.html#roadmap"&gt;roadmap&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/reader-3-10&amp;t=reader%203.10%20released%20%E2%80%93%20storage%20internal%20API"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=reader%203.10%20released%20%E2%80%93%20storage%20internal%20API%20https%3A//death.andgravity.com/reader-3-10"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/reader-3-10&amp;title=reader%203.10%20released%20%E2%80%93%20storage%20internal%20API"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/reader-3-10"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=reader%203.10%20released%20%E2%80%93%20storage%20internal%20API&amp;url=https%3A//death.andgravity.com/reader-3-10&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;h2 id="what-is-reader"&gt;What is &lt;em&gt;reader&lt;/em&gt;?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;reader&lt;/strong&gt; takes care
of the core functionality required by a feed reader,
so you can focus on what makes &lt;strong&gt;yours&lt;/strong&gt; different.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-2-0/reader.png" alt="reader in action" /&gt;
&lt;em&gt;reader&lt;/em&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retrieve, store, and manage &lt;strong&gt;Atom&lt;/strong&gt;, &lt;strong&gt;RSS&lt;/strong&gt;, and &lt;strong&gt;JSON&lt;/strong&gt; feeds&lt;/li&gt;
&lt;li&gt;mark articles as read or important&lt;/li&gt;
&lt;li&gt;add arbitrary tags/metadata to feeds and articles&lt;/li&gt;
&lt;li&gt;filter feeds and articles&lt;/li&gt;
&lt;li&gt;full-text search articles&lt;/li&gt;
&lt;li&gt;get statistics on feed and user activity&lt;/li&gt;
&lt;li&gt;write plugins to extend its functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...all these with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stable, clearly documented API&lt;/li&gt;
&lt;li&gt;excellent test coverage&lt;/li&gt;
&lt;li&gt;fully typed Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find out more, check out the &lt;a class="external" href="https://github.com/lemon24/reader"&gt;GitHub repo&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/"&gt;docs&lt;/a&gt;,
or give the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/tutorial.html"&gt;tutorial&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2 id="why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-use-a-feed-reader-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Have you been unhappy with existing feed readers and wanted to make your own, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never knew where to start?&lt;/li&gt;
&lt;li&gt;it seemed like too much work?&lt;/li&gt;
&lt;li&gt;you don't like writing backend code?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you already working with &lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/"&gt;feedparser&lt;/a&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;want an easier way to store, filter, sort and search feeds and entries?&lt;/li&gt;
&lt;li&gt;want to get back type-annotated objects instead of dicts?&lt;/li&gt;
&lt;li&gt;want to restrict or deny file-system access?&lt;/li&gt;
&lt;li&gt;want to change the way feeds are retrieved by using &lt;a class="external" href="https://requests.readthedocs.io"&gt;Requests&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to also support &lt;a class="external" href="https://jsonfeed.org/"&gt;JSON Feed&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to support custom information sources?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... while still supporting all the feed types feedparser does?&lt;/p&gt;
&lt;p&gt;If you answered yes to any of the above, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;h2 id="the-reader-philosophy"&gt;The &lt;em&gt;reader&lt;/em&gt; philosophy&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-reader-philosophy" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is a library&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is for the long term&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is extensible&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is stable (within reason)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is simple to use; API matters&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; features work well together&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is tested&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is documented&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; has minimal dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="why-make-your-own-feed-reader"&gt;Why make your own feed reader?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-make-your-own-feed-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;have full control over your data&lt;/li&gt;
&lt;li&gt;control what features it has or doesn't have&lt;/li&gt;
&lt;li&gt;decide how much you pay for it&lt;/li&gt;
&lt;li&gt;make sure it doesn't get closed while you're still using it&lt;/li&gt;
&lt;li&gt;really, it's &lt;a class="external" href="https://rachelbythebay.com/w/2011/10/26/fred/"&gt;easier than you think&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously, this may not be your cup of tea, but if it is, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
</content>
    <link href="https://death.andgravity.com/reader-3-10" rel="alternate"/>
    <published>2023-11-12T18:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/same-functions">
    <id>https://death.andgravity.com/same-functions</id>
    <title>When to use classes in Python? When you repeat similar sets of functions</title>
    <updated>2025-07-22T18:00:00+00:00</updated>
    <content type="html">&lt;p&gt;Are you having trouble figuring out when to use classes or how to organize them?&lt;/p&gt;
&lt;p&gt;Have you repeatedly searched for &amp;quot;when to use classes in Python&amp;quot;,
read all the articles and watched all the talks,
and &lt;em&gt;still&lt;/em&gt;  don't know whether you should be using classes in any given situation?&lt;/p&gt;
&lt;p&gt;Have you read discussions about it that for all you know &lt;em&gt;may be right&lt;/em&gt;,
but they're &lt;em&gt;so academic&lt;/em&gt; you can't parse the jargon?&lt;/p&gt;
&lt;p&gt;Have you read articles that all treat the &amp;quot;obvious&amp;quot; cases,
leaving you with no clear answer when you try to apply them to your own code?&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;My experience is that, &lt;strong&gt;unfortunately&lt;/strong&gt;,
the best way to learn this &lt;em&gt;is&lt;/em&gt; to &lt;a class="internal" href="/stdlib"&gt;look at lots of examples&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Most guidelines tend to either be too vague &lt;em&gt;if you don't already know enough&lt;/em&gt; about the subject,
or too specific and saying things you already know.&lt;/p&gt;
&lt;p&gt;This is one of those things that once you get it seems obvious and intuitive,
&lt;em&gt;but it's not&lt;/em&gt;, and is quite difficult to explain properly.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;So, instead of prescribing a general approach,
let's look at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;one specific case&lt;/strong&gt; where you may want to use classes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;examples from real-world code&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;some considerations you should keep in mind&lt;/li&gt;
&lt;/ul&gt;
&lt;details class="toc"&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#the-heuristic"&gt;The heuristic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#example-retrievers"&gt;Example: Retrievers&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#problem-can-t-add-new-feed-sources"&gt;Problem: can't add new feed sources&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#problem-can-t-validate-urls-until-retrieving-them"&gt;Problem: can't validate URLs until retrieving them&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#not-just-functions-attributes-too"&gt;Not just functions, attributes too&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#example-flask-s-tagged-json"&gt;Example: Flask's tagged JSON&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#formalizing-this"&gt;Formalizing this&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#duck-typing"&gt;Duck typing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#inheritance"&gt;Inheritance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#abstract-base-classes"&gt;Abstract base classes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#protocols"&gt;Protocols&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#counter-example-modules"&gt;Counter-example: modules&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#try-it-out"&gt;Try it out&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="the-heuristic"&gt;The heuristic&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-heuristic" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;If you repeat similar sets of functions, consider grouping them in a class.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That's it.&lt;/p&gt;
&lt;p&gt;In its most basic form,
a class is when you group data with functions that operate on that data;
sometimes, there is no data,
but it can still be useful to group the functions
into an &lt;em&gt;abstract object&lt;/em&gt; that exists only
to make things easier to use / understand.&lt;/p&gt;
&lt;p&gt;Depending on whether you choose which class to use at runtime,
this is sometimes called the &lt;a class="external" href="https://en.wikipedia.org/wiki/Strategy_pattern"&gt;strategy pattern&lt;/a&gt;.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;As Wikipedia &lt;a class="external" href="https://simple.wikipedia.org/wiki/Heuristic"&gt;puts it&lt;/a&gt;,
&amp;quot;A &lt;strong&gt;heuristic&lt;/strong&gt; is a practical way to solve a problem.
It is &lt;em&gt;better than chance&lt;/em&gt;, but &lt;em&gt;does not always work&lt;/em&gt;.
A person develops a heuristic by using
intelligence, experience, and common sense.&amp;quot;&lt;/p&gt;
&lt;p&gt;So, this is &lt;strong&gt;not&lt;/strong&gt; the correct thing to do &lt;strong&gt;all the time&lt;/strong&gt;,
or even &lt;em&gt;most&lt;/em&gt; of the time.&lt;/p&gt;
&lt;p&gt;Instead, I hope that this and &lt;em&gt;other&lt;/em&gt; heuristics
can help &lt;strong&gt;build the right intuition&lt;/strong&gt;
for people on their way from
&amp;quot;I know the class syntax, now what?&amp;quot; to
&amp;quot;proper&amp;quot; object-oriented design.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="example-retrievers"&gt;Example: Retrievers&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#example-retrievers" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;My &lt;a class="external" href="https://github.com/lemon24/reader"&gt;feed reader library&lt;/a&gt; retrieves and stores &lt;a class="external" href="https://en.wikipedia.org/wiki/Web_feed"&gt;web feeds&lt;/a&gt;
(Atom, RSS and so on).&lt;/p&gt;
&lt;p&gt;Usually, feeds come from the internet,
but you can also use local files.
The parsers for various formats don't really care where a feed is coming from,
so they always take an open file as input.&lt;/p&gt;
&lt;p&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt; supports conditional requests –
that is, only retrieve a feed if it changed.
To do this, it stores the &lt;a class="external" href="https://en.wikipedia.org/wiki/HTTP_ETag"&gt;ETag&lt;/a&gt; HTTP header from a response,
and passes it back as the If-None-Match header of the next request;
if nothing changed,
the server can respond with &lt;a class="external" href="https://en.wikipedia.org/wiki/List_of_HTTP_status_codes#304"&gt;304 Not Modified&lt;/a&gt;
instead of sending back the full content.&lt;/p&gt;
&lt;p&gt;Let's have a look at how the code to retrieve feeds evolved over time;
this version omits a few details,
but it will end up with a structure similar to that of the &lt;a class="external" href="https://github.com/lemon24/reader/blob/3.9/src/reader/_parser/_lazy.py#L190-L268"&gt;full version&lt;/a&gt;.
In the beginning, there was a function
– URL and old ETag in, file and new ETag out:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;http://&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;https://&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;If-None-Match&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;stream&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raise_for_status&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status_code&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;304&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;
        &lt;span class="n"&gt;etag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ETag&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode_content&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;

    &lt;span class="c1"&gt;# fall back to file&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extract_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rb&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We use &lt;a class="external" href="https://requests.readthedocs.io/"&gt;Requests&lt;/a&gt; to get HTTP URLs,
and return the underlying file-like object.&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;For local files, we suport both bare paths and &lt;a class="external" href="https://en.wikipedia.org/wiki/File_URI_scheme"&gt;file URIs&lt;/a&gt;;
for the latter, we do a bit of validation –
&lt;em&gt;file:feed&lt;/em&gt; and &lt;em&gt;file://localhost/feed&lt;/em&gt; are OK,
but &lt;em&gt;file://invalid/feed&lt;/em&gt; and &lt;em&gt;unknown:feed&lt;/em&gt;&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt; are not:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;extract_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;url_parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parse&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;urlparse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;url_parsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;file&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;url_parsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;netloc&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;localhost&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;unknown authority for file URI&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;urllib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url2pathname&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url_parsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;url_parsed&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scheme&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;unknown scheme for file URI&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# no scheme, treat as a path&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="problem-can-t-add-new-feed-sources"&gt;Problem: can't add new feed sources&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-can-t-add-new-feed-sources" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;One of &lt;em&gt;reader&lt;/em&gt;'s goals is to be extensible.
For example, it should be possible to add new feed sources
like an FTP server (&lt;em&gt;ftp://...&lt;/em&gt;) or &lt;a class="external" href="https://reader.readthedocs.io/en/latest/plugins.html#twitter"&gt;Twitter&lt;/a&gt; without changing &lt;em&gt;reader&lt;/em&gt; code;
however, our current implementation makes it hard to do so.&lt;/p&gt;
&lt;p&gt;We can fix this by extracting retrieval logic
into separate functions, one per protocol:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;http_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;file_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extract_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rb&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;...and then routing to the right one depending on the URL prefix:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# sorted by key length (longest first)&lt;/span&gt;
&lt;span class="n"&gt;RETRIEVERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;https://&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;http_retriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;http://&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;http_retriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="c1"&gt;# fall back to file&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;file_retriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;RETRIEVERS&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;no retriever for URL&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, plugins can register retrievers by adding them to &lt;code&gt;RETRIEVERS&lt;/code&gt;
(in practice, there's &lt;a class="external" href="https://github.com/lemon24/reader/blob/3.9/src/reader/_parser/_lazy.py#L354-L367"&gt;a method for that&lt;/a&gt;,
so users don't need to care about it staying sorted).&lt;/p&gt;
&lt;h3 id="problem-can-t-validate-urls-until-retrieving-them"&gt;Problem: can't validate URLs until retrieving them&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-can-t-validate-urls-until-retrieving-them" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;To add a feed, you call &lt;a class="external" href="https://reader.readthedocs.io/en/latest/api.html#reader.Reader.add_feed"&gt;add_feed()&lt;/a&gt; with the feed URL.&lt;/p&gt;
&lt;p&gt;But what if you pass an invalid URL?
The feed gets stored in the database,
and you get an &amp;quot;unknown scheme for file URI&amp;quot; error on the next update.
However, this can be confusing
– a good API should signal errors near the action that triggered them.
This means &lt;a class="external" href="https://reader.readthedocs.io/en/latest/api.html#reader.Reader.add_feed"&gt;add_feed()&lt;/a&gt; needs to validate the URL
without actually retrieving it.&lt;/p&gt;
&lt;p&gt;For HTTP, Requests can do the validation for us;
for files, we can call &lt;code&gt;extract_path()&lt;/code&gt; and ignore the result.
Of course, we should select the appropriate logic in the same way we select retrievers,
otherwise we're &lt;a class="anchor" href="#problem-can-t-add-new-feed-sources"&gt;back where we started&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now, there's more than one way of doing this.
We could keep a separate validator registry,
but that may accidentally become out of sync with the retriever one.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;URL_VALIDATORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;https://&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;http_url_validator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;http://&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;http_url_validator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;file_url_validator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Or, we could keep a (retriever, validator) pair in the retriever registry.
This is better, but it's not all that readable
(what if need to add a &lt;a class="anchor" href="#not-just-functions-attributes-too"&gt;third thing&lt;/a&gt;?);
also, it makes customizing behavior
that affects both the retriever and validator harder.&lt;/p&gt;
&lt;!-- TODO: link to future "when to use classes" article --&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;RETRIEVERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;https://&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http_retriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http_url_validator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;http://&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;http_retriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;http_url_validator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file_retriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file_url_validator&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Better yet, we can &lt;em&gt;use a class&lt;/em&gt; to make the grouping explicit:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;HTTPRetriever&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;headers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="c1"&gt;# ...&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raw&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;validate_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_adapter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;prepare_request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;requests&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Request&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;GET&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;FileRetriever&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;extract_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rb&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;validate_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;extract_path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We then instantiate them,
and update &lt;code&gt;retrieve()&lt;/code&gt; to call the methods:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;http_retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HTTPRetriever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;file_retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;FileRetriever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;validate_url()&lt;/code&gt; works just the same:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;validate_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;validate_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And there you have it – &lt;strong&gt;if you repeat similar sets of functions, consider grouping them in a class&lt;/strong&gt;.&lt;/p&gt;
&lt;h3 id="not-just-functions-attributes-too"&gt;Not just functions, attributes too&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#not-just-functions-attributes-too" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Say you want to update feeds in parallel, using multiple threads.&lt;/p&gt;
&lt;p&gt;Retrieving feeds is mostly waiting around for I/O,
so it will benefit the most from it.
Parsing, on the other hand,
is pure Python, CPU bound code,
so threads won't help due to the &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-global-interpreter-lock"&gt;global interpreter lock&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;However, because we're &lt;a class="external" href="https://requests.readthedocs.io/en/latest/user/advanced/#body-content-workflow"&gt;streaming the reponse body&lt;/a&gt;,
I/O is not done when the retriever returns the file,
but when the parser finishes reading it.&lt;sup class="footnote-ref" id="fnref-3"&gt;&lt;a href="#fn-3"&gt;3&lt;/a&gt;&lt;/sup&gt;
We can move all the (network) I/O in &lt;code&gt;retrieve()&lt;/code&gt;
by reading the response into a temporary file
and returning it instead.&lt;/p&gt;
&lt;p&gt;We'll allow any retriever to opt into this behavior
by using a &lt;em&gt;class attribute&lt;/em&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;HTTPRetriever&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;slow_to_read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;FileRetriever&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;slow_to_read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If a retriever is slow to read, &lt;code&gt;retrieve()&lt;/code&gt; does the swap:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slow_to_read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;temp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tempfile&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;TemporaryFile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;shutil&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;copyfileobj&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;




&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        Liking this so far? Here&amp;#39;s another article you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/same-arguments"&gt;
            When to use classes in Python? When your functions take the same arguments
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="example-flask-s-tagged-json"&gt;Example: Flask's tagged JSON&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#example-flask-s-tagged-json" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The Flask web framework provides
an extendable compact representation for non-standard JSON types
called &lt;a class="external" href="https://flask.palletsprojects.com/en/2.3.x/api/#tagged-json"&gt;tagged JSON&lt;/a&gt; (&lt;a class="external" href="https://github.com/pallets/flask/blob/2.3.x/src/flask/json/tag.py"&gt;code&lt;/a&gt;).
&lt;a class="external" href="https://flask.palletsprojects.com/en/2.3.x/api/#flask.json.tag.TaggedJSONSerializer"&gt;The serializer class&lt;/a&gt;
delegates most conversion work to methods of various &lt;a class="external" href="https://flask.palletsprojects.com/en/2.3.x/api/#flask.json.tag.JSONTag"&gt;JSONTag&lt;/a&gt; subclasses
(one per supported type):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;check()&lt;/code&gt; checks if a Python value should be tagged by that tag&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tag()&lt;/code&gt; converts it to tagged JSON&lt;/li&gt;
&lt;li&gt;&lt;code&gt;to_python()&lt;/code&gt; converts a JSON value back to Python
(the serializer uses the &lt;code&gt;key&lt;/code&gt; tag attribute to find the correct tag)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Interestingly,
tag instances have an attribute pointing back to the serializer,
likely to allow recursion –
when (un)packing a possibly nested collection,
you need to recursively (un)pack its values.
Passing the serializer to each method would have also worked,
but &lt;a class="internal" href="/same-arguments"&gt;when your functions take the same arguments...&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="formalizing-this"&gt;Formalizing this&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#formalizing-this" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;OK, the retriever code works.
But, how should you communicate to others
(readers, implementers, interpreters, type checkers)
that an HTTPRetriever is the same kind of thing as a FileRetriever,
and as anything else that can go in &lt;code&gt;RETRIEVERS&lt;/code&gt;?&lt;/p&gt;
&lt;h3 id="duck-typing"&gt;Duck typing&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#duck-typing" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Here's the definition of &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-duck-typing"&gt;duck typing&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;A programming style which does not look at an object's type to determine if it has the right interface; instead, the method or attribute is simply called or used (&amp;quot;If it looks like a duck and quacks like a duck, it must be a duck.&amp;quot;) [...]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;!-- By emphasizing interfaces rather than specific types, well-designed code improves its flexibility by allowing polymorphic substitution. Duck-typing avoids tests using [type()] or [isinstance()]. [...] Instead, it typically employs [hasattr()] tests or [EAFP] programming. --&gt;

&lt;p&gt;This is what we're doing now!
If it retrieves like a retriever and validates URLs like a retriever,
then it's a retriever.&lt;/p&gt;
&lt;p&gt;You see this all the time in Python.
For example, &lt;a class="external" href="https://docs.python.org/3/library/json.html#json.dump"&gt;json.dump()&lt;/a&gt; takes a &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-file-object"&gt;file-like object&lt;/a&gt;;
now, the full &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-text-file"&gt;text file&lt;/a&gt; &lt;a class="external" href="https://docs.python.org/3/library/io.html#io.TextIOBase"&gt;interface&lt;/a&gt;
has lots methods and attributes,
but dump() only cares about &lt;a class="external" href="https://docs.python.org/3/library/io.html#io.TextIOBase.write"&gt;write()&lt;/a&gt;,
and will accept &lt;em&gt;any&lt;/em&gt; object implementing it:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;MyFile&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;writing: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MyFile&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;writing: {&lt;/span&gt;
&lt;span class="go"&gt;writing: &amp;quot;one&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;writing: :&lt;/span&gt;
&lt;span class="go"&gt;writing: 1&lt;/span&gt;
&lt;span class="go"&gt;writing: }&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The main way to communicate this is through documentation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Serialize &lt;em&gt;obj&lt;/em&gt; [...] to &lt;em&gt;fp&lt;/em&gt; (a &lt;code&gt;.write()&lt;/code&gt;-supporting &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-file-object"&gt;file-like object&lt;/a&gt;)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="inheritance"&gt;Inheritance&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#inheritance" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Nevertheless, you may want to be more explicit about
the relationships between types.
The easiest option is to use a base class,
and require retrievers to inherit from it.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Retriever&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;slow_to_read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;NotImplementedError&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;validate_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;NotImplementedError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This allows you to check you the type with &lt;a class="external" href="https://docs.python.org/3/library/functions.html#isinstance"&gt;isinstance()&lt;/a&gt;,
provide default methods and attributes,
and will help type checkers and autocompletion,
at the expense of forcing a dependency on the base class.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;MyRetriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Retriever&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MyRetriever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;slow_to_read&lt;/span&gt;
&lt;span class="go"&gt;False&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Retriever&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;What it won't do is check subclasses actually define the methods:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;validate_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;myurl&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;NotImplementedError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="abstract-base-classes"&gt;Abstract base classes&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#abstract-base-classes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;This is where &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-abstract-base-class"&gt;abstract base classes&lt;/a&gt; come in.
The decorators in the &lt;a class="external" href="https://docs.python.org/3/library/abc.html"&gt;abc&lt;/a&gt; module allow defining abstract methods
that &lt;em&gt;must&lt;/em&gt; be overriden:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ABC&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="nd"&gt;@abstractproperty&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;slow_to_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;

    &lt;span class="nd"&gt;@abstractmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;NotImplementedError&lt;/span&gt;

    &lt;span class="nd"&gt;@abstractmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;validate_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;NotImplementedError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This is checked at runtime
(but only that methods and attributes &lt;em&gt;are present&lt;/em&gt;,
not their signatures or types):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;MyRetriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Retriever&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;MyRetriever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;Can&amp;#39;t instantiate abstract class MyRetriever with abstract methods retrieve, slow_to_read, validate_url&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;MyRetriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Retriever&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;slow_to_read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;validate_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;MyRetriever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;__main__.MyRetriever object at 0x1037aac50&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;You can also use ABCs to register arbitrary types
as &amp;quot;virtual subclasses&amp;quot;;
this allows them to pass &lt;a class="external" href="https://docs.python.org/3/library/functions.html#isinstance"&gt;isinstance()&lt;/a&gt; checks
without inheritance,
but won't check for required methods:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;MyRetriever&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;Retriever&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;register&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MyRetriever&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;class &amp;#39;__main__.MyRetriever&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;MyRetriever&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;Retriever&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;h3 id="protocols"&gt;Protocols&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#protocols" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Finally, we have protocols, aka structural subtyping,
aka static &lt;a class="anchor" href="#duck-typing"&gt;duck typing&lt;/a&gt;.
Introduced in &lt;a class="external" href="https://peps.python.org/pep-0544/"&gt;PEP 544&lt;/a&gt;,
they go in the opposite direction –
what if instead declaring what the type of something &lt;em&gt;is&lt;/em&gt;,
we declare what methods it has to have &lt;em&gt;to be&lt;/em&gt; of a specific type?&lt;/p&gt;
&lt;p&gt;You define a protocol by inheriting &lt;a class="external" href="https://docs.python.org/3/library/typing.html#typing.Protocol"&gt;typing.Protocol&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Protocol&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="nd"&gt;@property&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;slow_to_read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;IO&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;validate_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;...and then use it in type annotations:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;mount_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;retriever&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Retriever&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;NotImplementedError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Some other code
(not necessarily yours, not necessarily aware the protocol even exists)
defines an implementation:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;MyRetriever&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;slow_to_read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;validate_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;...and then uses it with annotated code:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;mount_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;my&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MyRetriever&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;A type checker like &lt;a class="external" href="https://mypy.readthedocs.io/en/stable/protocols.html"&gt;mypy&lt;/a&gt; will check if the provided instance conforms to the protocol
– not only that methods exist, but that their signatures are correct too
– all without the implementation having to declare anything.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;mypy&lt;span class="w"&gt; &lt;/span&gt;myproto.py
&lt;span class="go"&gt;myproto.py:11: error: Argument 2 to &amp;quot;mount_retriever&amp;quot; has incompatible type &amp;quot;MyRetriever&amp;quot;; expected &amp;quot;Retriever&amp;quot;  [arg-type]&lt;/span&gt;
&lt;span class="go"&gt;myproto.py:11: note: &amp;quot;MyRetriever&amp;quot; is missing following &amp;quot;Retriever&amp;quot; protocol member:&lt;/span&gt;
&lt;span class="go"&gt;myproto.py:11: note:     retrieve&lt;/span&gt;
&lt;span class="go"&gt;myproto.py:11: note: Following member(s) of &amp;quot;MyRetriever&amp;quot; have conflicts:&lt;/span&gt;
&lt;span class="go"&gt;myproto.py:11: note:     Expected:&lt;/span&gt;
&lt;span class="go"&gt;myproto.py:11: note:         def validate_url(self, url: str) -&amp;gt; None&lt;/span&gt;
&lt;span class="go"&gt;myproto.py:11: note:     Got:&lt;/span&gt;
&lt;span class="go"&gt;myproto.py:11: note:         def validate_url(self) -&amp;gt; Any&lt;/span&gt;
&lt;span class="go"&gt;Found 1 error in 1 file (checked 1 source file)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;If you decorate your protocol with &lt;a class="external" href="https://docs.python.org/3/library/typing.html#typing.runtime_checkable"&gt;runtime_checkable&lt;/a&gt;,
you can use it in &lt;a class="external" href="https://docs.python.org/3/library/functions.html#isinstance"&gt;isinstance()&lt;/a&gt; checks,
but like ABCs, it only checks methods are present.&lt;/p&gt;
&lt;/section&gt;
&lt;!-- TODO: zope.interface: https://peps.python.org/pep-0544/#existing-approaches-to-structural-subtyping --&gt;

&lt;h2 id="counter-example-modules"&gt;Counter-example: modules&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#counter-example-modules" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;If a class has no state
and you don't need inheritance,
you can use a module instead:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# module.py&lt;/span&gt;

&lt;span class="n"&gt;slow_to_read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;retrieve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;etag&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;NotImplementedError&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;validate_url&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;NotImplementedError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;From a &lt;a class="anchor" href="#duck-typing"&gt;duck typing&lt;/a&gt; perspective,
this is a valid retriever,
since it has all the expected methods and attributes.
So much so, that it's also compatible with &lt;a class="anchor" href="#protocols"&gt;protocols&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="IPython"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;module&lt;/span&gt;

&lt;span class="n"&gt;mount_retriever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mod&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;module&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;mypy&lt;span class="w"&gt; &lt;/span&gt;module.py
&lt;span class="go"&gt;Success: no issues found in 1 source file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I tried to keep the retriever example stateless,
but real world classes rarely are
(it may be &lt;a class="internal" href="/same-arguments#solution-make-the-class-immutable"&gt;immutable state&lt;/a&gt;, but it's state nonetheless).
Also, you're limited to exactly one implementation per module,
which is usually too much like Java for my taste.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;For a somewhat forced, but illustrative example
of a stateful &lt;a class="external" href="https://docs.python.org/3/library/concurrent.futures.html"&gt;concurrent.​futures&lt;/a&gt; executor implemented like this,
and a comparison with class-based alternatives,
check out &lt;a class="internal" href="/over-composition"&gt;Inheritance over composition, sometimes&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="try-it-out"&gt;Try it out&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#try-it-out" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;If you're doing something and you think you need a class,
do it and see how it looks.
If you think it's better, keep it,
otherwise, revert the change.
You can always switch in either direction later.&lt;/p&gt;
&lt;p&gt;If you got it right the first time, great!
If not, &lt;strong&gt;by having to fix it you'll learn something&lt;/strong&gt;,
and next time you'll know better.&lt;/p&gt;
&lt;p&gt;Also, don't beat yourself up.&lt;/p&gt;
&lt;p&gt;Sure, there are nice libraries out there
that use classes in &lt;em&gt;just the right way&lt;/em&gt;,
after spending lots of time to find the right abstraction.
But &lt;strong&gt;abstraction is difficult and time consuming&lt;/strong&gt;,
and in everyday code good enough is just that – good enough –
you don't need to go to the extreme.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/same-functions&amp;t=When%20to%20use%20classes%20in%20Python%3F%20When%20you%20repeat%20similar%20sets%20of%20functions"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=When%20to%20use%20classes%20in%20Python%3F%20When%20you%20repeat%20similar%20sets%20of%20functions%20https%3A//death.andgravity.com/same-functions"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/same-functions&amp;title=When%20to%20use%20classes%20in%20Python%3F%20When%20you%20repeat%20similar%20sets%20of%20functions"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/same-functions"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=When%20to%20use%20classes%20in%20Python%3F%20When%20you%20repeat%20similar%20sets%20of%20functions&amp;url=https%3A//death.andgravity.com/same-functions&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;





&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        If you&amp;#39;ve made it this far, you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/query-builder-how"&gt;
            Write an SQL query builder in 150 lines of Python!
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;This code has a potential bug:
if we were using a &lt;a class="external" href="https://requests.readthedocs.io/en/latest/user/advanced/#session-objects"&gt;persistent session&lt;/a&gt; instead of a transient one,
the connection would never be released,
since we're not closing the response after we're done with it.
In the actual code, we're doing both,
but the only way do so reliably is to &lt;a class="external" href="https://github.com/lemon24/reader/blob/3.9/src/reader/_parser/http.py#L39-L103"&gt;return a context manager&lt;/a&gt;;
I omitted this because it doesn't add anything
to our discussion about classes. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;We're handling unknown URI schemes here
because bare paths don't have a scheme,
so anything that didn't match a known scheme must be a bare path.
Also, on Windows (not supported yet),
the drive letter in a path like &lt;em&gt;c:\feed.xml&lt;/em&gt;
is indistinguishable from a scheme. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-3"&gt;&lt;p&gt;Unless the response is small enough to fit in the TCP receive buffer. &lt;a href="#fnref-3" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/same-functions" rel="alternate"/>
    <summary>Having trouble figuring out when to use classes? In this article, we look at another heuristic for using classes in Python, with examples from real-world code, and some things to keep in mind.</summary>
    <published>2023-09-11T21:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/reader-3-9">
    <id>https://death.andgravity.com/reader-3-9</id>
    <title>reader 3.9 released – update hook error handling</title>
    <updated>2023-08-28T20:00:00+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;I'm happy to announce version 3.9 of &lt;strong&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/strong&gt;, a Python feed reader library.&lt;/p&gt;
&lt;h2 id="what-s-new"&gt;What's new?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-new" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Here are the highlights since &lt;a class="internal" href="/reader-3-7"&gt;reader 3.7&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="better-handling-of-unexpected-update-errors"&gt;Better handling of unexpected update errors&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#better-handling-of-unexpected-update-errors" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Unexpected exceptions raised by &lt;a class="external" href="https://reader.readthedocs.io/en/latest/api.html#reader.Reader.before_feeds_update_hooks"&gt;update hooks&lt;/a&gt;, &lt;a class="external" href="https://reader.readthedocs.io/en/latest/internal.html#reader._parser.RetrieverType"&gt;retrievers&lt;/a&gt;, and &lt;a class="external" href="https://reader.readthedocs.io/en/latest/internal.html#reader._parser.ParserType"&gt;parsers&lt;/a&gt;
are now wrapped in UpdateError,
so errors for one feed don't prevent others from &lt;a class="external" href="https://reader.readthedocs.io/en/latest/guide.html#updating-feeds"&gt;being updated&lt;/a&gt;.
Also, hooks that run after a feed is updated are all run,
regardless of individual failures.
&lt;a class="external" href="https://reader.readthedocs.io/en/latest/guide.html#custom-plugins"&gt;Plugins&lt;/a&gt; should benefit most from the improved fault isolation.&lt;/p&gt;
&lt;h3 id="exception-hierarchy-diagram"&gt;Exception hierarchy diagram&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#exception-hierarchy-diagram" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The API docs got a cool new &lt;a class="external" href="https://reader.readthedocs.io/en/latest/api.html#exctree"&gt;exception hierarchy diagram&lt;/a&gt; (yes, it's &lt;a class="external" href="https://github.com/lemon24/reader/blob/3.8/docs/conf.py#L172-L249"&gt;autogenerated&lt;/a&gt;):&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;ReaderError
 ├── ReaderWarning [UserWarning]
 ├── ResourceNotFoundError
 ├── FeedError
 │    ├── FeedExistsError
 │    ├── FeedNotFoundError [ResourceNotFoundError]
 │    └── InvalidFeedURLError [ValueError]
 ├── EntryError
 │    ├── EntryExistsError
 │    └── EntryNotFoundError [ResourceNotFoundError]
 ├── UpdateError
 │    ├── ParseError [FeedError, ReaderWarning]
 │    └── UpdateHookError
 │         ├── SingleUpdateHookError
 │         └── UpdateHookErrorGroup [ExceptionGroup]
 ├── StorageError
 ├── SearchError
 │    ├── SearchNotEnabledError
 │    └── InvalidSearchQueryError [ValueError]
 ├── PluginError
 │    ├── InvalidPluginError [ValueError]
 │    └── PluginInitError
 └── TagError
      └── TagNotFoundError
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id="parser-cleanup"&gt;Parser cleanup&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#parser-cleanup" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;I moved all modules related to feed retrieval and parsing to &lt;a class="external" href="https://github.com/lemon24/reader/tree/3.9/src/reader/_parser"&gt;reader._parser&lt;/a&gt;,
another step towards internal API &lt;a class="internal" href="/reader-3-4#parser-internal-api"&gt;stabilization&lt;/a&gt;.
This has also given me an opportunity to make
&lt;a class="internal" href="/reader-3-4#faster-imports"&gt;lazy imports&lt;/a&gt; a bit less intrusive.&lt;/p&gt;
&lt;h3 id="timer-experimental-plugin"&gt;Timer experimental plugin&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#timer-experimental-plugin" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;There's a new &lt;a class="external" href="https://reader.readthedocs.io/en/latest/plugins.html#timer"&gt;timer&lt;/a&gt; experimental plugin to collect per-call method timings.&lt;/p&gt;
&lt;p&gt;The web app shows them in the footer like so:&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-3-9/stats.png" alt="web app method statistics" /&gt;&lt;/p&gt;
&lt;h3 id="python-versions"&gt;Python versions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#python-versions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Python 3.9 support is no more,
&lt;a class="internal" href="/reader-3-7#python-versions"&gt;as foretold in the ancient murals&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For more details, see the full &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-3-9"&gt;changelog&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Want to contribute?&lt;/strong&gt;
Check out the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/contributing.html"&gt;docs&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/dev.html#roadmap"&gt;roadmap&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/reader-3-9&amp;t=reader%203.9%20released%20%E2%80%93%20update%20hook%20error%20handling"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=reader%203.9%20released%20%E2%80%93%20update%20hook%20error%20handling%20https%3A//death.andgravity.com/reader-3-9"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/reader-3-9&amp;title=reader%203.9%20released%20%E2%80%93%20update%20hook%20error%20handling"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/reader-3-9"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=reader%203.9%20released%20%E2%80%93%20update%20hook%20error%20handling&amp;url=https%3A//death.andgravity.com/reader-3-9&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;h2 id="what-is-reader"&gt;What is &lt;em&gt;reader&lt;/em&gt;?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;reader&lt;/strong&gt; takes care
of the core functionality required by a feed reader,
so you can focus on what makes &lt;strong&gt;yours&lt;/strong&gt; different.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-2-0/reader.png" alt="reader in action" /&gt;
&lt;em&gt;reader&lt;/em&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retrieve, store, and manage &lt;strong&gt;Atom&lt;/strong&gt;, &lt;strong&gt;RSS&lt;/strong&gt;, and &lt;strong&gt;JSON&lt;/strong&gt; feeds&lt;/li&gt;
&lt;li&gt;mark articles as read or important&lt;/li&gt;
&lt;li&gt;add arbitrary tags/metadata to feeds and articles&lt;/li&gt;
&lt;li&gt;filter feeds and articles&lt;/li&gt;
&lt;li&gt;full-text search articles&lt;/li&gt;
&lt;li&gt;get statistics on feed and user activity&lt;/li&gt;
&lt;li&gt;write plugins to extend its functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...all these with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stable, clearly documented API&lt;/li&gt;
&lt;li&gt;excellent test coverage&lt;/li&gt;
&lt;li&gt;fully typed Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find out more, check out the &lt;a class="external" href="https://github.com/lemon24/reader"&gt;GitHub repo&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/"&gt;docs&lt;/a&gt;,
or give the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/tutorial.html"&gt;tutorial&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2 id="why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-use-a-feed-reader-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Have you been unhappy with existing feed readers and wanted to make your own, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never knew where to start?&lt;/li&gt;
&lt;li&gt;it seemed like too much work?&lt;/li&gt;
&lt;li&gt;you don't like writing backend code?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you already working with &lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/"&gt;feedparser&lt;/a&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;want an easier way to store, filter, sort and search feeds and entries?&lt;/li&gt;
&lt;li&gt;want to get back type-annotated objects instead of dicts?&lt;/li&gt;
&lt;li&gt;want to restrict or deny file-system access?&lt;/li&gt;
&lt;li&gt;want to change the way feeds are retrieved by using &lt;a class="external" href="https://requests.readthedocs.io"&gt;Requests&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to also support &lt;a class="external" href="https://jsonfeed.org/"&gt;JSON Feed&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to support custom information sources?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... while still supporting all the feed types feedparser does?&lt;/p&gt;
&lt;p&gt;If you answered yes to any of the above, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;h2 id="the-reader-philosophy"&gt;The &lt;em&gt;reader&lt;/em&gt; philosophy&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-reader-philosophy" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is a library&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is for the long term&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is extensible&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is stable (within reason)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is simple to use; API matters&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; features work well together&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is tested&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is documented&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; has minimal dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="why-make-your-own-feed-reader"&gt;Why make your own feed reader?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-make-your-own-feed-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;have full control over your data&lt;/li&gt;
&lt;li&gt;control what features it has or doesn't have&lt;/li&gt;
&lt;li&gt;decide how much you pay for it&lt;/li&gt;
&lt;li&gt;make sure it doesn't get closed while you're still using it&lt;/li&gt;
&lt;li&gt;really, it's &lt;a class="external" href="https://rachelbythebay.com/w/2011/10/26/fred/"&gt;easier than you think&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously, this may not be your cup of tea, but if it is, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
</content>
    <link href="https://death.andgravity.com/reader-3-9" rel="alternate"/>
    <published>2023-08-28T20:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/asyncio-bridge">
    <id>https://death.andgravity.com/asyncio-bridge</id>
    <title>Running async code from sync code in Python</title>
    <updated>2023-08-05T12:00:00+00:00</updated>
    <content type="html">&lt;p&gt;So, you're doing some sync stuff.&lt;/p&gt;
&lt;p&gt;But you also need to do &lt;a class="internal" href="/limit-concurrency"&gt;some async stuff&lt;/a&gt;,
&lt;strong&gt;without&lt;/strong&gt; making &lt;strong&gt;everything async&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Maybe the sync stuff is an existing application.&lt;/p&gt;
&lt;p&gt;Maybe you still want to use &lt;a class="internal" href="/output"&gt;your favorite sync library&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Or maybe you need just &lt;a class="external" href="https://twitter.com/_andgravity/status/1678431313332785152" title="as a treat"&gt;a little async&lt;/a&gt;,
without having to pay the full price.&lt;/p&gt;
&lt;p&gt;Of course,
you can run a coroutine with &lt;a class="external" href="https://docs.python.org/3/library/asyncio-runner.html#asyncio.run"&gt;asyncio.run()&lt;/a&gt;,
and blocking sync code from a coroutine with &lt;a class="external" href="https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread"&gt;asyncio.to_thread()&lt;/a&gt;,
but the former isn't granular enough,
and the latter doesn't solve async code being at the top.&lt;/p&gt;
&lt;p&gt;As always, there must be a better way.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Maybe something like this?&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
   &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
       &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;

&lt;span class="n"&gt;runner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ThreadRunner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;double&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;  &lt;span class="c1"&gt;# 4&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wrap_iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arange&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;  &lt;span class="c1"&gt;# 0 1 2 3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;!--
```python
session = runner.enter_context(factory=aiohttp.ClientSession)
coroutine = session.get('https://death.andgravity.com/')
with runner.wrap_context(coroutine) as response:
    lines = list(runner.wrap_iter(response.content))
print('got', len(lines), 'lines')  # got 624 lines
```
--&gt;

&lt;p&gt;Now, it would take a long time to fully explore
how you might build up to such a thing
and why you would actually need it...
so that's exactly what we're going to do.&lt;/p&gt;
&lt;!--
Now, it would take a long time to fully explore
all the different ways that you would feel this music,
feel this 9 8 groove,
and so that's exactly what we're going to do.
--&gt;

&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Update (August 2025)&lt;/p&gt;
&lt;p&gt;You can now install this
from &lt;a class="external" href="https://pypi.org/project/asyncio-thread-runner"&gt;PyPI&lt;/a&gt;! &lt;a class="external" href="https://github.com/lemon24/asyncio-thread-runner"&gt;⭐️&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;
&lt;details class="toc" open&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#but-i-need-to-do-the-thing-more-than-once"&gt;But I need to do the thing more than once&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#but-i-want-a-long-lived-event-loop"&gt;But I want a long-lived event loop&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#but-i-want-a-long-lived-session"&gt;But I want a long-lived session&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#but-i-want-to-do-something-else-during-the-async-stuff"&gt;But I want to do something else during the async stuff&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#but-i-want-to-do-async-stuff-from-multiple-threads"&gt;But I want to do async stuff from multiple threads&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#but-i-don-t-want-to-manage-async-context-managers-by-hand"&gt;But I don't want to manage async context managers by hand&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#but-i-want-to-use-async-iterables"&gt;But I want to use async iterables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#so-should-you-do-this"&gt;So, should you do this?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;!-- # Getting started --&gt;

&lt;h2 id="but-i-need-to-do-the-thing-more-than-once"&gt;But I need to do the thing more than once&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-need-to-do-the-thing-more-than-once" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;To make our example more life-like,
we'll make some requests using &lt;a class="external" href="https://docs.aiohttp.org/"&gt;aiohttp&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://death.andgravity.com/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_running_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;got&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;in loop&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We create the session separately,
since we might want to reuse it later on.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/library/asyncio-runner.html#asyncio.run"&gt;asyncio.run()&lt;/a&gt; allows you to execute a coroutine
in a transient event loop;
it's meant to be the entry point of your program,
and &amp;quot;should ideally only be called once&amp;quot;
– but you &lt;em&gt;can&lt;/em&gt; call it multiple times:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;brigde.py
&lt;span class="go"&gt;got 200 in loop 4399589904&lt;/span&gt;
&lt;span class="go"&gt;got 200 in loop 4386034640&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;!-- # Same loop --&gt;

&lt;h2 id="but-i-want-a-long-lived-event-loop"&gt;But I want a long-lived event loop&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-want-a-long-lived-event-loop" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Of course,
creating and cleaning up a loop for each call isn't that efficient,
and we might have loop-bound resources that need to live across calls.&lt;/p&gt;
&lt;p&gt;With a bit of diving into &lt;a class="external" href="https://docs.python.org/3/library/asyncio-eventloop.html"&gt;asyncio guts&lt;/a&gt;,
we can manage our own long-lived loop:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;new_event_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_event_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_until_complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_until_complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_until_complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdown_asyncgens&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_until_complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;shutdown_default_executor&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;These are low-level APIs, so a bit of care is needed to use them correctly:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;We could have used &lt;a class="external" href="https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.get_event_loop"&gt;get_event_loop()&lt;/a&gt; to both create and set the loop,
but this behavior was deprecated in Python 3.10.&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.python.org/3/library/asyncio-eventloop.html#asyncio.loop.close"&gt;loop.close()&lt;/a&gt; already schedules executor shutdown,
but does not wait for it to finish;
for predictable behavior,
it's best to wait cleanup tasks explicitly.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Anyway, it works – note the loop id:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;brigde.py
&lt;span class="go"&gt;got 200 in loop 4509807312&lt;/span&gt;
&lt;span class="go"&gt;got 200 in loop 4509807312&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;!-- ## Runner --&gt;

&lt;p&gt;Thankfully, starting with Python 3.11, &lt;a class="external" href="https://docs.python.org/3/library/asyncio-runner.html#asyncio.Runner"&gt;asyncio.Runner&lt;/a&gt; makes this much easier:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;!-- # Same loop --&gt;

&lt;h2 id="but-i-want-a-long-lived-session"&gt;But I want a long-lived session&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-want-a-long-lived-session" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Reusing the loop is not enough, though.&lt;/p&gt;
&lt;p&gt;We're  throwing the &lt;a class="external" href="https://docs.aiohttp.org/en/v3.8.4/client_reference.html#client-session"&gt;Session&lt;/a&gt; away after each request,
and with it the associated connection pool,
which means each request creates a new connection.
Reusing connections can make repeated requests much faster –
most &amp;quot;client&amp;quot; libraries (e.g. &lt;a class="external" href="https://github.com/aio-libs/aiobotocore"&gt;aiobotocore&lt;/a&gt;) keep a connection pool around
for this reason.&lt;/p&gt;
&lt;p&gt;Creating the session outside an async function and passing it in works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;...but with a bunch of warnings:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;brigde.py
&lt;span class="go"&gt;.../bridge.py:16: DeprecationWarning: The object should be created within an async function&lt;/span&gt;
&lt;span class="go"&gt;  session = aiohttp.ClientSession()&lt;/span&gt;
&lt;span class="go"&gt;got 200 in loop 4570601360&lt;/span&gt;
&lt;span class="go"&gt;got 200 in loop 4570601360&lt;/span&gt;
&lt;span class="go"&gt;Unclosed client session&lt;/span&gt;
&lt;span class="go"&gt;client_session: &amp;lt;aiohttp.client.ClientSession object at 0x10ff01c90&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Update (2025)&lt;/p&gt;
&lt;p&gt;...until it does not work at all &lt;a class="external" href="https://docs.aiohttp.org/en/stable/faq.html#why-is-creating-a-clientsession-outside-of-an-event-loop-dangerous"&gt;anymore&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python Traceback"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="x"&gt;$ python bridge.py&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;.../bridge.py&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;16&lt;/span&gt;, in &lt;span class="n"&gt;&amp;lt;module&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;.../aiohttp/client.py&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;316&lt;/span&gt;, in &lt;span class="n"&gt;__init__&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_running_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="gr"&gt;RuntimeError&lt;/span&gt;: &lt;span class="n"&gt;no running event loop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;p&gt;The first warning is easy
– the constructor wants to be called in an async function,
so we write one to call it in
(to pass in constructor arguments, we can use a &lt;a class="external" href="https://docs.python.org/3/library/functools.html#functools.partial"&gt;partial()&lt;/a&gt;):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;call_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;callable&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;callable&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second one happens because
we're expected to &lt;code&gt;async session.close()&lt;/code&gt; after we're done with it.
But we didn't do that before –
we used the (async) &lt;code&gt;with&lt;/code&gt; statement,
the idiomatic way for objects to clean up after themselves.
Since &lt;code&gt;async with&lt;/code&gt; is syntactic sugar over
calling the &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-asynchronous-context-manager"&gt;asynchronous context manager&lt;/a&gt; protocol methods
&lt;a class="external" href="https://snarky.ca/unravelling-the-async-with-statement/"&gt;in a specific way&lt;/a&gt;,
we can simulate it by doing that:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__aenter__&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__aexit__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;!-- # Threads --&gt;

&lt;h2 id="but-i-want-to-do-something-else-during-the-async-stuff"&gt;But I want to do something else during the async stuff&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-want-to-do-something-else-during-the-async-stuff" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;OK, we have a long-lived event loop
and a long-lived session.&lt;/p&gt;
&lt;p&gt;But when doing async stuff,
we can't do anything else,
because we are busy running the event loop;
and as a consequence of that,
outside of &lt;a class="external" href="https://docs.python.org/3/library/asyncio-runner.html#asyncio.Runner.run"&gt;run()&lt;/a&gt;
all async stuff is frozen.&lt;/p&gt;
&lt;p&gt;So let's use the &lt;em&gt;other&lt;/em&gt; way of doing more than one thing at a time,
and call &lt;a class="external" href="https://docs.python.org/3/library/asyncio-runner.html#asyncio.Runner.run"&gt;run()&lt;/a&gt; from another thread. First, more logging:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://death.andgravity.com/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_running_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;current_thread&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;got&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;in loop&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;in&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;brigde.py
&lt;span class="go"&gt;got 200 in loop 4538933264 in MainThread&lt;/span&gt;
&lt;span class="go"&gt;got 200 in loop 4538933264 in MainThread&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;!-- ## One thread --&gt;

&lt;p&gt;&lt;a id="one-thread"&gt;&lt;/a&gt;
Next up, the thread stuff –
in actual code, the &amp;quot;do something else&amp;quot; part would happen between
&lt;code&gt;thread.start()&lt;/code&gt; and &lt;code&gt;thread.join()&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;do_stuff_in_threads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;threads&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;),))&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;thread&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;threads&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__aenter__&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;do_stuff_in_threads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__aexit__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Hey, it works!&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;brigde.py
&lt;span class="go"&gt;got 200 in loop 4448024592 in Thread-1 (run)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;!-- ## Two threads --&gt;

&lt;h2 id="but-i-want-to-do-async-stuff-from-multiple-threads"&gt;But I want to do async stuff from multiple threads&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-want-to-do-async-stuff-from-multiple-threads" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;OK, but does it work from two threads?
This can be the case in an existing application.
Or we can &lt;em&gt;deliberately&lt;/em&gt; use threads most of the time,
because they work with normal code,
and only use async as an implementation detail.
Or, we just have one other thread, like above,
but then accidentally call &lt;a class="external" href="https://docs.python.org/3/library/asyncio-runner.html#asyncio.Runner.run"&gt;run()&lt;/a&gt; from the main thread.&lt;/p&gt;
&lt;p&gt;Glad you asked!&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;35&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="n"&gt;do_stuff_in_threads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python Traceback"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="x"&gt;$ python brigde.py&lt;/span&gt;
&lt;span class="x"&gt;Exception in thread Thread-2 (run):&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;.../asyncio/runners.py&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;118&lt;/span&gt;, in &lt;span class="n"&gt;run&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_until_complete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;task&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;.../asyncio/base_events.py&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;629&lt;/span&gt;, in &lt;span class="n"&gt;run_until_complete&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_check_running&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;.../asyncio/base_events.py&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;588&lt;/span&gt;, in &lt;span class="n"&gt;_check_running&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;This event loop is already running&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gr"&gt;RuntimeError&lt;/span&gt;: &lt;span class="n"&gt;This event loop is already running&lt;/span&gt;
&lt;span class="x"&gt;got 200 in loop 4544447440 in Thread-1 (run)&lt;/span&gt;
&lt;span class="x"&gt;got 200 in loop 4544447440 in Thread-1 (run)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Not only did one thread crash,
both coroutines ran in the other thread.
Worse, sometimes only one runs.
In a long-running program with infrequent uses of &lt;a class="external" href="https://docs.python.org/3/library/asyncio-runner.html#asyncio.Runner.run"&gt;run()&lt;/a&gt;,
we might not even get a hard failure,
but subtle, mysterious bugs. 👻&lt;/p&gt;
&lt;p&gt;And why not? The docs have a &lt;a class="external" href="https://docs.python.org/3/library/asyncio-dev.html#concurrency-and-multithreading"&gt;Concurrency and Multithreading&lt;/a&gt; section that warns:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Almost all asyncio objects are not thread safe,
which is typically not a problem unless
there is code that works with them from outside of a Task or a callback.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;...and that's exactly what we've been doing.
But the warning comes with a solution:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;To schedule a coroutine object from a different OS thread,
the &lt;a class="external" href="https://docs.python.org/3/library/asyncio-task.html#asyncio.run_coroutine_threadsafe"&gt;run_coroutine_threadsafe()&lt;/a&gt; function should be used.&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;So, we want a loop running continuously in another thread,
not just when we call &lt;a class="external" href="https://docs.python.org/3/library/asyncio-runner.html#asyncio.Runner.run"&gt;run()&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;runner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;loop_created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_event_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;loop_created&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;loop_thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;LoopThread&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;loop_thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;loop_created&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_coroutine_threadsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We have to wait for the loop to be created in its own thread,
otherwise the main thread can race ahead,
create the loop first with its &lt;a class="external" href="https://docs.python.org/3/library/asyncio-runner.html#asyncio.Runner.get_loop"&gt;get_loop()&lt;/a&gt; call,
and then deadlock when &lt;a class="external" href="https://docs.python.org/3/library/asyncio-task.html#asyncio.run_coroutine_threadsafe"&gt;run_coroutine_threadsafe()&lt;/a&gt; is passed
a loop that isn't running yet.&lt;/p&gt;
&lt;p&gt;We can go ahead and plug in the new &lt;code&gt;run()&lt;/code&gt;,
winding things down after we're done:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;
&lt;span class="normal"&gt;52&lt;/span&gt;
&lt;span class="normal"&gt;53&lt;/span&gt;
&lt;span class="normal"&gt;54&lt;/span&gt;
&lt;span class="normal"&gt;55&lt;/span&gt;
&lt;span class="normal"&gt;56&lt;/span&gt;
&lt;span class="normal"&gt;57&lt;/span&gt;
&lt;span class="normal"&gt;58&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__aenter__&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;do_stuff_in_threads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__aexit__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_soon_threadsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;loop_thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This time, it really works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;brigde.py
&lt;span class="go"&gt;got 200 in loop 4444839248 in LoopThread&lt;/span&gt;
&lt;span class="go"&gt;got 200 in loop 4444839248 in LoopThread&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;!-- ## Thread runner --&gt;

&lt;p&gt;Getting that in the right order every time looks tricky, though,
so we should probably do something about it.
Just like &lt;a class="external" href="https://docs.python.org/3/library/asyncio-runner.html#asyncio.Runner"&gt;asyncio.Runner&lt;/a&gt;,
we'll provide a higher-level API,
and since it already does similar work,
we get to reuse its API design for free:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;
&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;
&lt;span class="normal"&gt;52&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;ThreadRunner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_runner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__enter__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_lazy_init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__exit__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exc_tb&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_soon_threadsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_lazy_init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_coroutine_threadsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coro&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_lazy_init&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_thread&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;loop_created&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Event&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_runner&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_event_loop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;loop_created&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
                &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;threading&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;run_forever&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;LoopThread&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;daemon&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;loop_created&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This means using it looks &lt;a class="anchor" href="#one-thread"&gt;just the same&lt;/a&gt;,
which is pretty neat:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;77&lt;/span&gt;
&lt;span class="normal"&gt;78&lt;/span&gt;
&lt;span class="normal"&gt;79&lt;/span&gt;
&lt;span class="normal"&gt;80&lt;/span&gt;
&lt;span class="normal"&gt;81&lt;/span&gt;
&lt;span class="normal"&gt;82&lt;/span&gt;
&lt;span class="normal"&gt;83&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ThreadRunner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__aenter__&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;do_stuff_in_threads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__aexit__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;




&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        Liking this so far? Here&amp;#39;s another article you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/limit-concurrency"&gt;
            Limiting concurrency in Python asyncio: the story of async imap_unordered()
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;!-- # Context managers --&gt;

&lt;h2 id="but-i-don-t-want-to-manage-async-context-managers-by-hand"&gt;But I don't want to manage async context managers by hand&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-don-t-want-to-manage-async-context-managers-by-hand" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;If I'm being honest, this whole
&amp;quot;simulate &lt;code&gt;async with&lt;/code&gt; by calling the protocol methods&amp;quot;
is getting kinda tedious.
But now we have a high-level API,
so we can extend it with a &lt;em&gt;sync&lt;/em&gt; context manager to take care of it for us.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;54&lt;/span&gt;
&lt;span class="normal"&gt;55&lt;/span&gt;
&lt;span class="normal"&gt;56&lt;/span&gt;
&lt;span class="normal"&gt;57&lt;/span&gt;
&lt;span class="normal"&gt;58&lt;/span&gt;
&lt;span class="normal"&gt;59&lt;/span&gt;
&lt;span class="normal"&gt;60&lt;/span&gt;
&lt;span class="normal"&gt;61&lt;/span&gt;
&lt;span class="normal"&gt;62&lt;/span&gt;
&lt;span class="normal"&gt;63&lt;/span&gt;
&lt;span class="normal"&gt;64&lt;/span&gt;
&lt;span class="normal"&gt;65&lt;/span&gt;
&lt;span class="normal"&gt;66&lt;/span&gt;
&lt;span class="normal"&gt;67&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="nd"&gt;@contextlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;contextmanager&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;wrap_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;cm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;call_async&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="n"&gt;aenter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__aenter__&lt;/span&gt;
        &lt;span class="n"&gt;aexit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__aexit__&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aenter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aexit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;exc_info&lt;/span&gt;&lt;span class="p"&gt;())):&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aexit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This is the same code as before,
except we're going &lt;a class="external" href="https://docs.python.org/3/reference/compound_stmts.html#async-with"&gt;by the book&lt;/a&gt; with what gets called when;
see Brett Cannon's &lt;a class="external" href="https://snarky.ca/unravelling-the-async-with-statement/"&gt;Unravelling the &lt;code&gt;async with&lt;/code&gt; statement&lt;/a&gt;
for details.&lt;/p&gt;
&lt;p&gt;You use it like so:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;92&lt;/span&gt;
&lt;span class="normal"&gt;93&lt;/span&gt;
&lt;span class="normal"&gt;94&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ThreadRunner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wrap_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;do_stuff_in_threads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;!-- ## Enter context --&gt;

&lt;p&gt;That's good, but we can do better.
Here, we don't really need granular control over the lifetime of the session,
we just want it to live as long as the event loop,
so it would be nice to give users a shorthand for that.&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;
We can use an &lt;a class="external" href="https://docs.python.org/3/library/contextlib.html#contextlib.ExitStack"&gt;ExitStack&lt;/a&gt; to enter context managers
on the user's behalf and exit them when the runner is closed.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_runner&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Runner&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_thread&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_stack&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;contextlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ExitStack&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;70&lt;/span&gt;
&lt;span class="normal"&gt;71&lt;/span&gt;
&lt;span class="normal"&gt;72&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;enter_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;cm&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wrap_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_stack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enter_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_stack&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;close&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;finally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;loop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_loop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;call_soon_threadsafe&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_thread&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Now, we just need to do:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;100&lt;/span&gt;
&lt;span class="normal"&gt;101&lt;/span&gt;
&lt;span class="normal"&gt;102&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ThreadRunner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enter_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;do_stuff_in_threads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;!-- # Iterables --&gt;

&lt;h2 id="but-i-want-to-use-async-iterables"&gt;But I want to use async iterables&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-want-to-use-async-iterables" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;One final thing:
how would you consume an &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-asynchronous-iterable"&gt;asynchronous iterable&lt;/a&gt;
like the &lt;a class="external" href="https://docs.python.org/3/library/asyncio-stream.html#asyncio.StreamReader"&gt;content of an aiohttp response&lt;/a&gt;?
In async code, it would look something like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://death.andgravity.com/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;got&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;lines&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Like &lt;code&gt;async with&lt;/code&gt;,
&lt;code&gt;async for&lt;/code&gt; has its own &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-asynchronous-iterator"&gt;protocol&lt;/a&gt; we can wrap;
for slightly better error messages,
we'll use the &lt;a class="external" href="https://docs.python.org/3/library/functions.html#aiter"&gt;aiter()&lt;/a&gt; and &lt;a class="external" href="https://docs.python.org/3/library/functions.html#anext"&gt;anext()&lt;/a&gt; built-ins
instead of calling the corresponding &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-special-method"&gt;special methods&lt;/a&gt; directly:&lt;/p&gt;
&lt;!--
.. literalinclude:: 50-iter.py
    :lines: 106-
--&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;77&lt;/span&gt;
&lt;span class="normal"&gt;78&lt;/span&gt;
&lt;span class="normal"&gt;79&lt;/span&gt;
&lt;span class="normal"&gt;80&lt;/span&gt;
&lt;span class="normal"&gt;81&lt;/span&gt;
&lt;span class="normal"&gt;82&lt;/span&gt;
&lt;span class="normal"&gt;83&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;wrap_iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;aiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;anext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;StopAsyncIteration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;You use it like so:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;108&lt;/span&gt;
&lt;span class="normal"&gt;109&lt;/span&gt;
&lt;span class="normal"&gt;110&lt;/span&gt;
&lt;span class="normal"&gt;111&lt;/span&gt;
&lt;span class="normal"&gt;112&lt;/span&gt;
&lt;span class="normal"&gt;113&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;ThreadRunner&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;session&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enter_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;aiohttp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClientSession&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;coroutine&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;https://death.andgravity.com/&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wrap_context&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coroutine&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;lines&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;runner&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wrap_iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;got&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;lines&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;brigde.py
&lt;span class="go"&gt;got 624 lines&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And with this, we're done!&lt;/p&gt;
&lt;p&gt;Here's &lt;a class="attachment" href="/_file/asyncio-bridge/51-wrap-iter.py"&gt;the final version of the code&lt;/a&gt;.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Update (August 2025)&lt;/p&gt;
&lt;p&gt;...or even better,
install it from &lt;a class="external" href="https://pypi.org/project/asyncio-thread-runner"&gt;PyPI&lt;/a&gt;,
and check out the
documented, tested, type-annotated version
on &lt;a class="external" href="https://github.com/lemon24/asyncio-thread-runner"&gt;GitHub&lt;/a&gt;! &lt;a class="external" href="https://github.com/lemon24/asyncio-thread-runner"&gt;⭐️&lt;/a&gt;&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="so-should-you-do-this"&gt;So, should you do this?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#so-should-you-do-this" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I guess?&lt;/p&gt;
&lt;p&gt;I used something like this on a &amp;quot;production&amp;quot; project without any problems.&lt;/p&gt;
&lt;p&gt;Performance might be an issue if you &lt;code&gt;run()&lt;/code&gt; many small tasks,
but the overhead for bigger tasks should be negligible,
especially compared with the cost of doing I/O
(for example, returning an entire HTTP response is likely fine,
wrapping an iterator of lines might not be).&lt;/p&gt;
&lt;p&gt;In my case,
I was forced to use an async library that didn't have a sync counterpart,
and the benefit of retaining a mostly sync code base was overwhelming.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/asyncio-bridge&amp;t=Running%20async%20code%20from%20sync%20code%20in%20Python"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Running%20async%20code%20from%20sync%20code%20in%20Python%20https%3A//death.andgravity.com/asyncio-bridge"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/asyncio-bridge&amp;title=Running%20async%20code%20from%20sync%20code%20in%20Python"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/asyncio-bridge"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Running%20async%20code%20from%20sync%20code%20in%20Python&amp;url=https%3A//death.andgravity.com/asyncio-bridge&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;





&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        If you&amp;#39;ve made it this far, you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/query-builder-how"&gt;
            Write an SQL query builder in 150 lines of Python!
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;Which itself comes with another warning:
&lt;em&gt;to handle signals and to execute subprocesses,
the event loop must be run in the main thread&lt;/em&gt;;
this is a sacrifice we're willing to make. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;It could be argued that it is not ThreadRunner's job to do this,
but remember it exists to make it easy to use async code from sync code,
and this use case is quite common;
also, &lt;a class="external" href="https://click.palletsprojects.com/en/latest/advanced/#managing-resources"&gt;there is precedent for this kind of API&lt;/a&gt;,
and &lt;em&gt;&lt;a class="internal" href="/output#good-libraries-educate"&gt;something something copy, something something steal&lt;/a&gt;&lt;/em&gt;. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/asyncio-bridge" rel="alternate"/>
    <summary>So, you're doing some sync stuff. But you also need to do some async stuff, without making *everything* async. Hint: asyncio.Runner will get you at least part of the way there.</summary>
    <published>2023-07-29T18:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/reader-3-7">
    <id>https://death.andgravity.com/reader-3-7</id>
    <title>reader 3.7 released – contributor docs</title>
    <updated>2023-07-15T18:00:00+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;I'm happy to announce version 3.7 of &lt;strong&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/strong&gt;, a Python feed reader library.&lt;/p&gt;
&lt;p&gt;More importantly, &lt;em&gt;reader&lt;/em&gt; has reached 300 stars on GitHub!&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-3-7/stars.png" alt="lemon24/reader stars" /&gt;&lt;/p&gt;
&lt;h2 id="what-s-new"&gt;What's new?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-new" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Here are the highlights since &lt;a class="internal" href="/reader-3-4"&gt;reader 3.4&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="contributor-documentation"&gt;Contributor documentation&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#contributor-documentation" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; now has &lt;strong&gt;&lt;a class="external" href="https://reader.readthedocs.io/en/latest/contributing.html"&gt;contributor documentation&lt;/a&gt;&lt;/strong&gt;!
If you want to help
or you're just curious where it's all going,
check out the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/dev.html#roadmap"&gt;roadmap&lt;/a&gt; and &lt;a class="external" href="https://reader.readthedocs.io/en/latest/why.html#philosophy"&gt;the &lt;em&gt;reader&lt;/em&gt; philosophy&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Thank you to &lt;a class="external" href="https://kjamistan.com/"&gt;Katharine Jarmul&lt;/a&gt;
for getting me to do this!
(it only took me &lt;a class="internal" href="/reader-3-4#5-years-2000-commits"&gt;five years&lt;/a&gt;...)&lt;/p&gt;
&lt;h3 id="explicitly-unimportant"&gt;Explicitly unimportant&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#explicitly-unimportant" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;a class="external" href="https://reader.readthedocs.io/en/latest/api.html#reader.Entry.important"&gt;important&lt;/a&gt; &lt;a class="external" href="https://reader.readthedocs.io/en/latest/guide.html#entry-flags"&gt;entry flag&lt;/a&gt; is now &lt;code&gt;bool|None&lt;/code&gt; instead of &lt;code&gt;bool&lt;/code&gt;,&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;
so there's a sane way to express &amp;quot;I don't care about this article&amp;quot;.&lt;/p&gt;
&lt;p&gt;The point of marking an article as explicitly unimportant is to give you
&lt;a class="internal" href="/reader-2-5#statistics"&gt;a better understanding of how you consume content&lt;/a&gt;
– if you consistently don't care about the articles in a feed,
maybe it's time to &lt;a class="external" href="https://blog.ncase.me/back-to-the-future-with-rss/#tips"&gt;ruthlessly Marie Kondo that crap&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="discontinued-plugins"&gt;Discontinued plugins&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#discontinued-plugins" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;I removed the &lt;a class="internal" href="/reader-2-14#twitter-support"&gt;Twitter plugin&lt;/a&gt;,
since it's not possible to get tweets using the free API tier anymore,
rendering it effectively useless.
While at it, I also removed the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/plugins.html#tumblr-gdpr"&gt;Tumblr GDPR plugin&lt;/a&gt;
(not needed since August 2020).&lt;/p&gt;
&lt;p&gt;Nevertheless,
they are valuable examples of how to implement &lt;em&gt;reader&lt;/em&gt; plugins,
so the docs still mention them in &lt;a class="external" href="https://reader.readthedocs.io/en/latest/plugins.html#discontinued-plugins"&gt;Discontinued plugins&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="python-versions"&gt;Python versions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#python-versions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;With PyPy 3.10 added in &lt;em&gt;reader&lt;/em&gt; 3.7,
this is the last release to support Python 3.9.&lt;/p&gt;
&lt;p&gt;For more details, see the full &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-3-7"&gt;changelog&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/reader-3-7&amp;t=reader%203.7%20released%20%E2%80%93%20contributor%20docs"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=reader%203.7%20released%20%E2%80%93%20contributor%20docs%20https%3A//death.andgravity.com/reader-3-7"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/reader-3-7&amp;title=reader%203.7%20released%20%E2%80%93%20contributor%20docs"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/reader-3-7"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=reader%203.7%20released%20%E2%80%93%20contributor%20docs&amp;url=https%3A//death.andgravity.com/reader-3-7&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;h2 id="what-is-reader"&gt;What is &lt;em&gt;reader&lt;/em&gt;?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;reader&lt;/strong&gt; takes care
of the core functionality required by a feed reader,
so you can focus on what makes &lt;strong&gt;yours&lt;/strong&gt; different.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-2-0/reader.png" alt="reader in action" /&gt;
&lt;em&gt;reader&lt;/em&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retrieve, store, and manage &lt;strong&gt;Atom&lt;/strong&gt;, &lt;strong&gt;RSS&lt;/strong&gt;, and &lt;strong&gt;JSON&lt;/strong&gt; feeds&lt;/li&gt;
&lt;li&gt;mark articles as read or important&lt;/li&gt;
&lt;li&gt;add arbitrary tags/metadata to feeds and articles&lt;/li&gt;
&lt;li&gt;filter feeds and articles&lt;/li&gt;
&lt;li&gt;full-text search articles&lt;/li&gt;
&lt;li&gt;get statistics on feed and user activity&lt;/li&gt;
&lt;li&gt;write plugins to extend its functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...all these with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stable, clearly documented API&lt;/li&gt;
&lt;li&gt;excellent test coverage&lt;/li&gt;
&lt;li&gt;fully typed Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find out more, check out the &lt;a class="external" href="https://github.com/lemon24/reader"&gt;GitHub repo&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/"&gt;docs&lt;/a&gt;,
or give the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/tutorial.html"&gt;tutorial&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2 id="why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-use-a-feed-reader-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Have you been unhappy with existing feed readers and wanted to make your own, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never knew where to start?&lt;/li&gt;
&lt;li&gt;it seemed like too much work?&lt;/li&gt;
&lt;li&gt;you don't like writing backend code?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you already working with &lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/"&gt;feedparser&lt;/a&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;want an easier way to store, filter, sort and search feeds and entries?&lt;/li&gt;
&lt;li&gt;want to get back type-annotated objects instead of dicts?&lt;/li&gt;
&lt;li&gt;want to restrict or deny file-system access?&lt;/li&gt;
&lt;li&gt;want to change the way feeds are retrieved by using &lt;a class="external" href="https://requests.readthedocs.io"&gt;Requests&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to also support &lt;a class="external" href="https://jsonfeed.org/"&gt;JSON Feed&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to support custom information sources?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... while still supporting all the feed types feedparser does?&lt;/p&gt;
&lt;p&gt;If you answered yes to any of the above, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;h2 id="the-reader-philosophy"&gt;The &lt;em&gt;reader&lt;/em&gt; philosophy&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-reader-philosophy" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is a library&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is for the long term&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is extensible&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is stable (within reason)&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is simple to use; API matters&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; features work well together&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is tested&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; is documented&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; has minimal dependencies&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="why-make-your-own-feed-reader"&gt;Why make your own feed reader?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-make-your-own-feed-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;have full control over your data&lt;/li&gt;
&lt;li&gt;control what features it has or doesn't have&lt;/li&gt;
&lt;li&gt;decide how much you pay for it&lt;/li&gt;
&lt;li&gt;make sure it doesn't get closed while you're still using it&lt;/li&gt;
&lt;li&gt;really, it's &lt;a class="external" href="https://rachelbythebay.com/w/2011/10/26/fred/"&gt;easier than you think&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously, this may not be your cup of tea, but if it is, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;By the way, I'm quite pleased
with how I managed to do it
in an (&lt;a class="external" href="https://reader.readthedocs.io/en/latest/changelog.html#version-3-5"&gt;almost&lt;/a&gt;) &lt;a class="external" href="https://reader.readthedocs.io/en/latest/api.html#reader.types.TristateFilterInput"&gt;backwards-compatible way&lt;/a&gt;. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/reader-3-7" rel="alternate"/>
    <published>2023-07-15T18:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/output">
    <id>https://death.andgravity.com/output</id>
    <title>Why you should still read the docs</title>
    <updated>2023-05-16T18:00:00+00:00</updated>
    <content type="html">&lt;p&gt;Do you feel you're &lt;strong&gt;fighting your tools&lt;/strong&gt;?&lt;/p&gt;
&lt;p&gt;Do you feel you're &lt;strong&gt;relying too much on autocomplete&lt;/strong&gt; and inline documentation?
...always kinda guessing when using libraries?&lt;/p&gt;
&lt;p&gt;Or maybe not, but getting things done &lt;strong&gt;just seems harder than it should be&lt;/strong&gt;.&lt;/p&gt;
&lt;!-- should this say "advanced beginners"? --&gt;

&lt;p&gt;This can have many causes,
but I've repeatedly seen junior developers struggle with a specific one,
not even fully aware they're struggling in the first place.&lt;/p&gt;
&lt;!-- As always, there must be a better way. --&gt;

&lt;p&gt;This is a story about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;why you should still read the docs&lt;/li&gt;
&lt;li&gt;finding the right way of doing things&lt;/li&gt;
&lt;li&gt;command-line interfaces&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;tl;dr: &lt;strong&gt;Most &lt;em&gt;good&lt;/em&gt; documentation won't show up in your IDE&lt;/strong&gt; –
rather, it is about &lt;em&gt;how&lt;/em&gt; to use the library,
and the &lt;em&gt;problem&lt;/em&gt; the library is solving.&lt;/p&gt;
&lt;details class="toc" open&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#an-example"&gt;An example&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#the-path-is-hardcoded"&gt;The path is hardcoded&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-path-might-not-be-valid"&gt;The path might not be valid&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-path-to-what-exactly"&gt;The path to what, exactly?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#standard-output"&gt;Standard output&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#by-default"&gt;...by default&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#discussion"&gt;Discussion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#good-libraries-educate"&gt;Good libraries educate&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#read-the-docs"&gt;Read the docs&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#but-the-docs-suck"&gt;But the docs suck&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#but-i-m-using-lots-of-libraries"&gt;But I'm using lots of libraries&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#conclusion"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="an-example"&gt;An example&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#an-example" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;OK, instead of telling you, let me show you –
we'll go through &lt;em&gt;one possible path&lt;/em&gt; of developing something,
and stop along the way to point out how we're doing things.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Say you're writing a command-line tool
that gets data from an API
and saves it in a CSV file.
It might look something like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="IPython"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;csv&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;click&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="c1"&gt;# pretend some API calls happen here&lt;/span&gt;
    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Chaos&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;short_name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Chs&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Discord&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;short_name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Dsc&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Confusion&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;short_name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Cfn&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Bureaucracy&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;short_name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Bcy&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
        &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;The Aftermath&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;short_name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;Afm&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;write_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;calendar.csv&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;w&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;short_name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;short_name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;

&lt;span class="nd"&gt;@click&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Retrieve the names of the Discordian seasons to calendar.csv.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;write_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;__main__&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here it is in action:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;seasons.py&lt;span class="w"&gt; &lt;/span&gt;--help
&lt;span class="go"&gt;Usage: seasons.py [OPTIONS]&lt;/span&gt;

&lt;span class="go"&gt;  Retrieve the names of the Discordian seasons to calendar.csv.&lt;/span&gt;

&lt;span class="go"&gt;Options:&lt;/span&gt;
&lt;span class="go"&gt;  --help  Show this message and exit.&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;seasons.py
&lt;span class="gp"&gt;$ &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;calendar.csv
&lt;span class="go"&gt;name,short_name&lt;/span&gt;
&lt;span class="go"&gt;Chaos,Chs&lt;/span&gt;
&lt;span class="go"&gt;Discord,Dsc&lt;/span&gt;
&lt;span class="go"&gt;Confusion,Cfn&lt;/span&gt;
&lt;span class="go"&gt;Bureaucracy,Bcy&lt;/span&gt;
&lt;span class="go"&gt;The Aftermath,Afm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You're using &lt;a class="external" href="https://click.palletsprojects.com/"&gt;Click&lt;/a&gt; because
it’s powerful but comes with sensible defaults.&lt;/p&gt;
&lt;p&gt;This is pretty good code –
instead of calling &lt;code&gt;get_data()&lt;/code&gt; &lt;em&gt;from&lt;/em&gt; &lt;code&gt;write_csv()&lt;/code&gt;,
you're passing its result &lt;em&gt;to&lt;/em&gt; &lt;code&gt;write_csv()&lt;/code&gt; –
in other words, instead of &lt;em&gt;hiding&lt;/em&gt; the input,
you've &lt;em&gt;decoupled&lt;/em&gt; it.&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;As a result,
&lt;code&gt;write_csv()&lt;/code&gt; doesn't need to change when adding new arguments to &lt;code&gt;get_data()&lt;/code&gt;,
and we can test it without monkeypatching &lt;code&gt;get_data()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="IPython"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;seasons&lt;/span&gt;

&lt;span class="n"&gt;DATA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;one&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;short_name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;two&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;short_name&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;2&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;CSV_BYTES&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;name,short_name&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s2"&gt;one,1&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s2"&gt;two,2&lt;/span&gt;&lt;span class="se"&gt;\r\n&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_write_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;monkeypatch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;chdir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;seasons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DATA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;joinpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;calendar.csv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;CSV_BYTES&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;!--

# Too much monkey patching

Hmm... that's a bit involved.

Think about it – get_data() is *hidden* away,
but is still *coupled* to the things around it;
when get_data() needs a new argument:

* write_csv() will need to pass the argument through,
  even though it only cares about get_data()'s output
* we'll have to update the test to check the argument is passed around

.. literalinclude:: 10-data/seasons.py
    :lines: 23-27
    :linenos: no

.. literalinclude:: 10-data/seasons.py
    :lines: 16-17
    :linenos: no

The test is already simpler now:

.. literalinclude:: 10-data/test_seasons.py
    :lines: 9-
    :linenos: no

--&gt;

&lt;h3 id="the-path-is-hardcoded"&gt;The path is hardcoded&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-path-is-hardcoded" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;But, what if you need to run it twice,
and save the output in different files?
This becomes useful as soon as the output can change.
Also, having to be in the right directory
when running it is not cool.&lt;/p&gt;
&lt;p&gt;The obvious solution is to pass the path in:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@click&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nd"&gt;@click&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--path&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Retrieve the names of the Discordian seasons.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;write_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;write_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;w&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;We're definitely on the right track here –
the test is shorter,
and we don't even need to monkeypatch the current working directory
anymore:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_write_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;out_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tmp_path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;joinpath&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;out.csv&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;seasons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DATA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;out_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;out_path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read_bytes&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;CSV_BYTES&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="the-path-might-not-be-valid"&gt;The path might not be valid&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-path-might-not-be-valid" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;But, what if you pass a directory as the path?&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;seasons.py&lt;span class="w"&gt; &lt;/span&gt;--path&lt;span class="w"&gt; &lt;/span&gt;.
&lt;span class="go"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="go"&gt;  ...&lt;/span&gt;
&lt;span class="go"&gt;IsADirectoryError: [Errno 21] Is a directory: &amp;#39;.&amp;#39;&lt;/span&gt;
&lt;span class="go"&gt;python seasons.py --path .  0.07s user 0.02s system 4% cpu 2.104 total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;OK, it fails with a traceback.
Worse, we waited for the API calls
only to throw the output away.
Thankfully, Click has fancy &lt;a class="external" href="https://click.palletsprojects.com/en/latest/parameters/#parameter-types"&gt;parameter types&lt;/a&gt; that can take care of that:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@click&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nd"&gt;@click&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;--path&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;click&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exists&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dir_okay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;writable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;seasons.py&lt;span class="w"&gt; &lt;/span&gt;--path&lt;span class="w"&gt; &lt;/span&gt;.
&lt;span class="go"&gt;Usage: seasons.py [OPTIONS]&lt;/span&gt;
&lt;span class="go"&gt;Try &amp;#39;seasons.py --help&amp;#39; for help.&lt;/span&gt;

&lt;span class="go"&gt;Error: Invalid value for &amp;#39;--path&amp;#39;: File &amp;#39;.&amp;#39; is a directory.&lt;/span&gt;
&lt;span class="go"&gt;python seasons.py --path .  0.08s user 0.02s system 92% cpu 0.112 total&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Not only do we get a nice error message,
we get it instantly!&lt;/p&gt;
&lt;h3 id="the-path-to-what-exactly"&gt;The path to what, exactly?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-path-to-what-exactly" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Thing is, &lt;code&gt;--path&lt;/code&gt; isn't very descriptive.
I wonder if there's a better name for it...&lt;/p&gt;
&lt;p&gt;One thing I like to do is to look at what others are doing.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;curl&lt;span class="w"&gt; &lt;/span&gt;--help
&lt;span class="go"&gt;Usage: curl [options...] &amp;lt;url&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt; -o, --output &amp;lt;file&amp;gt; Write to file instead of stdout&lt;/span&gt;
&lt;span class="go"&gt; -O, --remote-name   Write output to a file named as the remote file&lt;/span&gt;
&lt;span class="go"&gt; ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;wget&lt;span class="w"&gt; &lt;/span&gt;--help
&lt;span class="go"&gt;GNU Wget 1.21.2, a non-interactive network retriever.&lt;/span&gt;
&lt;span class="go"&gt;Usage: wget [OPTION]... [URL]...&lt;/span&gt;
&lt;span class="go"&gt;  -o,  --output-file=FILE          log messages to FILE&lt;/span&gt;
&lt;span class="go"&gt;  -O,  --output-document=FILE      write documents to FILE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;sort&lt;span class="w"&gt; &lt;/span&gt;--help
&lt;span class="go"&gt;Usage: sort [OPTION]... [FILE]...&lt;/span&gt;
&lt;span class="go"&gt;  -o, --output=FILE         write result to FILE instead of standard output&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;pandoc&lt;span class="w"&gt; &lt;/span&gt;--help
&lt;span class="go"&gt;pandoc [OPTIONS] [FILES]&lt;/span&gt;
&lt;span class="go"&gt;  -o FILE               --output=FILE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Better yet, sometimes there's a comprehensive guide on a topic,
like &lt;a class="external" href="https://clig.dev/"&gt;Command Line Interface Guidelines&lt;/a&gt;
...and under &lt;a class="external" href="https://clig.dev/#arguments-and-flags"&gt;Arguments and flags&lt;/a&gt;,
we find this:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Use standard names for flags, if there is a standard.&lt;/strong&gt; If another commonly used command uses a flag name, it’s best to follow that existing pattern. That way, a user doesn’t have to remember two different options (and which command it applies to), and users can even guess an option without having to look at the help text.&lt;/p&gt;
&lt;p&gt;Here’s a list of commonly used options: [...]&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;-o&lt;/code&gt;, &lt;code&gt;--output&lt;/code&gt;: Output file. For example, &lt;code&gt;sort&lt;/code&gt;, &lt;code&gt;gcc&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/blockquote&gt;
&lt;h3 id="standard-output"&gt;Standard output&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#standard-output" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Further down below, there's another guideline:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If input or output is a file, support &lt;code&gt;-&lt;/code&gt; to read from &lt;code&gt;stdin&lt;/code&gt; or write to &lt;code&gt;stdout&lt;/code&gt;.&lt;/strong&gt; This lets the output of another command be the input of your command and vice versa, without using a temporary file. [...]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;I wonder how we could achieve this.&lt;/p&gt;
&lt;p&gt;One way would be to check the value and use &lt;a class="external" href="https://docs.python.org/3/library/sys.html#sys.stdout"&gt;sys.stdout&lt;/a&gt; if it's &lt;code&gt;-&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;But let's pause and look at the &lt;a class="external" href="https://click.palletsprojects.com/en/latest/api/#click.Path"&gt;Path&lt;/a&gt; docs a bit:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;allow_dash (bool) – Allow a single dash as a value, which indicates a standard stream (but does not open it). Use &lt;a class="external" href="https://click.palletsprojects.com/en/latest/api/#click.open_file"&gt;open_file()&lt;/a&gt; to handle opening this value.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Encouraging; following along, &lt;a class="external" href="https://click.palletsprojects.com/en/latest/api/#click.open_file"&gt;open_file()&lt;/a&gt; says:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Open a file, with extra behavior to handle &lt;code&gt;'-'&lt;/code&gt; to indicate a standard stream, lazy open on write, and atomic write. Similar to the behavior of the &lt;a class="external" href="https://click.palletsprojects.com/en/latest/api/#click.File"&gt;File&lt;/a&gt; param type.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;What's this File, eh?&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Declares a parameter to be a file for reading or writing. [...] The special value &lt;code&gt;-&lt;/code&gt; indicates stdin or stdout depending on the mode. [...]&lt;/p&gt;
&lt;p&gt;See &lt;a class="external" href="https://click.palletsprojects.com/en/latest/arguments/#file-args"&gt;File Arguments&lt;/a&gt; for more information.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Which finally takes us away from the API reference to the actual &lt;em&gt;documentation&lt;/em&gt;:&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Since all the examples have already worked with filenames, it makes sense to explain how to deal with files properly. Command line tools are more fun if they work with files the Unix way, which is to accept &lt;code&gt;-&lt;/code&gt; as a special file that refers to stdin/stdout.&lt;/p&gt;
&lt;p&gt;Click supports this through the &lt;a class="external" href="https://click.palletsprojects.com/en/latest/api/#click.File"&gt;click.File&lt;/a&gt; type which intelligently handles files for you. [...]&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;OK, OK, I get it, let's use it:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@click&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;command&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="nd"&gt;@click&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-o&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;--output&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;click&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;w&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lazy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;required&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Retrieve the names of the Discordian seasons.&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_data&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="c1"&gt;# click.File doesn&amp;#39;t have a newline argument&lt;/span&gt;
    &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reconfigure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;write_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With passing a file object to &lt;code&gt;write_csv()&lt;/code&gt;,
the &lt;em&gt;output&lt;/em&gt; part of I/O is also decoupled:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;write_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;short_name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;writer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;writerow&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;short_name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Interestingly enough, &lt;a class="external" href="https://docs.python.org/3/library/csv.html#csv.writer"&gt;csv.writer&lt;/a&gt; &lt;em&gt;already&lt;/em&gt; takes an open file.&lt;/p&gt;
&lt;p&gt;Anyway, it works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;seasons.py&lt;span class="w"&gt; &lt;/span&gt;--output&lt;span class="w"&gt; &lt;/span&gt;-
&lt;span class="go"&gt;name,short_name&lt;/span&gt;
&lt;span class="go"&gt;Chaos,Chs&lt;/span&gt;
&lt;span class="go"&gt;Discord,Dsc&lt;/span&gt;
&lt;span class="go"&gt;Confusion,Cfn&lt;/span&gt;
&lt;span class="go"&gt;Bureaucracy,Bcy&lt;/span&gt;
&lt;span class="go"&gt;The Aftermath,Afm&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Once again, the test gets simpler too –
instead of writing anything to disk,
we can use an &lt;a class="external" href="https://docs.python.org/3/library/io.html#io.BytesIO"&gt;in-memory stream&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_write_csv&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;io&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;StringIO&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;seasons&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write_csv&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;DATA&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getvalue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;CSV_BYTES&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="by-default"&gt;...by default&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#by-default" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Some of the help texts &lt;a class="anchor" href="#the-path-to-what-exactly"&gt;we looked at&lt;/a&gt;
say &amp;quot;write to FILE instead of stdout&amp;quot;...&lt;/p&gt;
&lt;p&gt;Why &lt;em&gt;instead?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Using commands in a pipeline is common enough that
most Unix commands write to stdout &lt;em&gt;by default&lt;/em&gt;;
some don't even bother with an &lt;code&gt;--output&lt;/code&gt; option,
since you can always redirect stdout to a file:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;tail&lt;span class="w"&gt; &lt;/span&gt;-n+2&lt;span class="w"&gt; &lt;/span&gt;calendar.csv&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;head&lt;span class="w"&gt; &lt;/span&gt;-n2
&lt;span class="go"&gt;Chaos,Chs&lt;/span&gt;
&lt;span class="go"&gt;Discord,Dsc&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;tail&lt;span class="w"&gt; &lt;/span&gt;-n+2&lt;span class="w"&gt; &lt;/span&gt;calendar.csv&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;calendar-no-heading.csv
&lt;span class="gp"&gt;$ &lt;/span&gt;head&lt;span class="w"&gt; &lt;/span&gt;-n2&lt;span class="w"&gt; &lt;/span&gt;calendar-no-heading.csv
&lt;span class="go"&gt;Chaos,Chs&lt;/span&gt;
&lt;span class="go"&gt;Discord,Dsc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It is so common that &lt;a class="external" href="https://clig.dev/"&gt;CLI Guidelines&lt;/a&gt;
makes it the third thing in &lt;a class="external" href="https://clig.dev/#the-basics"&gt;The Basics&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Send output to &lt;code&gt;stdout&lt;/code&gt;.&lt;/strong&gt; The primary output for your command should go to &lt;code&gt;stdout&lt;/code&gt;. Anything that is machine readable should also go to &lt;code&gt;stdout&lt;/code&gt;—this is where piping sends things by default.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We could remove &lt;code&gt;--output&lt;/code&gt;, but with Click, doing both is trivially easy:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@click&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;option&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-o&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;--output&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;click&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;File&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;w&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;lazy&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;-&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;seasons.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tail&lt;span class="w"&gt; &lt;/span&gt;-n+2&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;head&lt;span class="w"&gt; &lt;/span&gt;-n2
&lt;span class="go"&gt;Chaos,Chs&lt;/span&gt;
&lt;span class="go"&gt;Discord,Dsc&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="discussion"&gt;Discussion&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#discussion" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Looking at our journey, I can't help but notice a few things:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Click &lt;a class="external" href="https://click.palletsprojects.com/en/latest/api/"&gt;API reference&lt;/a&gt; seems to constantly funnel readers
to the &lt;a class="external" href="https://click.palletsprojects.com/en/latest/#documentation"&gt;user guide&lt;/a&gt; part of the documentation,
the part that tells you &lt;em&gt;how&lt;/em&gt; and &lt;em&gt;why&lt;/em&gt; to use stuff.&lt;/li&gt;
&lt;li&gt;Click mentions supporting &lt;code&gt;-&lt;/code&gt; for stdout, and more than once.&lt;/li&gt;
&lt;li&gt;The Click user guide discusses &lt;a class="external" href="https://click.palletsprojects.com/en/latest/api/#click.File"&gt;File&lt;/a&gt; arguments &lt;em&gt;before&lt;/em&gt; &lt;a class="external" href="https://click.palletsprojects.com/en/latest/api/#click.Path"&gt;Path&lt;/a&gt;
(in part, because it's more commonly used,
but also because it's more useful out of the box).&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.python.org/3/library/csv.html#csv.writer"&gt;csv.writer&lt;/a&gt; takes an open file,
just like our own &lt;code&gt;write_csv()&lt;/code&gt; does at the end.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These all hint that it may be a good idea to structure code a certain way;
the more we use things the way they &amp;quot;want&amp;quot; to be used,
the cleaner the code and the tests get.&lt;/p&gt;
&lt;h2 id="good-libraries-educate"&gt;Good libraries educate&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#good-libraries-educate" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I am tempted to call &lt;a class="external" href="https://click.palletsprojects.com/"&gt;Click&lt;/a&gt; a &lt;em&gt;great library&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Yes, it does its job more than well, and has lots of features.&lt;/p&gt;
&lt;p&gt;But, more importantly, its documentation
educates the reader about the subject matter
– it goes beyond API docs into how to use it,
how to get specific things done with it,
and the underlying problem it is solving.&lt;/p&gt;
&lt;p&gt;Here are a few more examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Classics – other libraries in the &lt;a class="external" href="https://flask.palletsprojects.com"&gt;Flask&lt;/a&gt;/Pallets ecosystem
(like &lt;a class="external" href="https://werkzeug.palletsprojects.com"&gt;Werkzeug&lt;/a&gt; and &lt;a class="external" href="https://jinja.palletsprojects.com"&gt;Jinja&lt;/a&gt;), &lt;a class="external" href="https://requests.readthedocs.io"&gt;Requests&lt;/a&gt;, &lt;a class="external" href="https://docs.pytest.org"&gt;pytest&lt;/a&gt;, and many others –
popular &lt;em&gt;and&lt;/em&gt; old enough libraries usually have great documentation.&lt;/li&gt;
&lt;li&gt;Python itself – you would be amazed at how many developers
use Python every day without having gone through &lt;a class="external" href="https://docs.python.org/3/tutorial/"&gt;the tutorial&lt;/a&gt; even once.&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://feedparser.readthedocs.io"&gt;feedparser&lt;/a&gt; teaches you about
&lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/advanced.html"&gt;all kinds of Atom and RSS quirks&lt;/a&gt;,
&lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/character-encoding.html"&gt;encodings on the web&lt;/a&gt;,
&lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/http.html"&gt;HTTP&lt;/a&gt;,
and how to be nice to servers you're making requests to.&lt;ul&gt;
&lt;li&gt;feedparser is close to my heart because I'm using it
for my feed reader library, &lt;a class="external" href="https://reader.readthedocs.io/"&gt;reader&lt;/a&gt;,
where I too am doing my best to have &lt;a class="external" href="https://reader.readthedocs.io/en/latest/guide.html"&gt;docs worth reading&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Stepping away from Python a bit,
I've learned a lot from both &lt;a class="external" href="https://dagger.dev/dev-guide/"&gt;Dagger&lt;/a&gt; and &lt;a class="external" href="https://site.mockito.org/"&gt;Mockito&lt;/a&gt;
(and for the latter, the API docs for the
&lt;a class="external" href="https://javadoc.io/doc/org.mockito/mockito-core/latest/org/mockito/Mockito.html"&gt;&amp;quot;main&amp;quot; class&lt;/a&gt;
pack way more than just API,
and that &lt;em&gt;will&lt;/em&gt; show up in your IDE).&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://beancount.github.io/docs/"&gt;Beancount&lt;/a&gt;, a command-line accounting tool,
does a great job of teaching double-entry bookkeeping
(it taught me almost all I know about the topic).&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="read-the-docs"&gt;Read the docs&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#read-the-docs" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;In a way, this whole article is a ruse to tell you to &lt;a class="external" href="https://en.wikipedia.org/wiki/RTFM"&gt;RTFM&lt;/a&gt;,
but with a bit of nuance.&lt;/p&gt;
&lt;p&gt;Read the documentation to find out &lt;em&gt;how&lt;/em&gt;,
and more importantly, &lt;em&gt;why&lt;/em&gt; –
a mental model of the thing you're using
will make you more productive,
and will improve any guesses you make.&lt;/p&gt;
&lt;p&gt;Aside from the user guide part,
I also like to skim the reference for libraries I use a lot;
this gives me an idea of what's available,
and often I remember later that
&amp;quot;hey, there was something in the docs about X&amp;quot;.&lt;/p&gt;
&lt;h3 id="but-the-docs-suck"&gt;But the docs suck&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-the-docs-suck" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Sometimes, documentation sucks or is missing entirely
(doubly so for internal code).
It's easy to get a habit of not bothering to look,
when most of the time it's not there.&lt;/p&gt;
&lt;p&gt;Still, always check. Worst case, you'll have wasted a minute.&lt;/p&gt;
&lt;p&gt;On the other hand, docs should definitely be
one of the criteria when picking a library.&lt;/p&gt;
&lt;h3 id="but-i-m-using-lots-of-libraries"&gt;But I'm using lots of libraries&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-m-using-lots-of-libraries" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Quoting a friend:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Well, for you, it might be &amp;quot;a library&amp;quot;,
but for them it's 10 new libraries,
and a 2-day deadline.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Like with most things, the &lt;a class="external" href="https://en.wikipedia.org/wiki/Pareto_principle"&gt;Pareto principle&lt;/a&gt; applies:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Read the docs for big stuff you use often –
language, standard library, test framework,
web framework or database of choice,
and so on;
these have an outsized return on investment,
and start paying out quite quickly.&lt;/li&gt;
&lt;li&gt;Don't read &lt;em&gt;all&lt;/em&gt; the docs; skimming is often enough.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the other hand, there is value in not using &lt;em&gt;that&lt;/em&gt; many libraries.&lt;/p&gt;
&lt;!--

## But reading the docs is hard

It's true, it is is extra work,
and it does slow you down in the short term.

But it's a skill like any other –
the more you do it, the easier it becomes.

--&gt;

&lt;h2 id="conclusion"&gt;Conclusion&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#conclusion" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;To sum up:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Good libraries educate on the subject matter.&lt;/li&gt;
&lt;li&gt;Read the documentation of the things you are using a lot.&lt;/li&gt;
&lt;li&gt;If you feel you're fighting your tools,
maybe you're using them wrong (read the docs!),
but maybe they're not all that good;
it's OK to look for better ones.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I'll end with a related idea from a post called &lt;a class="external" href="https://blog.ircmaxell.com/2011/07/why-i-dont-use-autocomplete.html"&gt;Why I Don’t Use Autocomplete&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;But be sure that if you do use autocomplete that you’re not leaning on it as a crutch. Be sure that you’re not deciding what to write as you write it [...] and not just using a method because the IDE tells you it’s available.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/output&amp;t=Why%20you%20should%20still%20read%20the%20docs"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Why%20you%20should%20still%20read%20the%20docs%20https%3A//death.andgravity.com/output"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/output&amp;title=Why%20you%20should%20still%20read%20the%20docs"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/output"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Why%20you%20should%20still%20read%20the%20docs&amp;url=https%3A//death.andgravity.com/output&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;For a detailed example about decoupling I/O,
how it affects affects testing,
plus interesting historical background,
check out Brandon Rhodes' &lt;em&gt;great&lt;/em&gt; talk
&lt;a class="external" href="https://rhodesmill.org/brandon/talks/#clean-architecture-python"&gt;The Clean Architecture in Python&lt;/a&gt;. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;Is it not written, &amp;quot;A few hours of trial and error can save you five minutes of reading the docs&amp;quot;? &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/output" rel="alternate"/>
    <summary>Do you feel you're fighting your tools? Do you feel you're relying too much on autocomplete and inline documentation? tl;dr: Most good documentation won't show up in your IDE – rather, it is about how to use the library, and the problem the library is solving.</summary>
    <published>2023-05-16T10:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/linesieve">
    <id>https://death.andgravity.com/linesieve</id>
    <title>Announcing linesieve: an unholy blend of grep, sed, awk, and Python, born out of spite</title>
    <updated>2023-04-26T16:00:00+00:00</updated>
    <content type="html">&lt;!-- *This is my text munging tool. There are many like it, but this one is mine.* --&gt;

&lt;p&gt;Java is notoriously verbose,
especially when used in a serious Enterprise Project™.&lt;/p&gt;
&lt;p&gt;...so naturally, I made &lt;a class="external" href="https://github.com/lemon24/linesieve"&gt;linesieve&lt;/a&gt;,
a Python text munging tool to split output into sections
and match/sub/split with the full power of Python's &lt;a class="external" href="https://docs.python.org/3/library/re.html"&gt;re&lt;/a&gt; module.&lt;/p&gt;
&lt;p&gt;Here's an example of using it on a file listing
(delay added to make it look cool):&lt;/p&gt;
&lt;figure class="figure"&gt;
&lt;img class="img-responsive" src="/_file/linesieve/lscolors.gif" alt="ls -1 /* | linesieve -s '.*:' show bin match ^d head -n2 output" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
&lt;code&gt;ls -1 /* | linesieve -s '.*:' show bin match ^d head -n2&lt;/code&gt; output
&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;!--
&lt;details&gt;
&lt;summary&gt;(click for text output)&lt;/summary&gt;

```console
$ ls -1 /* | linesieve -s '.*:' show bin match ^d head -n2
.....
/bin:
dash
date
......
/sbin:
disklabel
dmesg
...
```

&lt;/details&gt;
--&gt;

&lt;p&gt;You can find short examples in the &lt;a class="external" href="https://linesieve.readthedocs.io/en/latest/cli.html"&gt;reference&lt;/a&gt;,
and an advanced example &lt;a class="anchor" href="#example"&gt;below&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="features"&gt;Features&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#features" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="external" href="https://github.com/lemon24/linesieve"&gt;linesieve&lt;/a&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;split text input into sections&lt;/li&gt;
&lt;li&gt;apply filters to specific sections&lt;/li&gt;
&lt;li&gt;search and highlight success/failure markers&lt;/li&gt;
&lt;li&gt;match/sub/split with the full power of Python's &lt;a class="external" href="https://docs.python.org/3/library/re.html"&gt;re&lt;/a&gt; module&lt;/li&gt;
&lt;li&gt;shorten paths, links and module names&lt;/li&gt;
&lt;li&gt;chain filters into pipelines&lt;/li&gt;
&lt;li&gt;colors!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Here's a list of filters available in 1.0:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://linesieve.readthedocs.io/en/latest/cli.html#linesieve-head"&gt;head&lt;/a&gt;
– Output the first part of sections.&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://linesieve.readthedocs.io/en/latest/cli.html#linesieve-tail"&gt;tail&lt;/a&gt;
– Output the last part of sections.&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://linesieve.readthedocs.io/en/latest/cli.html#linesieve-span"&gt;span&lt;/a&gt;
– Output matching line spans.&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://linesieve.readthedocs.io/en/latest/cli.html#linesieve-match"&gt;match&lt;/a&gt;
– Search for pattern (think &lt;code&gt;grep&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://linesieve.readthedocs.io/en/latest/cli.html#linesieve-split"&gt;split&lt;/a&gt;
– Output selected parts of lines (think &lt;code&gt;cut&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://linesieve.readthedocs.io/en/latest/cli.html#linesieve-sub"&gt;sub&lt;/a&gt;
– Replace pattern (think &lt;code&gt;sed s///g&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://linesieve.readthedocs.io/en/latest/cli.html#linesieve-sub-paths"&gt;sub-paths&lt;/a&gt;
– Shorten paths of existing files.&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://linesieve.readthedocs.io/en/latest/cli.html#linesieve-sub-cwd"&gt;sub-cwd&lt;/a&gt;
– Make working directory paths relative.&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://linesieve.readthedocs.io/en/latest/cli.html#linesieve-sub-link"&gt;sub-link&lt;/a&gt;
– Replace symlink targets.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="installing"&gt;Installing&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#installing" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Install it using &lt;a class="external" href="https://pip.pypa.io/en/stable/getting-started/"&gt;pip&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;pip&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;--upgrade&lt;span class="w"&gt; &lt;/span&gt;linesieve
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="links"&gt;Links&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#links" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;You can find:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the documentation at &lt;a class="external" href="https://linesieve.readthedocs.io/"&gt;Read the Docs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;the code on &lt;a class="external" href="https://github.com/lemon24/linesieve"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;the latest release on &lt;a class="external" href="https://pypi.org/project/linesieve/"&gt;PyPI&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a id="examples"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="example"&gt;Example&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#example" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Here's a juicy Java example – this &lt;code&gt;linesieve&lt;/code&gt; command:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;linesieve&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
span&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;-X&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--start&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;^ (\s+) at \s ( org\.junit\. | \S+ \. reflect\.\S+\.invoke )&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--end&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;^ (?! \s+ at \s )&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;--repl&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\1...&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
match&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;^\s+at \S+\.(rethrowAs|translateTo)IOException&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
sub-paths&lt;span class="w"&gt; &lt;/span&gt;--include&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;{src,tst}/**/*.java&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--modules-skip&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
sub&lt;span class="w"&gt; &lt;/span&gt;-X&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;^( \s+ at \s+ (?! .+ \.\. | com\.example\. ) .*? ) \( .*&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\1&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
sub&lt;span class="w"&gt; &lt;/span&gt;-X&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;^( \s+ at \s+ com\.example\. .*? ) \ ~\[ .*&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\1&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
sub&lt;span class="w"&gt; &lt;/span&gt;-X&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;span class="s1"&gt;    (?P&amp;lt;pre&amp;gt; \s+ at \s .*)&lt;/span&gt;
&lt;span class="s1"&gt;    (?P&amp;lt;cls&amp;gt; \w+ )&lt;/span&gt;
&lt;span class="s1"&gt;    (?P&amp;lt;mid&amp;gt; .* \( )&lt;/span&gt;
&lt;span class="s1"&gt;    (?P=cls) \.java&lt;/span&gt;
&lt;span class="s1"&gt;    (?P&amp;lt;suf&amp;gt; : .* )&lt;/span&gt;
&lt;span class="s1"&gt;    &amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;\g&amp;lt;pre&amp;gt;\g&amp;lt;cls&amp;gt;\g&amp;lt;mid&amp;gt;\g&amp;lt;suf&amp;gt;&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... shortens this 76 line traceback:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;12:34:56.789 [main] ERROR com.example.someproject.somepackage.ThingDoer - exception while notifying done listener
java.lang.RuntimeException: listener failed
	at com.example.someproject.somepackage.ThingDoerTest$DummyListener.onThingDone(ThingDoerTest.java:420) ~[tests/:?]
	at com.example.someproject.somepackage.ThingDoer.doThing(ThingDoer.java:69) ~[library/:?]
	at com.example.otherproject.Framework.doAllTheThings(Framework.java:1066) ~[example-otherproject-2.0.jar:2.0]
	at com.example.someproject.somepackage.ThingDoerTest.listenerException(ThingDoerTest.java:666) ~[tests/:?]
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[?:?]
	...
	... 60+ more lines of JUnit stuff we don't really care about ...
	...
12:34:56.999 [main] INFO done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;... to just 7 lines:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;12:34:56.789 [main] ERROR ..ThingDoer - exception while notifying done listener
java.lang.RuntimeException: listener failed
	at ..ThingDoerTest$DummyListener.onThingDone(:420) ~[tests/:?]
	at ..ThingDoer.doThing(:69) ~[library/:?]
	at com.example.otherproject.Framework.doAllTheThings(:1066)
	at ..ThingDoerTest.listenerException(:666) ~[tests/:?]
	...
12:34:56.999 [main] INFO done
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Let's break that down a bit:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;span&lt;/code&gt; gets rid of all the traceback lines coming from JUnit.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;match -v&lt;/code&gt; skips some usually useless lines from stack traces.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sub-paths&lt;/code&gt; shortens and highlights the names of classes in the current project;
&lt;code&gt;com.example&lt;wbr&gt;.someproject.&lt;wbr&gt;somepackage.&lt;wbr&gt;ThingDoer&lt;/code&gt;
becomes &lt;code&gt;..ThingDoer&lt;/code&gt;
(presumably that's enough info to open the file in your IDE).&lt;/li&gt;
&lt;li&gt;The first &lt;code&gt;sub&lt;/code&gt; gets rid of line numbers and JAR names for everything
that's not either in the current project or in another &lt;code&gt;com.example.&lt;/code&gt; package.&lt;/li&gt;
&lt;li&gt;The second &lt;code&gt;sub&lt;/code&gt; gets rid of JAR names for things in other &lt;code&gt;com.example.&lt;/code&gt; packages.&lt;/li&gt;
&lt;li&gt;The third &lt;code&gt;sub&lt;/code&gt; gets rid of the source file name;
&lt;code&gt;..ThingDoer&lt;wbr&gt;.doThing(&lt;wbr&gt;ThingDoer.java&lt;wbr&gt;:69)&lt;/code&gt; becomes &lt;code&gt;..ThingDoer&lt;wbr&gt;.doThing(&lt;wbr&gt;:69)&lt;/code&gt;
(in Java, the file name matches the class name).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For an even more advanced example,
see &lt;a class="external" href="https://linesieve.readthedocs.io/en/latest/examples.html#apache-ant-output"&gt;how to clean up Apache Ant output&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Anyway, that's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/linesieve&amp;t=Announcing%20linesieve%3A%20an%20unholy%20blend%20of%20grep%2C%20sed%2C%20awk%2C%20and%20Python%2C%20born%20out%20of%20spite"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Announcing%20linesieve%3A%20an%20unholy%20blend%20of%20grep%2C%20sed%2C%20awk%2C%20and%20Python%2C%20born%20out%20of%20spite%20https%3A//death.andgravity.com/linesieve"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/linesieve&amp;title=Announcing%20linesieve%3A%20an%20unholy%20blend%20of%20grep%2C%20sed%2C%20awk%2C%20and%20Python%2C%20born%20out%20of%20spite"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/linesieve"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Announcing%20linesieve%3A%20an%20unholy%20blend%20of%20grep%2C%20sed%2C%20awk%2C%20and%20Python%2C%20born%20out%20of%20spite&amp;url=https%3A//death.andgravity.com/linesieve&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

</content>
    <link href="https://death.andgravity.com/linesieve" rel="alternate"/>
    <summary>Java is notoriously verbose, especially when used in a serious Enterprise Project™ ...so naturally, I made linesieve, a Python text munging tool to split output into sections and match/sub/split with the full power of Python's re module.</summary>
    <published>2023-04-25T18:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/limit-concurrency">
    <id>https://death.andgravity.com/limit-concurrency</id>
    <title>Limiting concurrency in Python asyncio: the story of async imap_unordered()</title>
    <updated>2024-01-30T08:15:00+00:00</updated>
    <content type="html">&lt;p&gt;So, you're doing some async stuff, repeatedly, many times.&lt;/p&gt;
&lt;p&gt;Like, &lt;strong&gt;hundreds of thousands of times&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Maybe you're scraping some data.&lt;/p&gt;
&lt;p&gt;Maybe it's more complicated
– you're calling an API,
and then passing the result to another one,
and then saving the result of that.&lt;/p&gt;
&lt;p&gt;Either way, it's a good idea to &lt;em&gt;not&lt;/em&gt; do it all at once.
For one, it's not polite to the services you're calling.
For another, it'll load everything in memory, &lt;em&gt;all at once&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;In &lt;strong&gt;sync&lt;/strong&gt; code,
you might use a &lt;a class="external" href="https://docs.python.org/3/library/multiprocessing.html#module-multiprocessing.dummy"&gt;thread pool&lt;/a&gt; and &lt;a class="external" href="https://docs.python.org/3/library/multiprocessing.html#multiprocessing.pool.Pool.imap_unordered"&gt;imap_unordered()&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dummy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;imap_unordered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;things_to_do&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Here, concurrency is limited by the fixed number of threads.&lt;/p&gt;
&lt;p&gt;But what about &lt;strong&gt;async&lt;/strong&gt; code?
In this article,
we'll look at a few ways of limiting concurrency in asycio,
and find out which one is best.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;No, it's not &lt;a class="anchor" href="#asyncio-semaphore"&gt;Semaphore&lt;/a&gt;,
despite what Stack Overflow may tell you.&lt;/p&gt;
&lt;/section&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;If you're in a hurry – it's &lt;a class="anchor" href="#asyncio-wait"&gt;wait()&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;details class="toc" open&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#getting-started"&gt;Getting started&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#asyncio-gather"&gt;asyncio.gather()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#asyncio-semaphore"&gt;asyncio.Semaphore&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#asyncio-as-completed"&gt;asyncio.as_completed()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#asyncio-queue"&gt;asyncio.Queue&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#aside-backpressure"&gt;Aside: backpressure&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#asyncio-wait"&gt;asyncio.wait()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#async-iterables"&gt;Async iterables&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#bonus-exceptions"&gt;Bonus: exceptions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#bonus-better-decorators"&gt;Bonus: better decorators?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="getting-started"&gt;Getting started&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#getting-started" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;In order to try things out more easily,
we'll start with a test harness of sorts.&lt;/p&gt;
&lt;!--
&lt;details&gt;
&lt;summary&gt;Imports:&lt;/summary&gt;

.. literalinclude:: mo_00_serial.py
    :lines: 1-5

&lt;/details&gt;
--&gt;

&lt;p&gt;Our &lt;code&gt;async map_unordered()&lt;/code&gt; behaves pretty much like &lt;a class="external" href="https://docs.python.org/3/library/multiprocessing.html#multiprocessing.pool.Pool.imap_unordered"&gt;imap_unordered()&lt;/a&gt;
– it takes a coroutine function and an iterable of arguments,
and runs the resulting awaitables &lt;code&gt;limit&lt;/code&gt; at a time:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;map_unordered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iterable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iterable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;limit_concurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The actual running is done by &lt;code&gt;limit_concurrency()&lt;/code&gt;.
For now, we run them one by one
(we'll get back to this later on):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;limit_concurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;aw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;StopIteration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;aw&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;To simulate work being done,
we just &lt;a class="external" href="https://docs.python.org/3/library/asyncio-task.html#asyncio.sleep"&gt;sleep()&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;  &lt;span class="c1"&gt;# raises ZeroDivisionError for i == 0&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Putting it all together,
we get a &lt;code&gt;map_unordered.py LIMIT TIME...&lt;/code&gt; script that does stuff in parallel,
printing timings as we get each result:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;async_main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;map_unordered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.1f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.1f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: done&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:]]&lt;/span&gt;
    &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;async_main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;__main__&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;... like so:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;map_unordered.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="go"&gt;0.0: done&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;map_unordered.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.1&lt;span class="w"&gt; &lt;/span&gt;.2
&lt;span class="go"&gt;0.1: 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.3: 0.2&lt;/span&gt;
&lt;span class="go"&gt;0.3: done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;If you need a refresher on lower level &lt;a class="external" href="https://docs.python.org/3/library/asyncio.html"&gt;asyncio&lt;/a&gt; stuff related to waiting,
check out Hynek Schlawack's excellent
&lt;a class="external" href="https://hynek.me/articles/waiting-in-asyncio/"&gt;Waiting in asyncio&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="asyncio-gather"&gt;asyncio.gather()&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#asyncio-gather" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;In the &lt;a class="external" href="https://docs.python.org/3/library/asyncio-task.html#running-tasks-concurrently"&gt;Running Tasks Concurrently&lt;/a&gt; section of the asyncio docs,
we find &lt;a class="external" href="https://docs.python.org/3/library/asyncio-task.html#asyncio.gather"&gt;asyncio.gather()&lt;/a&gt;,
which runs awaitables concurrently and returns their results.&lt;/p&gt;
&lt;p&gt;We can use it to run &lt;code&gt;limit&lt;/code&gt;-sized batches:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;limit_concurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;itertools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;islice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;batch&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This seems to work:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;map_unordered.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.1&lt;span class="w"&gt; &lt;/span&gt;.2
&lt;span class="go"&gt;0.2: 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.2: 0.2&lt;/span&gt;
&lt;span class="go"&gt;0.2: done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... except:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;map_unordered.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.1&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.1
&lt;span class="go"&gt;0.2: 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.2: 0.2&lt;/span&gt;
&lt;span class="go"&gt;0.4: 0.2&lt;/span&gt;
&lt;span class="go"&gt;0.4: 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.4: done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... those should fit in 0.3 seconds:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Text only"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;| sleep(.1) |       sleep(.2)       |
|       sleep(.2)       | sleep(.1) |
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... but we're waiting for the entire batch to finish,
even if some tasks finish earlier:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Text only"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;| sleep(.1) |...........|       sleep(.2)       |
|       sleep(.2)       | sleep(.1) |...........|
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="asyncio-semaphore"&gt;asyncio.Semaphore&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#asyncio-semaphore" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Screw the docs, too much to read;
after some googling,
the first few Stack Overflow answers all point to &lt;a class="external" href="https://docs.python.org/3/library/asyncio-sync.html#asyncio.Semaphore"&gt;asyncio.Semaphore&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Like its &lt;a class="external" href="https://docs.python.org/3/library/threading.html#threading.Semaphore"&gt;threading counterpart&lt;/a&gt;,
we can use it to limit
how many times the body of a &lt;code&gt;with&lt;/code&gt; block is entered in parallel:&lt;/p&gt;
&lt;!--
```python
sem = asyncio.Semaphore(10)

# ... later
async with sem:
    # no more than 10 times in parallel
```

Sounds about what we're looking for:
--&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;limit_concurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;semaphore&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Semaphore&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aw&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;semaphore&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;aw&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gather&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nb"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;map_unordered.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.1&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.1
&lt;span class="go"&gt;0.3: 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.3: 0.2&lt;/span&gt;
&lt;span class="go"&gt;0.3: 0.2&lt;/span&gt;
&lt;span class="go"&gt;0.3: 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.3: done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... except, because &lt;a class="external" href="https://docs.python.org/3/library/asyncio-task.html#asyncio.gather"&gt;gather()&lt;/a&gt; takes a sequence,
we end up consuming the entire &lt;code&gt;aws&lt;/code&gt; iterable
before &lt;a class="external" href="https://docs.python.org/3/library/asyncio-task.html#asyncio.gather"&gt;gather()&lt;/a&gt; is even called.
Let's highlight this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;on_iter_end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
    &lt;span class="n"&gt;callback&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;
&lt;span class="normal"&gt;48&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="n"&gt;timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;on_iter_end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;iter end&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;async_main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;As expected:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;map_unordered.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.1&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.1
&lt;span class="go"&gt;iter end&lt;/span&gt;
&lt;span class="go"&gt;0.3: 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.3: 0.2&lt;/span&gt;
&lt;span class="go"&gt;0.3: 0.2&lt;/span&gt;
&lt;span class="go"&gt;0.3: 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.3: done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For small iterables, this is fine,
but for bigger ones,
creating all the tasks upfront without running them
might cause memory issues.
Also, if the iterable is lazy (e.g. it comes from a paginated API),
we only start work after it's all consumed in memory,
instead processing it in a streaming fashion.&lt;/p&gt;
&lt;h2 id="asyncio-as-completed"&gt;asyncio.as_completed()&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#asyncio-as-completed" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;At a glance, &lt;a class="external" href="https://docs.python.org/3/library/asyncio-task.html#asyncio.as_completed"&gt;asyncio.as_completed()&lt;/a&gt; might do what we need
– it takes an iterable of awaitables, runs them concurrently,
and returns an iterator of coroutines that
&amp;quot;can be awaited to get the earliest next result
from the iterable of the remaining awaitables&amp;quot;.&lt;/p&gt;
&lt;p&gt;Sadly, &lt;a class="external" href="https://github.com/python/cpython/blob/3.11/Lib/asyncio/tasks.py#L557"&gt;it still consumes the iterable right away&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;as_completed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# set-up&lt;/span&gt;
    &lt;span class="n"&gt;todo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ensure_future&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;loop&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;  &lt;span class="c1"&gt;# actual logic&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;But there's another, subtler issue.&lt;/p&gt;
&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/library/asyncio-task.html#asyncio.as_completed"&gt;as_completed()&lt;/a&gt; has no limits of its own
– it's up to us to limit how fast we feed it awaitables.
Presumably, we could wrap the input iterable
into a generator that yields awaitables
only if enough results came out the other end,
and waits otherwise.&lt;/p&gt;
&lt;p&gt;However, due to &lt;a class="external" href="https://peps.python.org/pep-3156/#waiting-for-multiple-coroutines"&gt;historical&lt;/a&gt; &lt;a class="external" href="https://peps.python.org/pep-0525/#support-for-asynchronous-iteration-protocol"&gt;reasons&lt;/a&gt;,
as_completed() takes a plain-old-&lt;em&gt;sync&lt;/em&gt;-iterator
– we cannot &lt;code&gt;await&lt;/code&gt; anything in its (sync) &lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#iterator.__next__"&gt;__next__()&lt;/a&gt;,
and sync waiting of any kind would block (and possibly deadlock)
the entire event loop.&lt;/p&gt;
&lt;p&gt;So, no &lt;a class="external" href="https://docs.python.org/3/library/asyncio-task.html#asyncio.as_completed"&gt;as_completed()&lt;/a&gt; for you.&lt;/p&gt;
&lt;!--

.. literalinclude:: mo_30_as_completed.py
    :lines: 13-21

```console
$ python map_unordered.py 2 .1 .2 .2 .1
iter end
0.3: 0.1
0.3: 0.2
0.3: 0.2
0.3: 0.1
0.3: done
```

--&gt;

&lt;h2 id="asyncio-queue"&gt;asyncio.Queue&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#asyncio-queue" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Speaking of threading counterparts,
how would you implement &lt;a class="external" href="https://docs.python.org/3/library/multiprocessing.html#multiprocessing.pool.Pool.imap_unordered"&gt;imap_unordered()&lt;/a&gt;
if there was no Pool? Queues, of course!&lt;/p&gt;
&lt;p&gt;And &lt;a class="external" href="https://docs.python.org/3/library/asyncio.html"&gt;asyncio&lt;/a&gt; has its own &lt;a class="external" href="https://docs.python.org/3/library/asyncio-queue.html#asyncio.Queue"&gt;Queue&lt;/a&gt;,
which you use in pretty much the same way:
start &lt;code&gt;limit&lt;/code&gt; worker tasks that loop forever,
each pulling awaitables, awaiting them,
and putting the results into a queue.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;limit_concurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ndone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;aw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;StopIteration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;aw&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="n"&gt;worker_tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;ndone&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;rv&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;rv&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;rv&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;ndone&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The iterable is exhausted before the last &amp;quot;batch&amp;quot; starts:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;map_unordered.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.1&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.3&lt;span class="w"&gt; &lt;/span&gt;.3&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.1
&lt;span class="go"&gt;0.1: 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.2: 0.2&lt;/span&gt;
&lt;span class="go"&gt;0.4: 0.3&lt;/span&gt;
&lt;span class="go"&gt;0.5: 0.3&lt;/span&gt;
&lt;span class="go"&gt;iter end&lt;/span&gt;
&lt;span class="go"&gt;0.6: 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.6: 0.2&lt;/span&gt;
&lt;span class="go"&gt;0.6: done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I was going to work up to this in a few steps,
but I'll just point out three common bugs
this type of code might have (that apply to threads too).&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;
First,
we could increment &lt;code&gt;ndone&lt;/code&gt; from the worker,
but this makes &lt;code&gt;await queue.get()&lt;/code&gt; hang forever for empty iterables,
since workers never get to run by the time we get to it;
because there's no other await, it's not even a race condition.
&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;limit_concurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ndone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;nonlocal&lt;/span&gt; &lt;span class="n"&gt;ndone&lt;/span&gt;

        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;aw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;StopIteration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;ndone&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;aw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;worker_tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;ndone&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;map_unordered.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;
&lt;span class="go"&gt;iter end&lt;/span&gt;
&lt;span class="go"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="go"&gt;  ...&lt;/span&gt;
&lt;span class="go"&gt;asyncio.exceptions.TimeoutError&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;The solution is to signal the worker is done in-band,
by putting a &lt;a class="internal" href="/sentinels#what-s-a-sentinel-and-why-do-i-need-one"&gt;sentinel&lt;/a&gt; on the queue.
I guess a good rule of thumb
is that you want a put() for each get() without a timeout.&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;!--
```console
$ python map_unordered.py 2
iter end
0.0: done
```
--&gt;

&lt;details&gt;
&lt;summary&gt;
Second,
you have to catch all exceptions;
otherwise,
the worker gets killed,
and &lt;code&gt;get()&lt;/code&gt; waits forever for a sentinel that will never come.
&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;limit_concurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;ndone&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="n"&gt;done&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;aw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;StopIteration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;break&lt;/span&gt;

            &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;put&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;aw&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;worker_tasks&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create_task&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;worker&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;ndone&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="n"&gt;rv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;queue&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;rv&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;ndone&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;rv&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;map_unordered.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.1&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.1
&lt;span class="go"&gt;0.1: 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.2: 0.2&lt;/span&gt;
&lt;span class="go"&gt;0.4: 0.2&lt;/span&gt;
&lt;span class="go"&gt;iter end&lt;/span&gt;
&lt;span class="go"&gt;0.5: 0.1&lt;/span&gt;
&lt;span class="go"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="go"&gt;  ...&lt;/span&gt;
&lt;span class="go"&gt;asyncio.exceptions.TimeoutError&lt;/span&gt;
&lt;span class="go"&gt;Task exception was never retrieved&lt;/span&gt;
&lt;span class="go"&gt;future: &amp;lt;Task finished name=&amp;#39;Task-3&amp;#39; coro=&amp;lt;limit_concurrency.&amp;lt;locals&amp;gt;.worker() done, defined at map_unordered.py:20&amp;gt; exception=ZeroDivisionError(&amp;#39;float division by zero&amp;#39;)&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="go"&gt;  ...&lt;/span&gt;
&lt;span class="go"&gt;ZeroDivisionError: float division by zero&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;!--
```console
$ python map_unordered.py 2 .1 .2 0 .2 .1
0.1: 0.1
Traceback (most recent call last):
  ...
ZeroDivisionError: float division by zero
```
--&gt;

&lt;p&gt;Finally, our input iterator is synchronous (for now),
so no other task can run during &lt;code&gt;next(aws)&lt;/code&gt;.
But if it were async,
any number of tasks could &lt;code&gt;await anext(aws)&lt;/code&gt; in parallel,
leading to &lt;a class="external" href="https://stackoverflow.com/a/46858792"&gt;concurrency issues&lt;/a&gt;.
The fix is the same as with threads:
either protect that call with a &lt;a class="external" href="https://docs.python.org/3/library/asyncio-sync.html#asyncio.Lock"&gt;Lock&lt;/a&gt;,
or feed awaitables to workers through an input queue.&lt;/p&gt;
&lt;p&gt;Anyway, no need to worry about any of that – a better solution awaits.&lt;/p&gt;
&lt;h2 id="aside-backpressure"&gt;Aside: backpressure&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#aside-backpressure" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;At this point, we're technically done
– the queue solution does everything
&lt;a class="external" href="https://docs.python.org/3/library/multiprocessing.html#multiprocessing.pool.Pool.imap_unordered"&gt;Pool.​imap_unordered()&lt;/a&gt; does.&lt;/p&gt;
&lt;p&gt;So much so, that, &lt;a class="external" href="https://bugs.python.org/issue40110"&gt;like imap_unordered()&lt;/a&gt;,
it lacks &lt;a class="external" href="https://medium.com/@jayphelps/backpressure-explained-the-flow-of-data-through-software-2350b3e77ce7"&gt;backpressure&lt;/a&gt;:
when code consuming results from &lt;code&gt;map_unordered()&lt;/code&gt;
cannot keep up with the tasks producing them,
the results accumulate in the internal queue,
with potentially infinite memory usage.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;multiprocessing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dummy&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Pool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pool&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;imap_unordered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;got result&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="go"&gt;0&lt;/span&gt;
&lt;span class="go"&gt;1&lt;/span&gt;
&lt;span class="go"&gt;2&lt;/span&gt;
&lt;span class="go"&gt;3&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;async_print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;(async)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;map_unordered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;async_print&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;got result&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="go"&gt;0 (async)&lt;/span&gt;
&lt;span class="go"&gt;1 (async)&lt;/span&gt;
&lt;span class="go"&gt;2 (async)&lt;/span&gt;
&lt;span class="go"&gt;3 (async)&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To fix this, we make the queue bounded,
so that workers block while the queue is full.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;15&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="n"&gt;queue&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;map_unordered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;async_print&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;got result&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="go"&gt;0 (async)&lt;/span&gt;
&lt;span class="go"&gt;1 (async)&lt;/span&gt;
&lt;span class="go"&gt;2 (async)&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;span class="go"&gt;3 (async)&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;span class="go"&gt;4 (async)&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Alas, we can't do the same thing for
&lt;a class="external" href="https://docs.python.org/3/library/multiprocessing.html#multiprocessing.pool.Pool.imap_unordered"&gt;Pool.​imap_unordered()&lt;/a&gt;
because don't have access to its queue,
but that's a story for another time.&lt;/p&gt;
&lt;!--

# asyncio.Queue, redux

.. literalinclude:: mo_45_queue_as_completed.py
    :lines: 13-35

```console
$ python map_unordered.py 2 .1 .2 .2 .1
iter end
0.1: 0.1
0.2: 0.2
0.2: 0.2
0.2: 0.1
0.2: done
```

( ≖_≖)

Everything runs at once...


as_completed() yields the coroutines before they are actually finished:

&gt; Each coroutine returned can be awaited to get the earliest next result from the iterable of the remaining awaitables.

That is, the waiting happens when you `await` them.

This is unlike the original, not-async as_completed() from concurrent.futures:

https://docs.python.org/3/library/concurrent.futures.html#concurrent.futures.as_completed

.. literalinclude:: sync_as_completed.py
    :lines: 1-

```console
$ python sync_as_completed.py
0.1
0.2
0.3
0.3
```

Note there's no `future.result()` call here; unlike the async version, the wait happens *inside* the as_completed() iterator.


---

If only there was some other way to wait tasks, also inspired by the concurrent.futures API, that actually blocks until they're done.

https://docs.python.org/3/library/asyncio-task.html#asyncio.wait

.. literalinclude:: mo_46_queue_wait.py
    :lines: 26-27

```console
$ python map_unordered.py 2 .1 .2 .2 .1
0.1: 0.1
0.2: 0.2
iter end
0.3: 0.1
0.3: 0.2
0.3: done
```

--&gt;

&lt;h2 id="asyncio-wait"&gt;asyncio.wait()&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#asyncio-wait" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Pretending we're using threads works,
but it's not all that idiomatic.&lt;/p&gt;
&lt;p&gt;If only there was some sort of low level, &lt;a class="external" href="https://linux.die.net/man/2/select"&gt;select()&lt;/a&gt;-like primitive
taking a set of tasks
and blocking until at least one of them finishes.
And of course there is
– we've been purposefully avoiding it this entire time –
it's &lt;a class="external" href="https://docs.python.org/3/library/asyncio-task.html#asyncio.wait"&gt;asyncio.wait()&lt;/a&gt;, and it does exactly that.&lt;/p&gt;
&lt;p&gt;By default, it waits until all tasks are completed,
which isn't much better than &lt;a class="anchor" href="#asyncio-gather"&gt;gather()&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;But, with &lt;code&gt;return_when=​FIRST_COMPLETED&lt;/code&gt;,
it waits until &lt;em&gt;at least one&lt;/em&gt; task is completed.
We can use this to keep a &lt;code&gt;limit&lt;/code&gt;-sized set of running tasks
updated with new tasks as soon as the old ones finish:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;limit_concurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;aws_ended&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;aws_ended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;aws_ended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;aw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;StopIteration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;aws_ended&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ensure_future&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aw&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_when&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIRST_COMPLETED&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We change &lt;code&gt;limit_concurrency()&lt;/code&gt; to yield awaitables instead of results,
so it's more symmetric – awaitables in, awaitables out.
&lt;code&gt;map_unordered()&lt;/code&gt; then becomes an async generator function,
instead of a sync function returning an async generator.
This is functionally the same,
but does make it a bit more self-documenting.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;map_unordered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iterable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iterable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;limit_concurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;This implementation has all the properties that the Queue one has:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;map_unordered.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.1&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.1
&lt;span class="go"&gt;0.1: 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.2: 0.2&lt;/span&gt;
&lt;span class="go"&gt;0.3: 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.3: 0.2&lt;/span&gt;
&lt;span class="go"&gt;iter end&lt;/span&gt;
&lt;span class="go"&gt;0.3: done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... and backpressure too:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;map_unordered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;async_print&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sleep&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;got result&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="go"&gt;0 (async)&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;span class="go"&gt;1 (async)&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;span class="go"&gt;2 (async)&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;span class="go"&gt;3 (async)&lt;/span&gt;
&lt;span class="go"&gt;got result&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        Liking this so far? Here&amp;#39;s another article you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/asyncio-bridge"&gt;
            Running async code from sync code in Python
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="async-iterables"&gt;Async iterables&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#async-iterables" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;OK, but what if we pass &lt;code&gt;map_unordered()&lt;/code&gt;
an &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-asynchronous-iterable"&gt;asynchronous iterable&lt;/a&gt;?
We are talking about async stuff, after all.&lt;/p&gt;
&lt;p&gt;This opens up a whole looking-glass world of async iteration:
instead of &lt;a class="external" href="https://docs.python.org/3/library/functions.html#iter"&gt;iter()&lt;/a&gt; you have &lt;a class="external" href="https://docs.python.org/3/library/functions.html#aiter"&gt;aiter()&lt;/a&gt;,
instead of &lt;a class="external" href="https://docs.python.org/3/library/functions.html#next"&gt;next()&lt;/a&gt; you have &lt;a class="external" href="https://docs.python.org/3/library/functions.html#anext"&gt;anext()&lt;/a&gt;,
some of them you await, some you don't...
Thankfully,
we can support both
without making things much worse.&lt;/p&gt;
&lt;p&gt;And we don't need to be particularly smart about it either;
we can just feed the current code an async iterable from &lt;code&gt;main()&lt;/code&gt;,
and punch our way through the exceptions:&lt;/p&gt;
&lt;!-- as_async_iter --&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;57&lt;/span&gt;
&lt;span class="normal"&gt;58&lt;/span&gt;
&lt;span class="normal"&gt;59&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;as_async_iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;!-- args = as_async_iter... --&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;66&lt;/span&gt;
&lt;span class="normal"&gt;67&lt;/span&gt;
&lt;span class="normal"&gt;68&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;on_iter_end&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;iter end&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;as_async_iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait_for&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;async_main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;hr /&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;map_unordered.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.1&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.1
&lt;span class="go"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="go"&gt;  ...&lt;/span&gt;
&lt;span class="go"&gt;  File &amp;quot;map_unordered.py&amp;quot;, line 9, in map_unordered&lt;/span&gt;
&lt;span class="go"&gt;    aws = map(func, iterable)&lt;/span&gt;
&lt;span class="go"&gt;TypeError: &amp;#39;async_generator&amp;#39; object is not iterable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/library/functions.html#map"&gt;map()&lt;/a&gt; doesn't work with async iterables,
so we use a &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-generator-expression"&gt;generator expression&lt;/a&gt; instead.&lt;/p&gt;
&lt;!-- map_unordered --&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;map_unordered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iterable&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;iterable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;iterable&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;limit_concurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;task&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;In true &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-EAFP"&gt;easier to ask for forgiveness than permission&lt;/a&gt; style,
we handle the exception from &lt;a class="external" href="https://docs.python.org/3/library/functions.html#map"&gt;map()&lt;/a&gt; instead of, say,
checking if &lt;code&gt;aws&lt;/code&gt; is an instance of &lt;a class="external" href="https://docs.python.org/3/library/collections.abc.html#collections.abc.Iterable"&gt;collections.​abc.​Iterable&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We could wrap &lt;code&gt;aws&lt;/code&gt; to always be an async iterable,
but &lt;code&gt;limit_concurrency()&lt;/code&gt; is useful on it its own,
so it's better to support both.&lt;/p&gt;
&lt;hr /&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;map_unordered.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.1&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.1
&lt;span class="go"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="go"&gt;  ...&lt;/span&gt;
&lt;span class="go"&gt;  File &amp;quot;map_unordered.py&amp;quot;, line 19, in limit_concurrency&lt;/span&gt;
&lt;span class="go"&gt;    aws = iter(aws)&lt;/span&gt;
&lt;span class="go"&gt;TypeError: &amp;#39;async_generator&amp;#39; object is not iterable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For async iterables, we need to use &lt;a class="external" href="https://docs.python.org/3/library/functions.html#aiter"&gt;aiter()&lt;/a&gt;:&lt;/p&gt;
&lt;!-- aiter(aws) --&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;limit_concurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;aiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;is_async&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;is_async&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;hr /&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;map_unordered.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.1&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.1
&lt;span class="go"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="go"&gt;  ...&lt;/span&gt;
&lt;span class="go"&gt;  File &amp;quot;map_unordered.py&amp;quot;, line 32, in limit_concurrency&lt;/span&gt;
&lt;span class="go"&gt;    aw = next(aws)&lt;/span&gt;
&lt;span class="go"&gt;TypeError: &amp;#39;async_generator&amp;#39; object is not an iterator&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... and &lt;a class="external" href="https://docs.python.org/3/library/functions.html#anext"&gt;anext()&lt;/a&gt;:&lt;/p&gt;
&lt;!-- anext(aws) --&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;aws_ended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;aw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;anext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_async&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;StopAsyncIteration&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_async&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="ne"&gt;StopIteration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;aws_ended&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ensure_future&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aw&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;... which unlike &lt;a class="external" href="https://docs.python.org/3/library/functions.html#aiter"&gt;aiter()&lt;/a&gt;, has to be awaited.&lt;/p&gt;
&lt;hr /&gt;
&lt;details&gt;
&lt;summary&gt;Here's &lt;code&gt;limit_concurrency()&lt;/code&gt; in its entire glory:&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;limit_concurrency&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;aiter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;is_async&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;aws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;is_async&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;

    &lt;span class="n"&gt;aws_ended&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;aws_ended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;aws_ended&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;aw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;anext&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_async&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aws&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;StopAsyncIteration&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;is_async&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="ne"&gt;StopIteration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;aws_ended&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ensure_future&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;aw&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;

        &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pending&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;wait&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;return_when&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;asyncio&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FIRST_COMPLETED&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;done&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;/details&gt;

&lt;p&gt;Not as clean as before, but it gets the job done:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;map_unordered.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.1&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.1
&lt;span class="go"&gt;0.1: 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.2: 0.2&lt;/span&gt;
&lt;span class="go"&gt;0.3: 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.3: 0.2&lt;/span&gt;
&lt;span class="go"&gt;iter end&lt;/span&gt;
&lt;span class="go"&gt;0.3: done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;p&gt;Anyway, that's it for now.
Here's &lt;a class="attachment" href="/_file/limit-concurrency/mo_60_async_iter.py"&gt;the final version of the code&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/limit-concurrency&amp;t=Limiting%20concurrency%20in%20Python%20asyncio%3A%20the%20story%20of%20async%20imap_unordered%28%29"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Limiting%20concurrency%20in%20Python%20asyncio%3A%20the%20story%20of%20async%20imap_unordered%28%29%20https%3A//death.andgravity.com/limit-concurrency"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/limit-concurrency&amp;title=Limiting%20concurrency%20in%20Python%20asyncio%3A%20the%20story%20of%20async%20imap_unordered%28%29"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/limit-concurrency"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Limiting%20concurrency%20in%20Python%20asyncio%3A%20the%20story%20of%20async%20imap_unordered%28%29&amp;url=https%3A//death.andgravity.com/limit-concurrency&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;





&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        If you&amp;#39;ve made it this far, you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/output"&gt;
            Why you should still read the docs
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="bonus-exceptions"&gt;Bonus: exceptions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bonus-exceptions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;OK, so what about exceptions?&lt;/p&gt;
&lt;p&gt;A lot of times,
you still want to do the rest of the things,
even if one fails.
Also, you probably want to know &lt;em&gt;which&lt;/em&gt; one failed,
but the &lt;code&gt;map_unordered()&lt;/code&gt; results are not in order,
so how could you tell?&lt;/p&gt;
&lt;p&gt;The most flexible solution is to let the user handle it
just like they would with
&lt;a class="external" href="https://docs.python.org/3/library/multiprocessing.html#multiprocessing.pool.Pool.imap_unordered"&gt;Pool.​imap_unordered()&lt;/a&gt; –
by decorating the original function.
Here's one way of doing it:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;
&lt;span class="normal"&gt;52&lt;/span&gt;
&lt;span class="normal"&gt;53&lt;/span&gt;
&lt;span class="normal"&gt;54&lt;/span&gt;
&lt;span class="normal"&gt;55&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;return_args_and_exceptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_return_args_and_exceptions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_return_args_and_exceptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;e&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;66&lt;/span&gt;
&lt;span class="normal"&gt;67&lt;/span&gt;
&lt;span class="normal"&gt;68&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="n"&gt;wrapped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;return_args_and_exceptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;map_unordered&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wrapped&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.1f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; -&amp;gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;map_unordered.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.1&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;.2&lt;span class="w"&gt; &lt;/span&gt;.1
&lt;span class="go"&gt;0.1: 0.1 -&amp;gt; 0.1&lt;/span&gt;
&lt;span class="go"&gt;0.1: 0.0 -&amp;gt; float division by zero&lt;/span&gt;
&lt;span class="go"&gt;0.2: 0.2 -&amp;gt; 0.2&lt;/span&gt;
&lt;span class="go"&gt;0.3: 0.2 -&amp;gt; 0.2&lt;/span&gt;
&lt;span class="go"&gt;0.3: 0.1 -&amp;gt; 0.1&lt;/span&gt;
&lt;span class="go"&gt;iter end&lt;/span&gt;
&lt;span class="go"&gt;0.3: done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="bonus-better-decorators"&gt;Bonus: better decorators?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bonus-better-decorators" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Finally, here's a cool thing I learned from the &lt;a class="external" href="https://docs.python.org/3/library/asyncio-eventloop.html#asyncio-pass-keywords"&gt;asyncio docs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;When writing decorators,
you can use &lt;a class="external" href="https://docs.python.org/3/library/functools.html#functools.partial"&gt;partial()&lt;/a&gt; to bind the decorated function to an existing wrapper,
instead of always returning a new one.
The result is a more descriptive &lt;a class="external" href="https://docs.python.org/3/library/functions.html#repr"&gt;representation&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;return_args_and_exceptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;functools.partial(&amp;lt;function _return_args_and_exceptions at 0x10647fd80&amp;gt;, &amp;lt;function do_stuff at 0x10647d8a0&amp;gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Compare with the traditional version:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;return_args_and_exceptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;func&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;wrapper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;wrapper&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;return_args_and_exceptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;do_stuff&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;function return_args_and_exceptions.&amp;lt;locals&amp;gt;.wrapper at 0x103993560&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;Does this have a fancy, academic name? &lt;a class="internal" href="/about#contact"&gt;Do let me know!&lt;/a&gt; &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/limit-concurrency" rel="alternate"/>
    <summary>So, you're doing some async stuff, repeatedly, hundreds of thousands of times. How do you *not* do it all at once? Hint: asyncio.Semaphore is not always the best way, despite what Stack Overflow may tell you ;)</summary>
    <published>2023-03-27T18:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/yaml-unhashable-key">
    <id>https://death.andgravity.com/yaml-unhashable-key</id>
    <title>yaml: while constructing a mapping found unhashable key</title>
    <updated>2023-02-21T23:02:21+00:00</updated>
    <content type="html">&lt;p&gt;So you're trying to read some YAML using &lt;a class="external" href="https://github.com/yaml/pyyaml"&gt;PyYAML&lt;/a&gt;,
and get an exception like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;[0, 0]: top-left&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;[1, 1]: bottom-right&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;yaml.constructor.ConstructorError&lt;/span&gt;: &lt;span class="n"&gt;while constructing a mapping&lt;/span&gt;
&lt;span class="x"&gt;found unhashable key&lt;/span&gt;
&lt;span class="x"&gt;  in &amp;quot;&amp;lt;unicode string&amp;gt;&amp;quot;, line 1, column 1:&lt;/span&gt;
&lt;span class="x"&gt;    [0, 0]: top-left&lt;/span&gt;
&lt;span class="x"&gt;    ^&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;!-- https://github.com/yaml/pyyaml/issues/339 --&gt;

&lt;h2 id="what-does-it-mean"&gt;What does it mean?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-does-it-mean" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The error message is pretty self-explanatory, but let's unpack it a bit.&lt;/p&gt;
&lt;p&gt;First, it happened during construction –
that is,
while converting
the generic representation of the YAML document
to native data structures;
in this case, converting a mapping
to a Python &lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#dict"&gt;dict&lt;/a&gt;.&lt;/p&gt;
&lt;figure class="figure" id="yaml-processing-overview-diagram"&gt;
&lt;img class="img-responsive" src="/_file/any-yaml/overview2.svg" alt="YAML Processing Overview Diagram" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
&lt;a class="external" href="https://yaml.org/spec/1.2.2/#31-processes"&gt;YAML Processing Overview&lt;/a&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The problem is that a key of the mapping,
&lt;code&gt;[0, 0]&lt;/code&gt;, is not &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-hashable"&gt;hashable&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;An object is hashable if it has a hash value which never changes during its lifetime (it needs a &lt;code&gt;__hash__()&lt;/code&gt; method), and can be compared to other objects (it needs an &lt;code&gt;__eq__()&lt;/code&gt; method). Hashable objects which compare equal must have the same hash value.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Most &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-immutable"&gt;immutable&lt;/a&gt; built-ins are hashable;
&lt;a class="external" href="https://docs.python.org/3/glossary.html#term-mutable"&gt;mutable&lt;/a&gt; containers (such as lists) are not;
immutable containers (such as tuples) are hashable only if their elements are.&lt;/p&gt;
&lt;h2 id="why-does-this-happen"&gt;Why does this happen?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-does-this-happen" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;This is not a limitation of YAML itself;
quoting the &lt;a class="external" href="https://yaml.org/spec/1.2.2/#mapping"&gt;spec&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The content of a mapping node is an unordered set of key/value node pairs, with the restriction that each of the keys is unique. &lt;strong&gt;YAML places no further restrictions on the nodes.&lt;/strong&gt; In particular, &lt;strong&gt;keys may be arbitrary nodes&lt;/strong&gt;, the same node may be used as the value of several key/value pairs and a mapping could even contain itself as a key or a value.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We can load everything up to the representation:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[0, 0]: top-left&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;MappingNode(&lt;/span&gt;
&lt;span class="go"&gt;    tag=&amp;#39;tag:yaml.org,2002:map&amp;#39;,&lt;/span&gt;
&lt;span class="go"&gt;    value=[&lt;/span&gt;
&lt;span class="go"&gt;        (&lt;/span&gt;
&lt;span class="go"&gt;            SequenceNode(&lt;/span&gt;
&lt;span class="go"&gt;                tag=&amp;#39;tag:yaml.org,2002:seq&amp;#39;,&lt;/span&gt;
&lt;span class="go"&gt;                value=[&lt;/span&gt;
&lt;span class="go"&gt;                    ScalarNode(tag=&amp;#39;tag:yaml.org,2002:int&amp;#39;, value=&amp;#39;0&amp;#39;),&lt;/span&gt;
&lt;span class="go"&gt;                    ScalarNode(tag=&amp;#39;tag:yaml.org,2002:int&amp;#39;, value=&amp;#39;0&amp;#39;),&lt;/span&gt;
&lt;span class="go"&gt;                ],&lt;/span&gt;
&lt;span class="go"&gt;            ),&lt;/span&gt;
&lt;span class="go"&gt;            ScalarNode(tag=&amp;#39;tag:yaml.org,2002:str&amp;#39;, value=&amp;#39;top-left&amp;#39;),&lt;/span&gt;
&lt;span class="go"&gt;        )&lt;/span&gt;
&lt;span class="go"&gt;    ],&lt;/span&gt;
&lt;span class="go"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The limitation comes from how dicts are implemented, &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-hashable"&gt;specifically&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Hashability makes an object usable as a dictionary key and a set member, because these data structures use the hash value internally.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;{[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;top-left&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;&amp;lt;stdin&amp;gt;&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;1&lt;/span&gt;, in &lt;span class="n"&gt;&amp;lt;module&amp;gt;&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;unhashable type: &amp;#39;list&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If we use a (hashable) tuple instead, it works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;{(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;top-left&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="go"&gt;{(0, 0): &amp;#39;top-left&amp;#39;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="what-now"&gt;What now?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-now" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id="use-the-representation"&gt;Use the representation&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#use-the-representation" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Depending on your needs, the representation might be enough.
But probably not...&lt;/p&gt;
&lt;h3 id="use-full-load-and-python-specific-tags"&gt;Use full_load() and Python-specific tags&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#use-full-load-and-python-specific-tags" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;If you control the input,
and you're OK with language-specific tags,
use &lt;code&gt;full_load()&lt;/code&gt;;
it resolves all tags &lt;em&gt;except&lt;/em&gt; those known to be unsafe,
including all the Python-specific tags listed
&lt;a class="external" href="https://pyyaml.org/wiki/PyYAMLDocumentation#yaml-tags-and-python-types"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;full_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;!!python/tuple [0, 0]: top-left&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;!!python/tuple [1, 1]: bottom-right&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;{(0, 0): &amp;#39;top-left&amp;#39;, (1, 1): &amp;#39;bottom-right&amp;#39;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You could also use &lt;code&gt;unsafe_load()&lt;/code&gt;,
but most of the time it's not what you want:&lt;/p&gt;
&lt;section class="admonition warning"&gt;
&lt;p class="admonition-title"&gt;Warning&lt;/p&gt;
&lt;p&gt;&lt;code&gt;yaml.unsafe_load()&lt;/code&gt; is &lt;strong&gt;unsafe&lt;/strong&gt; for &lt;strong&gt;untrusted data&lt;/strong&gt;,
because it allows &lt;strong&gt;running arbitrary code&lt;/strong&gt;.
Consider using &lt;code&gt;safe_load()&lt;/code&gt; or &lt;code&gt;full_load()&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;For example, you can do this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unsafe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!!python/object/new:os.system [echo WOOSH. YOU HAVE been compromised]&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;WOOSH. YOU HAVE been compromised&lt;/span&gt;
&lt;span class="go"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;There were a bunch of &lt;a class="external" href="https://www.cvedetails.com/vulnerability-list/vendor_id-13115/Pyyaml.html"&gt;CVEs&lt;/a&gt; about it.&lt;/p&gt;
&lt;/section&gt;
&lt;h3 id="but-i-don-t-control-the-input"&gt;But, I don't control the input&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-don-t-control-the-input" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;If you don't control the input,
you can use a custom constructor
to convert the keys to something hashable.
Here, we convert list keys to tuples:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SafeLoader&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;construct_mapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;construct_pairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;deep&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;rv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;rv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rv&lt;/span&gt;

&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;construct_mapping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;construct_mapping&lt;/span&gt;
&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;tag:yaml.org,2002:map&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;construct_mapping&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;!--
yaml.load("""\
[0, 0]: top-left
[1, 1]: bottom-right
""", Loader=Loader)
--&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;[0, 0]: top-left&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;[1, 1]: bottom-right&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;{(0, 0): &amp;#39;top-left&amp;#39;, (1, 1): &amp;#39;bottom-right&amp;#39;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We subclass SafeLoader to account for
&lt;a class="internal" href="/any-yaml#a-note-on-pyyaml-extensibility"&gt;a PyYAML quirk&lt;/a&gt;
– calling &lt;code&gt;add_constructor()&lt;/code&gt; directly
would modify it in-place, for &lt;em&gt;everyone&lt;/em&gt;, which isn't necessarily great.
We still override &lt;code&gt;construct_mapping&lt;/code&gt; so that other constructors
wanting to make a mapping get to use our version.&lt;/p&gt;
&lt;p&gt;Alas, this is quite limited,
because new key types need to be handled explicitly;
for example, we might be able to convert dicts to a &lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#set-types-set-frozenset"&gt;frozenset&lt;/a&gt; of &lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#dict.items"&gt;items()&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{0: 1}&lt;/span&gt;&lt;span class="s2"&gt;: top-left&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;rv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;unhashable type: &amp;#39;dict&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;But nested keys don't work either,
we need to convert them recursively ourselves:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[[0]]: top-left&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;rv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;unhashable type: &amp;#39;list&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="but-i-don-t-know-the-key-types-in-advance"&gt;But, I don't know the key types in advance&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-don-t-know-the-key-types-in-advance" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;A decent trade-off is to just let the mapping devolve into a list of pairs:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;construct_mapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;pairs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;construct_pairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;[0, 0]: top-left&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;[1, 1]: bottom-right&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[([0, 0], &amp;#39;top-left&amp;#39;), ([1, 1], &amp;#39;bottom-right&amp;#39;)]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{0: 1}&lt;/span&gt;&lt;span class="s2"&gt;: top-left&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[({0: 1}, &amp;#39;top-left&amp;#39;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="but-i-need-to-round-trip-the-data"&gt;But, I need to round-trip the data&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-need-to-round-trip-the-data" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;This works, until you need to round-trip the data,
or emit this kind of YAML yourself.&lt;/p&gt;
&lt;p&gt;Luckily, I've already written &lt;strong&gt;&lt;a class="internal" href="/any-yaml"&gt;a whole article&lt;/a&gt;&lt;/strong&gt;
on how to do that, complete with code;
the trick is to mark the list of pairs in some way:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;[0, 0]: top-left&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;[1, 1]: bottom-right&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="go"&gt;Pairs([([0, 0], &amp;#39;top-left&amp;#39;), ([1, 1], &amp;#39;bottom-right&amp;#39;)])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... so you can represent it back into a mapping:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dumper&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Dumper&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;? - 0&lt;/span&gt;
&lt;span class="go"&gt;  - 0&lt;/span&gt;
&lt;span class="go"&gt;: top-left&lt;/span&gt;
&lt;span class="go"&gt;? - 1&lt;/span&gt;
&lt;span class="go"&gt;  - 1&lt;/span&gt;
&lt;span class="go"&gt;: bottom-right&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;(The fancy &lt;code&gt;? &lt;/code&gt; syntax indicates a
&lt;a class="external" href="https://yaml.org/spec/1.2.2/#example-mapping-between-sequences"&gt;complex mapping key&lt;/a&gt;,
but that's just another way of writing the original input.)&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/yaml-unhashable-key&amp;t=yaml%3A%20while%20constructing%20a%20mapping%20found%20unhashable%20key"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=yaml%3A%20while%20constructing%20a%20mapping%20found%20unhashable%20key%20https%3A//death.andgravity.com/yaml-unhashable-key"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/yaml-unhashable-key&amp;title=yaml%3A%20while%20constructing%20a%20mapping%20found%20unhashable%20key"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/yaml-unhashable-key"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=yaml%3A%20while%20constructing%20a%20mapping%20found%20unhashable%20key&amp;url=https%3A//death.andgravity.com/yaml-unhashable-key&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


</content>
    <link href="https://death.andgravity.com/yaml-unhashable-key" rel="alternate"/>
    <summary>... in which you'll find out what "while constructing a mapping found unhashable key" PyYAML errors mean, why do they happen, and what you can do about it.</summary>
    <published>2023-02-21T23:02:21+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/caching-methods">
    <id>https://death.andgravity.com/caching-methods</id>
    <title>Caching a lot of methods in Python</title>
    <updated>2023-02-13T20:00:00+00:00</updated>
    <content type="html">&lt;p&gt;So,
you're using a &lt;strong&gt;Python API client&lt;/strong&gt; to get a bunch of data,
and you need to &lt;strong&gt;cache results&lt;/strong&gt; and &lt;strong&gt;retry on errors&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;You could write a wrapper class,
but that's just &lt;strong&gt;one more thing to test and maintain&lt;/strong&gt;.&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Thankfully, this is Python,
and more often than not,
&lt;strong&gt;there must be a better way&lt;/strong&gt;.&lt;/p&gt;
&lt;details class="toc" open&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#but-i-don-t-own-the-client-code"&gt;But I don't own the client code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#but-decorating-methods-keeps-instances-around-for-too-long"&gt;But decorating methods keeps instances around for too long&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#but-i-need-to-cache-a-lot-of-methods"&gt;But I need to cache a lot of methods&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#but-i-don-t-instantiate-the-client"&gt;But I don't instantiate the client&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#but-the-proxy-breaks-completion"&gt;But the proxy breaks completion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#but-the-proxy-fails-isinstance-checks"&gt;But the proxy fails isinstance() checks&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#but-i-only-need-to-cache-a-few-methods"&gt;But I only need to cache a few methods&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;In this article, I will focus on caching
using &lt;a class="external" href="https://docs.python.org/3/library/functools.html#functools.lru_cache"&gt;functools.lru_cache()&lt;/a&gt;.
Retrying with a library like &lt;a class="external" href="https://tenacity.readthedocs.io/"&gt;tenacity&lt;/a&gt;
would work more or less the same.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="but-i-don-t-own-the-client-code"&gt;But I don't own the client code&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-don-t-own-the-client-code" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;We'll assume a client that looks like this
(but keep in mind it's not our code – we can only use it, not change it):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Client.one(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;two&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Client.two(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If Client were our code,
we could just slap &lt;code&gt;@functools.lru_cache&lt;/code&gt; on the methods.&lt;/p&gt;
&lt;p&gt;As is, we can subclass it,
override each method, and decorate the overrides:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;SubclassOverride&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="nd"&gt;@functools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lru_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="nd"&gt;@functools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lru_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;two&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;two&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Of course, this doesn't really help with maintainability,
but it does give us an excuse to look at a potential issue with &lt;a class="external" href="https://docs.python.org/3/library/functools.html#functools.lru_cache"&gt;lru_cache()&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="but-decorating-methods-keeps-instances-around-for-too-long"&gt;But decorating methods keeps instances around for too long&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-decorating-methods-keeps-instances-around-for-too-long" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;a class="external" href="https://docs.python.org/3/library/functools.html#functools.lru_cache"&gt;lru_cache()&lt;/a&gt; docs say,&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;If a method is cached, the &lt;code&gt;self&lt;/code&gt; instance argument is included in the cache. See &lt;a class="external" href="https://docs.python.org/3/faq/programming.html#faq-cache-method-calls"&gt;How do I cache method calls?&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;... in turn:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The &lt;em&gt;lru_cache&lt;/em&gt; approach [...] creates a reference to the instance [...] The disadvantage is that instances are kept alive until they age out of the cache or until the cache is cleared.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Depending on how often Client is instantiated,
this &lt;em&gt;might&lt;/em&gt; count as a memory and/or resource leak.
For a more detailed explanation, see &lt;a class="external" href="https://rednafi.github.io/reflections/dont-wrap-instance-methods-with-functoolslru_cache-decorator-in-python.html" title="Don't wrap instance methods with 'functools.lru_cache' decorator in Python"&gt;this article&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;One solution is to decorate the &lt;em&gt;bound&lt;/em&gt; method,
&lt;em&gt;after&lt;/em&gt; the object is created:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;SubclassShadow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lru_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;two&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lru_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;two&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This way, the instance is not in the cache keys,
and we have one cache per instance.&lt;/p&gt;
&lt;p&gt;There's still a reference cycle between the cache and the instance
(via the bound method),
but the garbage collector will take care of that
– normally, all three go away at the same time.
Before, the cache had the same lifetime as the &lt;em&gt;unbound&lt;/em&gt; method
(that is, the same lifetime as the class).&lt;/p&gt;
&lt;section class="admonition important"&gt;
&lt;p class="admonition-title"&gt;Important&lt;/p&gt;
&lt;p&gt;You might be tempted to say,
&amp;quot;but if I call &lt;code&gt;one(arg)&lt;/code&gt; on two different instances,
the underlying method will be called twice&amp;quot;.&lt;/p&gt;
&lt;p&gt;True, but this is the opposite of a problem,
it's likely &lt;em&gt;the correct behavior&lt;/em&gt;.
Because each instance has different state,
the results of the calls may be different
(it helps to think of instance attributes
as &lt;a class="internal" href="/same-arguments"&gt;implicit arguments passed to each method&lt;/a&gt;);
for example, clients instantiated with different users
should not return the same thing.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="but-i-need-to-cache-a-lot-of-methods"&gt;But I need to cache a lot of methods&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-need-to-cache-a-lot-of-methods" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;OK, the above is a bit less verbose,
but we still have to decorate methods one by one.&lt;/p&gt;
&lt;p&gt;Thankfully, we can do it on the fly:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;SubclassDynamic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__getattribute__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# conveniently, also prevents infinite recursion&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;_&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__dict__&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;callable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lru_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__dict__&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/reference/datamodel.html#object.__getattribute__"&gt;__getattribute__()&lt;/a&gt; allows us to intercept
all attribute accesses (including methods).
When a method is requested,
we get it from the parent,
decorate it,
and store it in the instance dictionary;
the second time around,
we return the already decorated method.&lt;/p&gt;
&lt;p&gt;We don't decorate methods whose names start with an underscore,
including any &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-magic-method"&gt;&lt;code&gt;__magic__&lt;/code&gt;&lt;/a&gt; or &lt;code&gt;_private&lt;/code&gt; ones;
additional filtering logic
(e.g. only cache methods starting with &lt;code&gt;get_&lt;/code&gt;)
would go here as well.&lt;/p&gt;
&lt;p&gt;Note we can't use &lt;a class="external" href="https://docs.python.org/3/reference/datamodel.html#object.__getattr__"&gt;__getattr__()&lt;/a&gt;,
because it's only called
if the attribute is not found through the normal mechanism –
and looking up the class tree &lt;em&gt;is&lt;/em&gt; part of the normal mechanism.&lt;/p&gt;
&lt;p&gt;We could just decorate all the methods upfront, in  &lt;code&gt;__init__()&lt;/code&gt;,
but that might not work if the parent does magic stuff
without implementing &lt;a class="external" href="https://docs.python.org/3/reference/datamodel.html#object.__dir__"&gt;__dir__()&lt;/a&gt; properly.&lt;/p&gt;
&lt;h2 id="but-i-don-t-instantiate-the-client"&gt;But I don't instantiate the client&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-don-t-instantiate-the-client" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Sometimes, you're not the one instantiating the client;
it may be created by a &lt;a class="external" href="https://python-patterns.guide/gang-of-four/abstract-factory/#the-pythonic-approach-callable-factories"&gt;callable factory&lt;/a&gt;
that doesn't allow passing in a different class,&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;
or you might just get it from a framework.&lt;/p&gt;
&lt;p&gt;No worries, subclassing is not required for Python meta­programming –
instead,
we can wrap the instance in an object proxy
(also known as &lt;a class="external" href="https://python-patterns.guide/gang-of-four/decorator-pattern/#implementing-dynamic-wrapper"&gt;dynamic wrapper&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;The code looks pretty much the same,
but instead of delegating to the parent,
we delegate to the &lt;em&gt;wrapped&lt;/em&gt; instance:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Proxy&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wrapped&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__wrapped__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;wrapped&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__getattr__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__wrapped__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;_&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;callable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lru_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="nb"&gt;setattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Because the proxy is a separate object,
now we &lt;em&gt;can&lt;/em&gt; use &lt;a class="external" href="https://docs.python.org/3/reference/datamodel.html#object.__getattr__"&gt;__getattr__()&lt;/a&gt;,
and we don't have to check the instance dictionary explicitly –
on the second call, the decorated method is already there,
so &lt;a class="external" href="https://docs.python.org/3/reference/datamodel.html#object.__getattr__"&gt;__getattr__()&lt;/a&gt; isn't called anymore.&lt;/p&gt;
&lt;h2 id="but-the-proxy-breaks-completion"&gt;But the proxy breaks completion&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-the-proxy-breaks-completion" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Oops, we're guilty of the not implementing &lt;a class="external" href="https://docs.python.org/3/reference/datamodel.html#object.__dir__"&gt;__dir__()&lt;/a&gt; mentioned before.&lt;/p&gt;
&lt;p&gt;Among others, this breaks completion:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;False&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__wrapped__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;True&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&amp;lt;&lt;/span&gt;&lt;span class="n"&gt;TAB&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;span class="go"&gt;           ... crickets ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... but it's pretty easy to fix:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__dir__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__wrapped__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Proxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With this, we have a solution that covers almost everything I've met in practice.&lt;/p&gt;
&lt;h2 id="but-the-proxy-fails-isinstance-checks"&gt;But the proxy fails isinstance() checks&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-the-proxy-fails-isinstance-checks" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Turns out, &lt;a class="external" href="https://python-patterns.guide/gang-of-four/decorator-pattern/#caveat-wrapping-doesnt-actually-work"&gt;writing a perfect proxy is pretty difficult&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;For example, although not recommended,
code may need to do &lt;a class="external" href="https://docs.python.org/3/library/functions.html#isinstance"&gt;isinstance()&lt;/a&gt; checks:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We could fix this by implementing &lt;a class="external" href="https://docs.python.org/3/reference/datamodel.html#class.__instancecheck__"&gt;__instancecheck__()&lt;/a&gt;,
but I thought we were past wrapping things one by one.
&lt;a class="external" href="https://wrapt.readthedocs.io/en/latest/wrappers.html#object-proxy"&gt;wrapt&lt;/a&gt; gives us an almost-perfect proxy:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;WraptProxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wrapt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ObjectProxy&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wrapped&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wrapped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_self_cached_ones&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__getattr__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_self_cached_ones&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;pass&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__getattr__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;_&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;callable&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lru_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_self_cached_ones&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It's not as elegant as ours,
but check this out:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;WraptProxy&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;WraptProxy at 0x106c2f5c0 for Client at 0x106c2ec10&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;__main__.Client object at 0x10d71ad90&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__class__&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;class &amp;#39;__main__.Client&amp;#39;&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;True&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;dir&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="but-i-only-need-to-cache-a-few-methods"&gt;But I only need to cache a few methods&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-only-need-to-cache-a-few-methods" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;OK, all that seems a bit excessive if you only need to cache a couple of methods.&lt;/p&gt;
&lt;p&gt;If we're the only ones using the client,
we could go back to decorating the bound methods
...except, didn't we say the instance already exists?&lt;/p&gt;
&lt;p&gt;So what? It's not like you can only set attributes in &lt;code&gt;__init__()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Client&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;cache_method_in_place&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;cached&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lru_cache&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)(&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;setattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cached&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;cache_method_in_place&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;cache_method_in_place&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;maxsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Unexpected? A bit.
Unholy? Maybe, depending where you're coming from.
But actually, no, not really –
after all, it still &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-duck-typing"&gt;walks and quacks&lt;/a&gt; like a Client,
and we've only changed &lt;em&gt;our&lt;/em&gt; instance.&lt;/p&gt;
&lt;p&gt;In the end, that's one of the reasons people like Python
–
you're free to bring in the big guns of meta­programming if you need to,
but sometimes a tiny bit of &lt;a class="external" href="https://en.wikipedia.org/wiki/Monkey_patch"&gt;monkey patching&lt;/a&gt;
will do just as well.&lt;sup class="footnote-ref" id="fnref-3"&gt;&lt;a href="#fn-3"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;!-- TODO: example from andgravity-generator --&gt;

&lt;hr /&gt;
&lt;p&gt;Anyway, that's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/caching-methods&amp;t=Caching%20a%20lot%20of%20methods%20in%20Python"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Caching%20a%20lot%20of%20methods%20in%20Python%20https%3A//death.andgravity.com/caching-methods"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/caching-methods&amp;title=Caching%20a%20lot%20of%20methods%20in%20Python"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/caching-methods"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Caching%20a%20lot%20of%20methods%20in%20Python&amp;url=https%3A//death.andgravity.com/caching-methods&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;I'm mainly dissing &lt;a class="external" href="https://python-patterns.guide/gang-of-four/decorator-pattern/#implementing-static-wrapper"&gt;static&lt;/a&gt; (1:1) wrappers here.
As a higher level of abstraction,
wrappers can be &lt;em&gt;very&lt;/em&gt; useful,
both maintainability and readability-wise;
for a great example of this, check out Raymond Hettinger's
&lt;a class="external" href="https://www.youtube.com/watch?v=wf-BqAjZb8M"&gt;Beyond PEP 8 – Best practices for beautiful intelligible code&lt;/a&gt;
talk. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;For example, &lt;a class="external" href="https://docs.python.org/3/library/sqlite3.html#sqlite3.connect"&gt;sqlite3.connect()&lt;/a&gt; does, via the &lt;em&gt;factory&lt;/em&gt; parameter;
&lt;a class="external" href="https://reader.readthedocs.io/en/stable/api.html#reader.make_reader"&gt;reader.​make_reader()&lt;/a&gt; doesn't (at least, not yet). &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-3"&gt;&lt;p&gt;Just like cats and salami,
a very small amount of monkey patching is probably fine,
though you definitely shouldn't make it a staple of your diet. &lt;a href="#fnref-3" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/caching-methods" rel="alternate"/>
    <summary>... in which you'll learn how to cache a lot of methods in Python, even for objects created by someone else. Did you know that, contrary to popular belief, Python programmers can have a little monkey patching as a treat?</summary>
    <published>2023-02-13T20:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/reader-3-4">
    <id>https://death.andgravity.com/reader-3-4</id>
    <title>reader 3.4 released – 5 years, 2000 commits</title>
    <updated>2023-01-23T18:00:00+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;I'm happy to announce version 3.4 of &lt;strong&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/strong&gt;, a Python feed reader library.&lt;/p&gt;
&lt;p&gt;More importantly, reader is now 5 years old!&lt;/p&gt;
&lt;h2 id="5-years-2000-commits"&gt;5 years, 2000 commits&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#5-years-2000-commits" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;3.3, released in December, marks &lt;em&gt;reader&lt;/em&gt;'s &lt;a class="external" href="https://github.com/lemon24/reader/commit/73ac0bd3b8d0e5429e0bd7caf5281e4c9c74f16d"&gt;5th anniversary&lt;/a&gt; and its &lt;a class="external" href="https://github.com/lemon24/reader/tree/3.3"&gt;2000th commit&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I don't really know what to say,
so I'll just say something that maybe will help others.&lt;/p&gt;
&lt;p&gt;I'm happy about &lt;em&gt;reader&lt;/em&gt;.
But more than that,
I'm proud of it &lt;em&gt;growing into&lt;/em&gt; what it is.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; started as a hobby and learning project,
scratch-your-own-itch kind of thing.
Starting off, I had two goals:
it should be a library,
and it should be for the long term.&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;It continued beyond that, into an experiment.&lt;/p&gt;
&lt;p&gt;First, on applying &lt;a class="external" href="https://en.wikipedia.org/wiki/Pieter_Hintjens"&gt;Pieter Hintjens&lt;/a&gt;' &lt;a class="external" href="https://hintjens.gitbooks.io/scalable-c/content/chapter1.html#problem-what-do-we-do-next"&gt;problem-solution&lt;/a&gt; approach,
and the ideas in the
&lt;a class="external" href="https://programmingisterrible.com/post/139222674273/write-code-that-is-easy-to-delete-not-easy-to"&gt;Write code that is easy to delete, not easy to extend&lt;/a&gt; and
&lt;a class="external" href="https://programmingisterrible.com/post/176657481103/repeat-yourself-do-more-than-one-thing-and"&gt;Repeat yourself, do more than one thing, and rewrite everything&lt;/a&gt;
essays by tef of &lt;a class="external" href="https://programmingisterrible.com/"&gt;programming is terrible&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Second, and more mundane, it was an exercise
in how testing and type checking
keep code maintainable over time.
&amp;quot;Everyone knows&amp;quot; they are good things™,
but most jobs rarely emphasize the &lt;em&gt;over time&lt;/em&gt; part –
I know mine at the time didn't.
I wanted a place where I could get an intuitive feel for it.&lt;/p&gt;
&lt;p&gt;(If you'd like to read more about any of the above, &lt;a class="internal" href="/about#contact"&gt;do let me know&lt;/a&gt;.)&lt;/p&gt;
&lt;p&gt;Judging the success of either is probably highly subjective.
Nevertheless, &lt;em&gt;I think&lt;/em&gt; they were both &lt;em&gt;wildly successful&lt;/em&gt;.
&lt;em&gt;reader&lt;/em&gt; is still alive and kicking,
despite the limited time I have for it,
and I've developed habits and ways of solving problems
that I'm constantly using in my professional life.&lt;/p&gt;
&lt;p&gt;Sure, &lt;em&gt;reader&lt;/em&gt; has flaws, and it's definitely not done (what is?),
but it's the longest project I've ever worked on,
and probably the best code I've ever written.&lt;/p&gt;
&lt;p&gt;Finally, I feel I have to say that while not hugely popular,
&lt;em&gt;reader&lt;/em&gt; is way more popular than I could've dreamt 5 years ago,
especially for a niche, but hopefully still relevant topic like web feeds.
Thank you!&lt;/p&gt;
&lt;h2 id="what-s-new"&gt;What's new?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-new" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Here are the highlights since &lt;a class="internal" href="/reader-3-0"&gt;reader 3.0&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="parser-internal-api"&gt;Parser internal API&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#parser-internal-api" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- 3.4 --&gt;

&lt;p&gt;The parser &lt;a class="external" href="https://reader.readthedocs.io/en/latest/internal.html#parser"&gt;internal API&lt;/a&gt; is fully documented.
It continues to be unstable,
but this is a huge first step on the road to internal API stabilization.&lt;/p&gt;
&lt;p&gt;I started with the parser because
it's probably most useful for plugins
(for example, the &lt;a class="internal" href="/reader-2-14#twitter-support"&gt;Twitter plugin&lt;/a&gt; uses it extensively),
or if you want to retrieve and/or parse feeds with &lt;em&gt;reader&lt;/em&gt;,
but handle storage and so on yourself
(here's a recipe for &lt;a class="external" href="https://reader.readthedocs.io/en/latest/internal.html#parsing-a-feed-retrieved-with-something-other-than-reader"&gt;parsing feeds retrieved with HTTPX&lt;/a&gt;).&lt;/p&gt;
&lt;h3 id="faster-imports"&gt;Faster imports&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#faster-imports" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- 3.3 --&gt;

&lt;p&gt;The time from process start to usable Reader instance is 3x shorter,
by postponing update-related imports until actually needed.&lt;/p&gt;
&lt;h3 id="simpler-entry-sorting"&gt;Simpler entry sorting&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#simpler-entry-sorting" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- 3.1 --&gt;

&lt;p&gt;When sorting by &lt;code&gt;recent&lt;/code&gt;,
newly added entries now always appear at the top,
regardless of their published date,
except on a feed's first update.&lt;/p&gt;
&lt;p&gt;This replaces a more complicated heuristic
that caused entries added to a feed months after their published
(yup, that happens!)
to never appear at the top (so you would never see them).&lt;/p&gt;
&lt;h3 id="fewer-dependencies"&gt;Fewer dependencies&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#fewer-dependencies" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- 3.1 --&gt;

&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; is a library, so I try to keep the number of dependencies as small as possible.&lt;/p&gt;
&lt;p&gt;Starting with 3.1,
the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/plugins.html#reader-readtime"&gt;readtime&lt;/a&gt; built-in plugin has &lt;em&gt;no dependencies&lt;/em&gt;,
and it can be used  even if lxml is not available
(the library I was using before to calculate read time
has a transitive dependency on lxml,
which does not always have PyPy Windows wheels).&lt;/p&gt;
&lt;h3 id="python-versions"&gt;Python versions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#python-versions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; 3.3 adds support for Python 3.11.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; 3.4 drops support for Python 3.8.&lt;/p&gt;
&lt;h3 id="bug-fixes"&gt;Bug fixes&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bug-fixes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;There were lots of bug fixes,
mainly to plugins (I guess that's a good thing).&lt;/p&gt;
&lt;p&gt;For more details, see the full &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-3-4"&gt;changelog&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/reader-3-4&amp;t=reader%203.4%20released%20%E2%80%93%205%20years%2C%202000%20commits"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=reader%203.4%20released%20%E2%80%93%205%20years%2C%202000%20commits%20https%3A//death.andgravity.com/reader-3-4"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/reader-3-4&amp;title=reader%203.4%20released%20%E2%80%93%205%20years%2C%202000%20commits"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/reader-3-4"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=reader%203.4%20released%20%E2%80%93%205%20years%2C%202000%20commits&amp;url=https%3A//death.andgravity.com/reader-3-4&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;h2 id="what-is-reader"&gt;What is &lt;em&gt;reader&lt;/em&gt;?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;reader&lt;/strong&gt; takes care
of the core functionality required by a feed reader,
so you can focus on what makes &lt;strong&gt;yours&lt;/strong&gt; different.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-2-0/reader.png" alt="reader in action" /&gt;
&lt;em&gt;reader&lt;/em&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retrieve, store, and manage &lt;strong&gt;Atom&lt;/strong&gt;, &lt;strong&gt;RSS&lt;/strong&gt;, and &lt;strong&gt;JSON&lt;/strong&gt; feeds&lt;ul&gt;
&lt;li&gt;and even follow &lt;strong&gt;Twitter&lt;/strong&gt; accounts&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;mark articles as read or important&lt;/li&gt;
&lt;li&gt;add arbitrary tags/metadata to feeds and articles&lt;/li&gt;
&lt;li&gt;filter feeds and articles&lt;/li&gt;
&lt;li&gt;full-text search articles&lt;/li&gt;
&lt;li&gt;get statistics on feed and user activity&lt;/li&gt;
&lt;li&gt;write plugins to extend its functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...all these with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stable, clearly documented API&lt;/li&gt;
&lt;li&gt;excellent test coverage&lt;/li&gt;
&lt;li&gt;fully typed Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find out more, check out the &lt;a class="external" href="https://github.com/lemon24/reader"&gt;GitHub repo&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/"&gt;docs&lt;/a&gt;,
or give the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/tutorial.html"&gt;tutorial&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2 id="why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-use-a-feed-reader-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Have you been unhappy with existing feed readers and wanted to make your own, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never knew where to start?&lt;/li&gt;
&lt;li&gt;it seemed like too much work?&lt;/li&gt;
&lt;li&gt;you don't like writing backend code?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you already working with &lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/"&gt;feedparser&lt;/a&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;want an easier way to store, filter, sort and search feeds and entries?&lt;/li&gt;
&lt;li&gt;want to get back type-annotated objects instead of dicts?&lt;/li&gt;
&lt;li&gt;want to restrict or deny file-system access?&lt;/li&gt;
&lt;li&gt;want to change the way feeds are retrieved by using &lt;a class="external" href="https://requests.readthedocs.io"&gt;Requests&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to also support &lt;a class="external" href="https://jsonfeed.org/"&gt;JSON Feed&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to follow &lt;a class="external" href="https://reader.readthedocs.io/en/latest/plugins.html#twitter"&gt;Twitter&lt;/a&gt; accounts?&lt;/li&gt;
&lt;li&gt;want to support custom information sources?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... while still supporting all the feed types feedparser does?&lt;/p&gt;
&lt;p&gt;If you answered yes to any of the above, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;h2 id="why-make-your-own-feed-reader"&gt;Why make your own feed reader?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-make-your-own-feed-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;have full control over your data&lt;/li&gt;
&lt;li&gt;control what features it has or doesn't have&lt;/li&gt;
&lt;li&gt;decide how much you pay for it&lt;/li&gt;
&lt;li&gt;make sure it doesn't get closed while you're still using it&lt;/li&gt;
&lt;li&gt;really, it's &lt;a class="external" href="https://rachelbythebay.com/w/2011/10/26/fred/"&gt;easier than you think&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously, this may not be your cup of tea, but if it is, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;A bit more on this &lt;a class="internal" href="/own-query-builder#background"&gt;here&lt;/a&gt;. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/reader-3-4" rel="alternate"/>
    <summary>reader, my Python feed reader library, is 5 years old; since this is a special occasion, I thought I'd share a few thoughts on the journey so far.</summary>
    <published>2023-01-23T18:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/pwned">
    <id>https://death.andgravity.com/pwned</id>
    <title>Has your password been pwned? Or, how I almost failed to search a 37 GB text file in under 1 millisecond (in Python)</title>
    <updated>2022-12-15T09:42:01+00:00</updated>
    <content type="html">&lt;p&gt;So, there's this website, &lt;a class="external" href="https://haveibeenpwned.com/"&gt;Have I Been Pwned&lt;/a&gt;,
where you can check if your email address has appeared in a data breach.&lt;/p&gt;
&lt;p&gt;There's also a &lt;a class="external" href="https://haveibeenpwned.com/Passwords"&gt;Pwned Passwords&lt;/a&gt; section for passwords
... but, typing your password on a random website
probably isn't such a great idea, right?&lt;/p&gt;
&lt;p&gt;Of course, you could
read about &lt;a class="external" href="https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2/"&gt;how HIBP protects the privacy of searched passwords&lt;/a&gt;,
and understand how &lt;a class="external" href="https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2/#cloudflareprivacyandkanonymity"&gt;k-Anonymity&lt;/a&gt; works,
and check that the website uses the &lt;a class="external" href="https://haveibeenpwned.com/API/v3#PwnedPasswords"&gt;k-Anonymity API&lt;/a&gt;,
but that's waaay too much work.&lt;/p&gt;
&lt;p&gt;Of course, you could use the API yourself,
but where's the fun in that?&lt;/p&gt;
&lt;p&gt;Instead, we'll do it the hard way –
we'll check passwords &lt;strong&gt;offline&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;And we're not stopping until it's &lt;strong&gt;fast&lt;/strong&gt;.&lt;/p&gt;
&lt;details class="toc" open&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#the-pwned-passwords-list"&gt;The Pwned Passwords list&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#a-minimal-plausible-solution"&gt;A minimal plausible solution&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#problem-it-s-slow"&gt;Problem: it's slow&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#skipping"&gt;Skipping&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#problem-it-needs-tuning-it-s-still-slow"&gt;Problem: it needs tuning, it's still slow&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#binary-skipping"&gt;Binary skipping&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#better-timing"&gt;Better timing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#failing-to-get-to-under-1-millisecond"&gt;Failing to get to under 1 millisecond&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#profile-before-optimizing-profile"&gt;Profile before optimizing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#position-heuristic"&gt;Position heuristic&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#index-file"&gt;Index file&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#binary-file"&gt;Binary file&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#getting-to-under-1-millisecond"&gt;Getting to under 1 millisecond&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#generating-the-index"&gt;Generating the index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#using-the-index"&gt;Using the index&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#i-heard-you-like-indexes-the-end"&gt;I heard you like indexes (the end)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#bonus-better-data-structures"&gt;Bonus: better data structures&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="the-pwned-passwords-list"&gt;The Pwned Passwords list&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-pwned-passwords-list" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;OK, first we need the password list.&lt;/p&gt;
&lt;p&gt;Go to &lt;a class="external" href="https://haveibeenpwned.com/Passwords"&gt;Pwned Passwords&lt;/a&gt;,
scroll to &lt;em&gt;Downloading the Pwned Passwords list&lt;/em&gt;,
and download the SHA-1 ordered-by-hash file –
be nice and use the torrent, if you can.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;You can also use the &lt;a class="external" href="https://www.troyhunt.com/downloading-pwned-passwords-hashes-with-the-hibp-downloader/"&gt;downloader&lt;/a&gt; to get an updated version of the file,
but that didn't exist when I started writing this.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;The archive extracts to a 37 GB text file:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;pushd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;~/Downloads
&lt;span class="gp"&gt;$ &lt;/span&gt;stat&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;%z&lt;span class="w"&gt; &lt;/span&gt;pwned-passwords-sha1-ordered-by-hash-v8.7z
&lt;span class="go"&gt;16257755606&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;7zz&lt;span class="w"&gt; &lt;/span&gt;e&lt;span class="w"&gt; &lt;/span&gt;pwned-passwords-sha1-ordered-by-hash-v8.7z
&lt;span class="gp"&gt;$ &lt;/span&gt;stat&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="w"&gt; &lt;/span&gt;%z&lt;span class="w"&gt; &lt;/span&gt;pwned-passwords-sha1-ordered-by-hash-v8.txt
&lt;span class="go"&gt;37342268646&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;popd&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... that looks like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;head&lt;span class="w"&gt; &lt;/span&gt;-n5&lt;span class="w"&gt; &lt;/span&gt;~/Downloads/pwned-passwords-sha1-ordered-by-hash-v8.txt
&lt;span class="go"&gt;000000005AD76BD555C1D6D771DE417A4B87E4B4:10&lt;/span&gt;
&lt;span class="go"&gt;00000000A8DAE4228F821FB418F59826079BF368:4&lt;/span&gt;
&lt;span class="go"&gt;00000000DD7F2A1C68A35673713783CA390C9E93:873&lt;/span&gt;
&lt;span class="go"&gt;00000001E225B908BAC31C56DB04D892E47536E0:6&lt;/span&gt;
&lt;span class="go"&gt;00000006BAB7FC3113AA73DE3589630FC08218E7:3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each line has the format:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;&amp;lt;SHA-1 of the password&amp;gt;:&amp;lt;times it appeared in various breaches&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;For more details:
&lt;a class="external" href="https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2/#theyrestillsha1hashedbutwithsomejunkremoved"&gt;why hash the passwords&lt;/a&gt;, and
&lt;a class="external" href="https://www.troyhunt.com/ive-just-launched-pwned-passwords-version-2/#eachpasswordnowhasacountnexttoit"&gt;why include a count&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;To make commands shorter, we link the file in the current directory:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;ln&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;~/Downloads/pwned-passwords-sha1-ordered-by-hash-v8.txt&lt;span class="w"&gt; &lt;/span&gt;pwned.txt
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="a-minimal-plausible-solution"&gt;A minimal plausible solution&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#a-minimal-plausible-solution" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;We'll take an iterative, &lt;a class="external" href="https://hintjens.gitbooks.io/scalable-c/content/chapter1.html#problem-what-do-we-do-next"&gt;problem-solution&lt;/a&gt; approach to this.
But since right now we don't have &lt;em&gt;any&lt;/em&gt; solution,
we start with &lt;a class="external" href="https://wiki.c2.com/?DoTheSimplestThingThatCouldPossiblyWork"&gt;the simplest thing that could possibly work&lt;/a&gt;.&lt;/p&gt;
&lt;!-- ## Imports --&gt;

&lt;p&gt;First, we get the imports out of the way:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;1&lt;/span&gt;
&lt;span class="normal"&gt;2&lt;/span&gt;
&lt;span class="normal"&gt;3&lt;/span&gt;
&lt;span class="normal"&gt;4&lt;/span&gt;
&lt;span class="normal"&gt;5&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="IPython"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;getpass&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;hashlib&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;!-- ## File --&gt;

&lt;p&gt;Then, we open the file:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;7&lt;/span&gt;
&lt;span class="normal"&gt;8&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rb&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Opening the file in binary mode makes searching a bit faster,
as it skips needless decoding.
By opening it early,
we get free error checking
– no point in reading the password if the file isn't there.&lt;/p&gt;
&lt;!-- ## Password --&gt;

&lt;p&gt;Next, the password:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;IndexError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getpass&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getpass&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;hexdigest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;looking for&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We either take the password as the second argument to the script,
or read it with &lt;a class="external" href="https://docs.python.org/3/library/getpass.html#getpass.getpass"&gt;getpass()&lt;/a&gt;,
so real passwords don't remain in the shell history.
After computing its hash, we delete it;
we don't go as far as to &lt;a class="external" href="https://en.wikipedia.org/wiki/Zeroisation#Software"&gt;zero&lt;/a&gt;‍&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt; it,
but at least we avoid accidental printing.&lt;/p&gt;
&lt;p&gt;Let's see if it works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;pwned.py&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="w"&gt; &lt;/span&gt;password
&lt;span class="go"&gt;looking for 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;pwned.py&lt;span class="w"&gt; &lt;/span&gt;pwned.txt
&lt;span class="go"&gt;Password:&lt;/span&gt;
&lt;span class="go"&gt;looking for 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;sha1sum&lt;/code&gt; seems to agree:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;password&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sha1sum
&lt;span class="go"&gt;5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8  -&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;!-- ## Finding the hash --&gt;

&lt;p&gt;To find the hash, we go through the file line by line –
remember, simplest thing that could possibly work.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;find_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If a line was found, we print the count:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hexdigest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;times&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pwned! seen &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;times&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; times before&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;not found&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;!-- ## Timing stuff --&gt;

&lt;p&gt;Finally, let's add some timing code:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hexdigest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;39&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.6f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; seconds&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And, it works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;pwned.py&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="w"&gt; &lt;/span&gt;blocking
&lt;span class="go"&gt;looking for 000085013a02852372159cb94101b99ccaec59e1&lt;/span&gt;
&lt;span class="go"&gt;pwned! seen 587 times before&lt;/span&gt;
&lt;span class="go"&gt;in 0.002070 seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a class="attachment" href="/_file/pwned/00-linear.py"&gt;The code so far.&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="problem-it-s-slow"&gt;Problem: it's slow&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-it-s-slow" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;You may have noticed I switched from &lt;code&gt;password&lt;/code&gt; to &lt;code&gt;blocking&lt;/code&gt;.
That's because I deliberately
chose a password whose hash is at the beginning of the file.&lt;/p&gt;
&lt;p&gt;On my 2013 laptop, &lt;code&gt;password&lt;/code&gt; actually takes &lt;strong&gt;86 seconds&lt;/strong&gt;!&lt;/p&gt;
&lt;p&gt;To put a lower bound on the time it takes to go through the file:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;for line in open(&amp;quot;pwned.txt&amp;quot;, &amp;quot;rb&amp;quot;): pass&amp;#39;&lt;/span&gt;

&lt;span class="go"&gt;real	1m28.325s&lt;/span&gt;
&lt;span class="go"&gt;user	1m10.049s&lt;/span&gt;
&lt;span class="go"&gt;sys	0m15.577s&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;wc&lt;span class="w"&gt; &lt;/span&gt;-l&lt;span class="w"&gt; &lt;/span&gt;pwned.txt
&lt;span class="go"&gt; 847223402 pwned.txt&lt;/span&gt;

&lt;span class="go"&gt;real	0m51.729s&lt;/span&gt;
&lt;span class="go"&gt;user	0m27.908s&lt;/span&gt;
&lt;span class="go"&gt;sys	0m12.119s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;a class="external" href="https://www.youtube.com/watch?v=UANN2Eu6ZnM&amp;amp;t=438s"&gt;There must be a better way.&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="skipping"&gt;Skipping&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#skipping" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;There's a hint in the file name: the hashes are ordered.&lt;/p&gt;
&lt;p&gt;That means we don't &lt;em&gt;have&lt;/em&gt; to check all the lines.
We can skip ahead repeatedly until we're past the hash,
go back one step, and only check each line from there.&lt;/p&gt;
&lt;p&gt;Lines are different lengths,
so we can't skip exactly X lines without reading them.
But we don't need to,
any line that's a reasonable amount ahead will do.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;skip_to_before_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;old_position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tell&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SEEK_CUR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="c1"&gt;# print(&amp;quot;jumped to&amp;quot;, (line or b&amp;#39;&amp;lt;eof&amp;gt;&amp;#39;).decode().rstrip())&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old_position&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;

        &lt;span class="n"&gt;old_position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tell&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;So we just &lt;a class="external" href="https://docs.python.org/3/library/io.html#io.IOBase.seek"&gt;seek()&lt;/a&gt; ahead a set number of bytes.
Since that might not leave us at the start of a line,
we discard the incomplete line,
and use the next one.&lt;/p&gt;
&lt;!--
If we can still skip ahead,
we remember the [current position][tell],
so we can return to it if needed.
--&gt;

&lt;p&gt;Finally, we wrap the original &lt;code&gt;find_line()&lt;/code&gt; to do the skipping:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;find_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;skip_to_before_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;find_line_linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="hll"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;find_line_linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;pwned.py&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="w"&gt; &lt;/span&gt;password
&lt;span class="go"&gt;looking for 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8&lt;/span&gt;
&lt;span class="go"&gt;pwned! seen 9,545,824 times before&lt;/span&gt;
&lt;span class="go"&gt;in 0.027203 seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To find the magic 16M offset, I had to try a bunch values:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;offset (MiB)   time (s)
           1      0.05
           4      0.035
           8      0.030
          16      0.027  &amp;lt;-- sweet spot
          32      0.14
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a class="attachment" href="/_file/pwned/01-linear-skip.py"&gt;The code so far.&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="problem-it-needs-tuning-it-s-still-slow"&gt;Problem: it needs tuning, it's still slow&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-it-needs-tuning-it-s-still-slow" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;While three orders of magnitude faster, we still have a bunch of issues:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The ideal offset depends on the computer you're running this on.&lt;/li&gt;
&lt;li&gt;The run time still increases linearly with file size –
we haven't really solved the problem,
as much as made it smaller by a (large, but) constant factor.&lt;/li&gt;
&lt;li&gt;The run time still increases linearly with where the hash is in the file.&lt;/li&gt;
&lt;li&gt;It's still kinda slow. ¯\_(ツ)_/¯&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;&lt;a class="external" href="https://www.youtube.com/watch?v=OSGv2VnC0go&amp;amp;t=557s"&gt;There must be a better way.&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="binary-skipping"&gt;Binary skipping&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#binary-skipping" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;To make the &amp;quot;linear&amp;quot; part painfully obvious,
uncomment the &lt;code&gt;jumped to&lt;/code&gt; line.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;pwned.py&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="w"&gt; &lt;/span&gt;password&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;jumped to .&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uniq&lt;span class="w"&gt; &lt;/span&gt;-c
&lt;span class="go"&gt; 139 jumped to 0&lt;/span&gt;
&lt;span class="go"&gt; 139 jumped to 1&lt;/span&gt;
&lt;span class="go"&gt; 139 jumped to 2&lt;/span&gt;
&lt;span class="go"&gt; 139 jumped to 3&lt;/span&gt;
&lt;span class="go"&gt; 139 jumped to 4&lt;/span&gt;
&lt;span class="go"&gt; 103 jumped to 5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Surely, after we've jumped to &lt;code&gt;0&lt;/code&gt; once,
we don't &lt;em&gt;need&lt;/em&gt; to do it 138 more times, right?&lt;/p&gt;
&lt;p&gt;We could jump directly to a line in the middle of the file;
if there at all,
the hash will be in either of the halves.
We could then jump to the middle of that half,
and to the middle of &lt;em&gt;that&lt;/em&gt; half,
and so on,
until we either find the hash
or there's nowhere left to jump.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;If that sounds a lot like &lt;a class="external" href="https://en.wikipedia.org/wiki/Binary_search_algorithm"&gt;binary search&lt;/a&gt;, that's because it is
– it's just not wearing its regular array clothing.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;And most of the work is already done:
we can jump to a line at most X bytes from where the hash should be,
we only need to do it repeatedly,
in smaller and smaller fractions of the file size:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;skip_to_before_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;//=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
        &lt;span class="n"&gt;skip_to_before_line_linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="hll"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;skip_to_before_line_linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;    &lt;span class="n"&gt;old_position&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tell&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The only thing left is to get the file size:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;find_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SEEK_END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tell&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;skip_to_before_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;find_line_linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;!--

.. note:: Fun fact

    I initially used `os.stat(path).st_size` for this,
    and passing `file.fileno()` instead of the path would've worked too.

    But, the `seek(0, os.SEEK_END)` + `tell()` trick I discovered
    by trying to get [ChatGPT] to write the code for this article
    from a description of the problem alone
    (it got as far as the linear solution!).

    I swear everything else in this article was generated by a human.

    ...

    Written. Written by a human.

    [ChatGPT]: https://en.wikipedia.org/wiki/ChatGPT

--&gt;

&lt;p&gt;It works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;pwned.py&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="w"&gt; &lt;/span&gt;password
&lt;span class="go"&gt;looking for 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8&lt;/span&gt;
&lt;span class="go"&gt;pwned! seen 9,545,824 times before&lt;/span&gt;
&lt;span class="go"&gt;in 0.009559 seconds&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;pwned.py&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="w"&gt; &lt;/span&gt;password
&lt;span class="go"&gt;looking for 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8&lt;/span&gt;
&lt;span class="go"&gt;pwned! seen 9,545,824 times before&lt;/span&gt;
&lt;span class="go"&gt;in 0.000268 seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The huge time difference is due to &lt;a class="external" href="https://en.wikipedia.org/wiki/Page_cache"&gt;operating system and/or disk caches&lt;/a&gt;
– on the second run, the same parts of the file are likely already in memory.&lt;/p&gt;
&lt;p&gt;Anyway, look again at the &lt;code&gt;jumped to&lt;/code&gt; output:
instead of jumping blindly through the whole file,
now we're jumping &lt;em&gt;around&lt;/em&gt; the hash,
getting closer and closer to it.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;pwned.py&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="w"&gt; &lt;/span&gt;password&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;jumped to .&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uniq&lt;span class="w"&gt; &lt;/span&gt;-c
&lt;span class="go"&gt;   1 jumped to 7&lt;/span&gt;
&lt;span class="go"&gt;   1 jumped to 3&lt;/span&gt;
&lt;span class="go"&gt;   1 jumped to 7&lt;/span&gt;
&lt;span class="go"&gt;   1 jumped to 5&lt;/span&gt;
&lt;span class="go"&gt;   1 jumped to 4&lt;/span&gt;
&lt;span class="go"&gt;  39 jumped to 5&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;pwned.py&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="w"&gt; &lt;/span&gt;password&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;jumped to ..&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uniq&lt;span class="w"&gt; &lt;/span&gt;-c
&lt;span class="go"&gt;   1 jumped to 7F&lt;/span&gt;
&lt;span class="go"&gt;   1 jumped to 3F&lt;/span&gt;
&lt;span class="go"&gt;   1 jumped to 7F&lt;/span&gt;
&lt;span class="go"&gt;   1 jumped to 5F&lt;/span&gt;
&lt;span class="go"&gt;   1 jumped to 4F&lt;/span&gt;
&lt;span class="go"&gt;   1 jumped to 5F&lt;/span&gt;
&lt;span class="go"&gt;   1 jumped to 57&lt;/span&gt;
&lt;span class="go"&gt;   1 jumped to 5F&lt;/span&gt;
&lt;span class="go"&gt;   1 jumped to 5B&lt;/span&gt;
&lt;span class="go"&gt;   1 jumped to 59&lt;/span&gt;
&lt;span class="go"&gt;   1 jumped to 5B&lt;/span&gt;
&lt;span class="go"&gt;   1 jumped to 5A&lt;/span&gt;
&lt;span class="go"&gt;  32 jumped to 5B&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Notice we land on the same &lt;code&gt;7F...&lt;/code&gt; prefix twice;
this makes sense –
we skip ahead by half the file size,
then back,
then ahead two times by a quarter.
Caching allows us to not care about that.&lt;/p&gt;
&lt;p&gt;&lt;a class="attachment" href="/_file/pwned/02-binary-search.py"&gt;The code so far.&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="better-timing"&gt;Better timing&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#better-timing" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Given the way caching muddies the waters,
how fast is it &lt;em&gt;really?&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;This function averages a hundred runs,
each with a different password:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;average-many&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;_&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;..100&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;import time; print(time.time())&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;seconds&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cut&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-f2&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;paste&lt;span class="w"&gt; &lt;/span&gt;-sd+&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s#^#scale=6;(#&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sed&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;s#$#)/100#&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bc
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;details&gt;
&lt;summary&gt;
After a few repeats, the average time settles around 3 ms.
&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;_&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;..20&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;average-many&lt;span class="w"&gt; &lt;/span&gt;pwned.py&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;span class="go"&gt;.004802&lt;/span&gt;
&lt;span class="go"&gt;.003904&lt;/span&gt;
&lt;span class="go"&gt;.004088&lt;/span&gt;
&lt;span class="go"&gt;.003486&lt;/span&gt;
&lt;span class="go"&gt;.003451&lt;/span&gt;
&lt;span class="go"&gt;.003476&lt;/span&gt;
&lt;span class="go"&gt;.003414&lt;/span&gt;
&lt;span class="go"&gt;.003442&lt;/span&gt;
&lt;span class="go"&gt;.003169&lt;/span&gt;
&lt;span class="go"&gt;.003297&lt;/span&gt;
&lt;span class="go"&gt;.002931&lt;/span&gt;
&lt;span class="go"&gt;.003077&lt;/span&gt;
&lt;span class="go"&gt;.003092&lt;/span&gt;
&lt;span class="go"&gt;.003011&lt;/span&gt;
&lt;span class="go"&gt;.002980&lt;/span&gt;
&lt;span class="go"&gt;.003147&lt;/span&gt;
&lt;span class="go"&gt;.003112&lt;/span&gt;
&lt;span class="go"&gt;.002942&lt;/span&gt;
&lt;span class="go"&gt;.002984&lt;/span&gt;
&lt;span class="go"&gt;.002934&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;Again, this is due to caching:
the more we run the script,
the more likely it is that
the pages at {half, quarters, eights, sixteenths, ...} of the file size
are already in memory
– and the path to any line starts with a subset of those.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;And, we're done.&lt;/p&gt;
&lt;p&gt;&lt;a class="external" href="https://qntm.org/destro"&gt;I wave my hands&lt;/a&gt;, get a 2020 laptop, and a miracle happens.
It's far enough into the totally unpredictable future, now,
that you can search any password in under 1 millisecond.
You can do anything you want.&lt;/p&gt;
&lt;!--
I wave my hands, and a miracle happens.
It's far enough into the totally unpredictable future, now,
that you can completely destroy a rocky planet.
You can do anything you want.
--&gt;

&lt;p&gt;&lt;a class="external" href="https://youtu.be/0twDETh6QaI?t=1787"&gt;So, there we go.&lt;/a&gt;
Wasn't that an interesting story?
That's the end of the article.&lt;/p&gt;
&lt;p&gt;Don't look at the scroll bar.
Don't worry about it.&lt;/p&gt;
&lt;p&gt;If you came here to
check your password,
you can go.&lt;/p&gt;
&lt;!--
If you came here to
find a somewhat inconvenient way of checking if your password has been compromised,
you can go.
--&gt;

&lt;p&gt;Subscribe on the way out if you'd like, but take care :)&lt;/p&gt;
&lt;p&gt;Go solve &lt;a class="internal" href="/conway-cubes"&gt;Advent of Code&lt;/a&gt; or something.&lt;/p&gt;
&lt;p&gt;I'm just chilling out.&lt;/p&gt;
&lt;!--
I'll go chill out.
--&gt;

&lt;p&gt;See ya.&lt;/p&gt;

&lt;h2 id="failing-to-get-to-under-1-millisecond"&gt;Failing to get to under 1 millisecond&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#failing-to-get-to-under-1-millisecond" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I swear this was supposed to be the end.
This really was supposed to be a short one.&lt;/p&gt;
&lt;p&gt;Here's a quote from a friend of mine,
that chronologically should be way later into the article,
but makes a great summary for what follows:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;And now that you have arrived at this point,
spend a moment to ponder the arbitrary nature of 1 millisecond
given its dependency on the current year
and the choice of your particular hardware.&lt;/p&gt;
&lt;p&gt;After that moment, continue celebrating.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Nah, fuck it, it has to take less than 1 millisecond &lt;strong&gt;on the old laptop&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;... so yeah, here's a bunch of stuff that didn't work.&lt;/p&gt;



&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        Liking this so far? Here&amp;#39;s another article you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/stdlib"&gt;
            Learn by reading code: Python standard library design decisions explained
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="profile-before-optimizing-profile"&gt;&lt;a class="external" href="http://wiki.c2.com/?ProfileBeforeOptimizing"&gt;Profile before optimizing&lt;/a&gt;&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#profile-before-optimizing-profile" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;With the obvious improvements out of the way,
it's probably a good time to stop and
&lt;a class="internal" href="/fast-conway-cubes#intro-to-profiling"&gt;find out where time is being spent&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;cProfile&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;cumulative&lt;span class="w"&gt; &lt;/span&gt;pwned.py&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;date&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;looking for 3960626a8c59fe927d3cf2e991d67f4c505ae198&lt;/span&gt;
&lt;span class="go"&gt;not found&lt;/span&gt;
&lt;span class="go"&gt;in 0.004902 seconds&lt;/span&gt;
&lt;span class="go"&gt;         1631 function calls (1614 primitive calls) in 0.010 seconds&lt;/span&gt;

&lt;span class="go"&gt;   Ordered by: cumulative time&lt;/span&gt;

&lt;span class="go"&gt;   ncalls  tottime  percall  cumtime  percall filename:lineno(function)&lt;/span&gt;
&lt;span class="go"&gt;      ...&lt;/span&gt;
&lt;span class="go"&gt;        1    0.000    0.000    0.005    0.005 02-binary-search.py:8(find_line)&lt;/span&gt;
&lt;span class="go"&gt;        1    0.000    0.000    0.005    0.005 02-binary-search.py:22(skip_to_before_line)&lt;/span&gt;
&lt;span class="go"&gt;       28    0.000    0.000    0.005    0.000 02-binary-search.py:28(skip_to_before_line_linear)&lt;/span&gt;
&lt;span class="go"&gt;       86    0.004    0.000    0.004    0.000 {method &amp;#39;readline&amp;#39; of &amp;#39;_io.BufferedReader&amp;#39; objects}&lt;/span&gt;
&lt;span class="go"&gt;      ...&lt;/span&gt;
&lt;span class="go"&gt;       71    0.000    0.000    0.000    0.000 {method &amp;#39;seek&amp;#39; of &amp;#39;_io.BufferedReader&amp;#39; objects}&lt;/span&gt;
&lt;span class="go"&gt;      ...&lt;/span&gt;
&lt;span class="go"&gt;       44    0.000    0.000    0.000    0.000 {method &amp;#39;tell&amp;#39; of &amp;#39;_io.BufferedReader&amp;#39; objects}&lt;/span&gt;
&lt;span class="go"&gt;      ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;From the output above, we learn that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Most of the time is spent in &lt;a class="external" href="https://docs.python.org/3/library/io.html#io.IOBase.readline"&gt;readline()&lt;/a&gt; calls.&lt;/li&gt;
&lt;li&gt;Both &lt;a class="external" href="https://docs.python.org/3/library/io.html#io.IOBase.seek"&gt;seek()&lt;/a&gt; and &lt;a class="external" href="https://docs.python.org/3/library/io.html#io.IOBase.tell"&gt;tell()&lt;/a&gt; calls are basically free.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/library/io.html#io.IOBase.readline"&gt;readline()&lt;/a&gt; is &lt;a class="external" href="https://github.com/python/cpython/blob/3.10/Modules/_io/bufferedio.c#L1070-L1178"&gt;implemented in C&lt;/a&gt;,
so there's not much we can change there.&lt;/p&gt;
&lt;p&gt;What we &lt;em&gt;can&lt;/em&gt; change, however, is how often we call it.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Another thing of interest is how much individual &lt;a class="external" href="https://docs.python.org/3/library/io.html#io.IOBase.readline"&gt;readline()&lt;/a&gt; calls take.&lt;/p&gt;
&lt;p&gt;In &lt;code&gt;skip_to_before_line_linear()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;jumped to&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;eof&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
              &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="mi"&gt;1000000&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;4.0f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; us&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
              &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;at offset &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tell&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;16,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;details&gt;
&lt;summary&gt;
The output is pretty enlightening:
&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;pwned.py&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="w"&gt; &lt;/span&gt;asdf
&lt;span class="go"&gt;looking for 3da541559918a808c2402bba5012f6c60b27661c&lt;/span&gt;
&lt;span class="go"&gt;jumped to 7FF9E in   10 us at offset   18,671,134,394  &amp;lt;-- 1/2 file size&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3FF89 in    4 us at offset    9,335,567,234  &amp;lt;-- 1/4 file size&lt;/span&gt;
&lt;span class="go"&gt;jumped to 1FFBA in    3 us at offset    4,667,783,663  &amp;lt;-- 1/8 file size&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3FF89 in    3 us at offset    9,335,567,322  &amp;lt;-- 1/4 file size&lt;/span&gt;
&lt;span class="go"&gt;jumped to 2FFA4 in    5 us at offset    7,001,675,508&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3FF89 in    4 us at offset    9,335,567,366  &amp;lt;-- 1/4 file size&lt;/span&gt;
&lt;span class="go"&gt;jumped to 37F98 in    4 us at offset    8,168,621,453&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3FF89 in    3 us at offset    9,335,567,410  &amp;lt;-- 1/4 file size&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3BF94 in    3 us at offset    8,752,094,477&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3FF89 in    2 us at offset    9,335,567,498  &amp;lt;-- 1/4 file size&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DF8E in    3 us at offset    9,043,831,007&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3CF90 in    3 us at offset    8,897,962,782&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DF8E in    2 us at offset    9,043,831,095&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3D790 in    3 us at offset    8,970,896,964&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DF8E in    2 us at offset    9,043,831,139&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DB90 in  253 us at offset    9,007,364,072&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3D990 in  206 us at offset    8,989,130,552&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DB90 in    6 us at offset    9,007,364,160&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA8F in  270 us at offset    8,998,247,402  &amp;lt;-- page 2,196,837&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA0F in  189 us at offset    8,993,689,007&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA8F in    5 us at offset    8,998,247,446  &amp;lt;-- page 2,196,837&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA4F in  212 us at offset    8,995,968,274&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA8F in    5 us at offset    8,998,247,534  &amp;lt;-- page 2,196,837&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA6F in  266 us at offset    8,997,107,921&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA5F in  203 us at offset    8,996,538,139&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA57 in  195 us at offset    8,996,253,241&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA53 in  197 us at offset    8,996,110,772&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA57 in    6 us at offset    8,996,253,285&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA55 in  193 us at offset    8,996,182,045&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA54 in  178 us at offset    8,996,146,471&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA54 in  189 us at offset    8,996,128,666&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA54 in  191 us at offset    8,996,119,760  &amp;lt;-- page 2,196,318&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA54 in   32 us at offset    8,996,128,710&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA54 in    5 us at offset    8,996,124,259&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA54 in   10 us at offset    8,996,122,057  &amp;lt;-- page 2,196,318&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA54 in    4 us at offset    8,996,120,955  &amp;lt;-- page 2,196,318&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA54 in    4 us at offset    8,996,120,382  &amp;lt;-- page 2,196,318&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA54 in    9 us at offset    8,996,120,112  &amp;lt;-- page 2,196,318&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA54 in    1 us at offset    8,996,120,470  &amp;lt;-- page 2,196,318&lt;/span&gt;
&lt;span class="go"&gt;jumped to 3DA54 in    1 us at offset    8,996,120,338  &amp;lt;-- page 2,196,318&lt;/span&gt;
&lt;span class="go"&gt;pwned! seen 324,774 times before&lt;/span&gt;
&lt;span class="go"&gt;in 0.003654 seconds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;Half the reads are pretty fast:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;In the beginning, because searches start with the same few pages.&lt;/li&gt;
&lt;li&gt;At the end, because searches end on the same page.&lt;/li&gt;
&lt;li&gt;All reads of any page, after the first.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, it's those reads in the middle that we need to get rid of.&lt;/p&gt;
&lt;h3 id="position-heuristic"&gt;Position heuristic&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#position-heuristic" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;In theory, the output of a good hash function
&lt;a class="external" href="https://en.wikipedia.org/wiki/Hash_function#Uniformity"&gt;should be uniformly distributed&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This means that with a bit of math,
we can estimate where a hash would be
–
a hash that's ~1/5 in the range of all possible hashes
should be at ~1/5 of the file.&lt;/p&gt;
&lt;p&gt;Here's a tiny example:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;5b&amp;#39;&lt;/span&gt;  &lt;span class="c1"&gt;# 1-byte hash (2 hex digits)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;    &lt;span class="c1"&gt;# 1000-byte long file&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;int_digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# == 91&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;int_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;   &lt;span class="c1"&gt;# == 0xff + 1 == 256&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;int_digest&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;int_end&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;355&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We can do this once,
and then binary search a safety interval around that position.
Alas,
this only gets rid of the fast jumps at the beginning of the binary search,
and for some reason,
it ends up being slightly slower than binary search alone.
(&lt;a class="attachment" href="/_file/pwned/80-proportional-position-lite.py"&gt;code&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;We can also narrow down around the estimated position iteratively,
making the interval smaller by a constant factor each time.
This seems to work:
a factor of 1000 yields 1.7 ms,
and a factor of 8000 yields 1.2 ms,
both in 2 steps.
(&lt;a class="attachment" href="/_file/pwned/80-proportional-position.py"&gt;code&lt;/a&gt;)&lt;/p&gt;
&lt;p&gt;However, it has deeper issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Having arbitrary start/end offsets complicates the code quite a bit.&lt;/li&gt;
&lt;li&gt;I don't know how to reliably determine the factor.&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;li&gt;I don't know how to prove it's correct,&lt;sup class="footnote-ref" id="fnref-3"&gt;&lt;a href="#fn-3"&gt;3&lt;/a&gt;&lt;/sup&gt;
especially for smaller intervals,
where the hashes are less uniform.
To be honest, I don't think it &lt;em&gt;can&lt;/em&gt; be 100% correct,
and I don't know how to estimate how correct it is.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Anyway, &lt;a class="external" href="https://peps.python.org/pep-0020/"&gt;if the implementation is hard to explain, it's a bad idea&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="index-file"&gt;Index file&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#index-file" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;An arbitrary self-imposed restriction I had was that
any solution should mostly use the original passwords list,
with little to no preparation.&lt;/p&gt;
&lt;p&gt;By relaxing this a bit,
and going through the file once,
we can build an index like:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;&amp;lt;SHA-1 of the password&amp;gt;:&amp;lt;offset in file&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;... that we can then search with &lt;code&gt;skip_to_before_line()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Of course, we won't include all the hashes
–
by including lines a few kilobytes apart from each other,
we can seek directly to within a few kilobytes in the big file.&lt;/p&gt;
&lt;p&gt;The only thing left to figure out is how much &amp;quot;a few kilobytes&amp;quot; is.&lt;/p&gt;
&lt;p&gt;After my endless harping about caching and pages,
the answer should be obvious: one page size (&lt;a class="external" href="https://developer.apple.com/library/archive/documentation/Performance/Conceptual/ManagingMemory/Articles/AboutMemory.html#//apple_ref/doc/uid/20001880-99100-TPXREF113"&gt;4K&lt;/a&gt;).
And this actually gets us &lt;strong&gt;0.8 ms&lt;/strong&gt;!
But back when I wrote the code,
that hadn't really sunk in,
so after getting 1.2 ms with a 32K distance, I moved on.&lt;/p&gt;
&lt;p&gt;Code: &lt;a class="attachment" href="/_file/pwned/81-index.py"&gt;pwned.py&lt;/a&gt;, &lt;a class="attachment" href="/_file/pwned/81-generate-index.py"&gt;generate-index.py&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="binary-file"&gt;Binary file&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#binary-file" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Already on the additional file slippery slope,
I converted the list to binary,
mostly to make it smaller – smaller file, fewer reads.&lt;/p&gt;
&lt;p&gt;I packed each line into 24 bytes:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;| binary hash (20 bytes) | count (4 bytes) |
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This halved the file, but only lowered the runtime to a modest 2.6 ms.&lt;/p&gt;
&lt;p&gt;More importantly, it made the code much, much simpler:
because items are fixed size,
you &lt;em&gt;can&lt;/em&gt; know where the Nth item is,
so I was able to use &lt;a class="external" href="https://docs.python.org/3/library/bisect.html"&gt;bisect&lt;/a&gt; for the binary search.&lt;/p&gt;
&lt;p&gt;Code: &lt;a class="attachment" href="/_file/pwned/82-binary.py"&gt;pwned.py&lt;/a&gt;, &lt;a class="attachment" href="/_file/pwned/82-convert-to-binary.py"&gt;convert-to-binary.py&lt;/a&gt;.&lt;/p&gt;
&lt;!-- TODO FIXME

## Aside: mmap

Mentioning this mainly because I'm not the only one that thought of it.

As a low hanging fruit, I though of [mmap()]-ing the file.
Sadly, this was roughly twice as slow as normal file,
both for the text and the binary file.

Another thing that might help is [fadvise()],
but that's not available on macOS.

[mmap()]: https://docs.python.org/3/library/mmap.html
[fadvise()]: https://linux.die.net/man/2/posix_fadvise

--&gt;

&lt;h2 id="getting-to-under-1-millisecond"&gt;Getting to under 1 millisecond&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#getting-to-under-1-millisecond" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;OK, what now? This is what we have so far:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The position heuristic kinda (maybe?) works,
but is hard to reason about.&lt;/li&gt;
&lt;li&gt;The index file gets us there, but barely,
and the index is pretty big.&lt;/li&gt;
&lt;li&gt;The binary file isn't much faster,
and it creates a huge file.
But, less code!&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I don't know what to do with the first one, but I think we can combine the last two.&lt;/p&gt;
&lt;h3 id="generating-the-index"&gt;Generating the index&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#generating-the-index" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Let's start with the script I made for the text index:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="IPython"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;os&lt;/span&gt;&lt;span class="o"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sys&lt;/span&gt;

&lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdin&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;
&lt;span class="n"&gt;outf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tell&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;

    &lt;span class="n"&gt;outf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SEEK_CUR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The output looks like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;generate-index.py&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;head&lt;span class="w"&gt; &lt;/span&gt;-n5
&lt;span class="go"&gt;000000005AD76BD555C1D6D771DE417A4B87E4B4:0&lt;/span&gt;
&lt;span class="go"&gt;00000099A4D3034E14DF60EF50799F695C27C0EC:4157&lt;/span&gt;
&lt;span class="go"&gt;00000172E8E1D1BD54AC23B3F9AB4383F291CA17:8312&lt;/span&gt;
&lt;span class="go"&gt;000002C8F808A7DB504BBC3C711BE8A8D508C0F9:12453&lt;/span&gt;
&lt;span class="go"&gt;0000047139578F13D70DD96BADD425C372DB64A9:16637&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We need to pack that into bytes.&lt;/p&gt;
&lt;p&gt;A hash takes 20 bytes.
But,
we only need slightly more than 3 bytes (6 hex digits)
to distinguish between the index lines:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;generate-index.py&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;&amp;gt;/dev/null&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cut&lt;span class="w"&gt; &lt;/span&gt;-c-6&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;uniq&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;head
&lt;span class="go"&gt;   2 000000&lt;/span&gt;
&lt;span class="go"&gt;   1 000001&lt;/span&gt;
&lt;span class="go"&gt;   1 000002&lt;/span&gt;
&lt;span class="go"&gt;   1 000004&lt;/span&gt;
&lt;span class="go"&gt;   1 000005&lt;/span&gt;
&lt;span class="go"&gt;   1 000007&lt;/span&gt;
&lt;span class="go"&gt;   1 000008&lt;/span&gt;
&lt;span class="go"&gt;   1 00000A&lt;/span&gt;
&lt;span class="go"&gt;   1 00000B&lt;/span&gt;
&lt;span class="go"&gt;   1 00000C&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To represent all the offsets in the file,
we need &lt;code&gt;log2(35G) / 8 = 4.39...&lt;/code&gt; bytes,
which results in a total of 9 bytes
(maybe even 8, if we mess with individual bits).&lt;/p&gt;
&lt;p&gt;Let's make it future-proof:
6 bytes for the hash buys at least 55 trillion lines
(2.4 petabyte files), and
6 bytes for the offset buys 0.28 petabyte files.&lt;/p&gt;
&lt;!--
&gt;&gt;&gt; size = 37342268646
&gt;&gt;&gt; lines = 847223402
&gt;&gt;&gt; size/lines
44.0760589920532
&gt;&gt;&gt; lines * 256**2
55523632873472
&gt;&gt;&gt; f"{lines * 256**2:,}"
'55,523,632,873,472'
&gt;&gt;&gt; f"{lines * 256**2 * (size/lines):,}"
'2,447,262,917,984,256.0'
&gt;&gt;&gt; f"{ 256**6 :,}"
'281,474,976,710,656'
--&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;outf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fromhex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())[:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;outf&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;write&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;big&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;If you look at the text index,
you'll notice the offsets are 4K + ~50 bytes apart;
this results in sometimes having to read 2 pages,
because not all pages have an index entry.
Let's fix that by reading the first whole line after a 4K boundary instead:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;pos&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;readline&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;OK, we're done:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="nb"&gt;time&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;generate-index.py&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;index.bin

&lt;span class="go"&gt;real	1m2.729s&lt;/span&gt;
&lt;span class="go"&gt;user	0m34.292s&lt;/span&gt;
&lt;span class="go"&gt;sys	0m21.392s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="using-the-index"&gt;Using the index&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#using-the-index" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;We start with a skeleton that's functionally identical
to the &lt;a class="anchor" href="#a-minimal-plausible-solution"&gt;naive&lt;/a&gt; script.
The only difference is that I've added stubs
for passing and using the index:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;find_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;skip_to_before_line_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;find_line_linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;skip_to_before_line_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;index_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;43&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hexdigest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;details&gt;
&lt;summary&gt;
The whole skeleton, if you want to see it:
&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;
&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;
&lt;span class="normal"&gt;52&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="IPython"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;os&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sys&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;bisect&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;getpass&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;hashlib&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;find_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;skip_to_before_line_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;find_line_linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;find_line_linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;lines&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;skip_to_before_line_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;


&lt;span class="n"&gt;path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rb&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;index_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;IndexError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;getpass&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;getpass&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;hexdigest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sha1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;del&lt;/span&gt; &lt;span class="n"&gt;password&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;looking for&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_line&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hexdigest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;monotonic&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;times&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;pwned! seen &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;times&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; times before&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;not found&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.6f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; seconds&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;/details&gt;

&lt;p&gt;As mentioned before,
we'll use the standard library &lt;a class="external" href="https://docs.python.org/3/library/bisect.html"&gt;bisect&lt;/a&gt; module
to search the index.&lt;/p&gt;
&lt;p&gt;We &lt;em&gt;could&lt;/em&gt; read the entire index in memory,
as a list of 12-byte &lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#bytes"&gt;bytes&lt;/a&gt;.
But that would still be slow, even if outside the current timing code,
and memory usage would increase with the size of the file.&lt;/p&gt;
&lt;p&gt;Fortunately,
&lt;a class="external" href="https://docs.python.org/3/library/bisect.html"&gt;bisect&lt;/a&gt; doesn't only work with lists,
it works with any &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-sequence"&gt;sequence&lt;/a&gt;
– that is, any object that can pretend to be a list.
So we'll build our own,
by implementing the two required&lt;sup class="footnote-ref" id="fnref-4"&gt;&lt;a href="#fn-4"&gt;4&lt;/a&gt;&lt;/sup&gt; special methods.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;BytesArray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="n"&gt;item_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;12&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;file&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We can go ahead and plug it in:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;index_path&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BytesArray&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;rb&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The first special method is &lt;a class="external" href="https://docs.python.org/3/reference/datamodel.html#object.__getitem__"&gt;__getitem__()&lt;/a&gt;, for &lt;code&gt;a[i]&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__getitem__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;item_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;buffer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;item_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;buffer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;item_size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;IndexError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# out of bounds&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;buffer&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The second special method is &lt;a class="external" href="https://docs.python.org/3/reference/datamodel.html#object.__len__"&gt;__len__()&lt;/a&gt;, for &lt;code&gt;len(a)&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__len__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SEEK_END&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tell&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;item_size&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Using the index becomes straightforward:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;skip_to_before_line_index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;item_prefix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;bytes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fromhex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;())[:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;find_lt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;item_prefix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;item&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;:],&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;big&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;item&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;file&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seek&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offset&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;find_lt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bisect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bisect_left&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We get the first 6 bytes of the hash,
find the rightmost value less than that,
extract the offset from it,
and seek to there.
&lt;code&gt;find_lt()&lt;/code&gt; comes from &lt;a class="external" href="https://docs.python.org/3/library/bisect.html"&gt;bisect&lt;/a&gt;'s recipes for &lt;a class="external" href="https://docs.python.org/3/library/bisect.html#searching-sorted-lists"&gt;searching sorted lists&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;And we're done:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;average-many&lt;span class="w"&gt; &lt;/span&gt;pwned.py&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="w"&gt; &lt;/span&gt;index.bin
&lt;span class="go"&gt;.002546&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Huh? ... that's unexpected...&lt;/p&gt;
&lt;p&gt;Oh.&lt;/p&gt;
&lt;p&gt;I said we won't read the index in memory.
But we &lt;em&gt;can&lt;/em&gt; force it into the cache
by reading it a bunch of times:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;_&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;..10&lt;span class="o"&gt;}&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;do&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;index.bin&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/dev/null&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Finally:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;average-many&lt;span class="w"&gt; &lt;/span&gt;pwned.py&lt;span class="w"&gt; &lt;/span&gt;pwned.txt&lt;span class="w"&gt; &lt;/span&gt;index.bin
&lt;span class="go"&gt;.000421&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Code: &lt;a class="attachment" href="/_file/pwned/11-binary-index.py"&gt;pwned.py&lt;/a&gt;, &lt;a class="attachment" href="/_file/pwned/10-generate-index.py"&gt;generate-index.py&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="i-heard-you-like-indexes-the-end"&gt;I heard you like indexes (the end)&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#i-heard-you-like-indexes-the-end" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Hmmm... isn't that cold start bugging you?
If we make an index for the big index,
we get 1.2 ms from a cold start.
Maybe another smaller index can take us to below 1 ms?&lt;/p&gt;
&lt;p&gt;...&lt;/p&gt;
&lt;p&gt;Just kidding, this is it, this really is the end.&lt;/p&gt;
&lt;p&gt;And now, let's take that moment to ponder:&lt;/p&gt;
&lt;table class="table"&gt;
&lt;thead&gt;
&lt;tr&gt;
  &lt;th&gt;method&lt;/th&gt;
  &lt;th style="text-align:right"&gt;statements&lt;/th&gt;
  &lt;th style="text-align:right"&gt;time (ms, order of magnitude)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
  &lt;td&gt;linear&lt;/td&gt;
  &lt;td style="text-align:right"&gt;29&lt;/td&gt;
  &lt;td style="text-align:right"&gt;100,000&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;linear+skip&lt;/td&gt;
  &lt;td style="text-align:right"&gt;42&lt;/td&gt;
  &lt;td style="text-align:right"&gt;100&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;binary search&lt;/td&gt;
  &lt;td style="text-align:right"&gt;49&lt;/td&gt;
  &lt;td style="text-align:right"&gt;10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td&gt;binary index&lt;/td&gt;
  &lt;td style="text-align:right"&gt;59 (72)&lt;/td&gt;
  &lt;td style="text-align:right"&gt;1&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;For twice the code, it's 5 orders of magnitude faster!
I'm deliberately not counting bisect or the OS cache here,
beacuse that's the point, they're basically free.&lt;/p&gt;
&lt;p&gt;Turns out, you can get pretty far with just &lt;em&gt;a few&lt;/em&gt; tricks.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/pwned&amp;t=Has%20your%20password%20been%20pwned%3F%20Or%2C%20how%20I%20almost%20failed%20to%20search%20a%2037%C2%A0GB%20text%20file%20in%20under%201%C2%A0millisecond%20%28in%20Python%29"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Has%20your%20password%20been%20pwned%3F%20Or%2C%20how%20I%20almost%20failed%20to%20search%20a%2037%C2%A0GB%20text%20file%20in%20under%201%C2%A0millisecond%20%28in%20Python%29%20https%3A//death.andgravity.com/pwned"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/pwned&amp;title=Has%20your%20password%20been%20pwned%3F%20Or%2C%20how%20I%20almost%20failed%20to%20search%20a%2037%C2%A0GB%20text%20file%20in%20under%201%C2%A0millisecond%20%28in%20Python%29"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/pwned"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Has%20your%20password%20been%20pwned%3F%20Or%2C%20how%20I%20almost%20failed%20to%20search%20a%2037%C2%A0GB%20text%20file%20in%20under%201%C2%A0millisecond%20%28in%20Python%29&amp;url=https%3A//death.andgravity.com/pwned&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;




&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        If you&amp;#39;ve made it this far, you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/query-builder-how"&gt;
            Write an SQL query builder in 150 lines of Python!
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="bonus-better-data-structures"&gt;Bonus: better data structures&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bonus-better-data-structures" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;As always, a specialized data structure can solve a problem better.&lt;/p&gt;
&lt;p&gt;In &lt;a class="external" href="https://scotthelme.co.uk/sketchy-pwned-passwords/"&gt;Sketchy Pwned Passwords&lt;/a&gt;,
Scott Helme manages to &amp;quot;pack&amp;quot; the entire passwords list
into a 1.5G in-memory &lt;a class="external" href="https://en.wikipedia.org/wiki/Count%E2%80%93min_sketch"&gt;count-min sketch&lt;/a&gt;,
which can then be queried in 1 microsecond.
And, if you don't care about the counts,
&lt;a class="external" href="https://scotthelme.co.uk/when-pwned-passwords-bloom/"&gt;a plain Bloom filter&lt;/a&gt; can even take you to 0.1 µs!
(There is a trade-off, and it's that it takes 11 hours to create either.)&lt;/p&gt;

&lt;!--

# Bonus: the k-Anonymity API

&lt;!-- TODO: fix file to be api only

.. literalinclude:: 03-kanon.py
    :lines: 45-52

.. literalinclude:: 03-kanon.py
    :lines: 55-61
    :emphasize-lines: 2-4,7

.. literalinclude:: 03-kanon.py
    :lines: 72-74
    :emphasize-lines: 2

https://api.pwnedpasswords.com/range/5baa6

https://haveibeenpwned.com/API/v3#PwnedPasswords

--&gt;

&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;It's &lt;a class="external" href="https://stackoverflow.com/q/28676177"&gt;kinda difficult&lt;/a&gt; to do in Python anyway. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;Something like &lt;code&gt;(size / 4096) ** (1 / int(log(size, 4096)))&lt;/code&gt;, maybe? &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-3"&gt;&lt;p&gt;I mean, I did cross-check it with other solutions
for a few thousand values, and it &lt;em&gt;seems&lt;/em&gt; correct,
but that's not proof. &lt;a href="#fnref-3" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-4"&gt;&lt;p&gt;We're only implementing the parts of the protocol that &lt;code&gt;bisect&lt;/code&gt; uses.&lt;/p&gt;
&lt;p&gt;For the full protocol,
&lt;a class="external" href="https://docs.python.org/3/reference/datamodel.html#object.__getitem__"&gt;__getitem__()&lt;/a&gt; would need to implement
negative indexes and slicing.
For more, free sequence methods, inherit &lt;a class="external" href="https://docs.python.org/3/library/collections.abc.html#collections.abc.Sequence"&gt;collections.abc.Sequence&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Interestingly, the class will work in a &lt;code&gt;for&lt;/code&gt; loop
without an &lt;a class="external" href="https://docs.python.org/3/reference/datamodel.html#object.__iter__"&gt;__iter__()&lt;/a&gt; method.
That's because there are actually two iteration protocols:
an older one, using __getitem__(),
and a newer one, &lt;a class="external" href="https://peps.python.org/pep-0234/"&gt;added in Python 2.1&lt;/a&gt;,
using __iter__() and __next__().
For details on the logic,
see &lt;a class="external" href="https://snarky.ca/unravelling-for-statements/#iter-iterable-"&gt;Unravelling &lt;code&gt;for&lt;/code&gt; statements&lt;/a&gt;. &lt;a href="#fnref-4" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/pwned" rel="alternate"/>
    <summary>... in which we check if your password has been compromised in many inconvenient ways, in a tale of destruction, obsession, and self-discovery.</summary>
    <published>2022-12-13T11:20:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/reader-3-0">
    <id>https://death.andgravity.com/reader-3-0</id>
    <title>reader 3.0 released – multithreading</title>
    <updated>2022-09-16T20:00:00+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;I'm happy to announce version 3.0 of &lt;strong&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/strong&gt;, a Python feed reader library.&lt;/p&gt;
&lt;p&gt;This release removes a number of deprecated methods and attributes,
for a cleaner, more consistent API.
See the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-3-0"&gt;changelog&lt;/a&gt; for details.&lt;/p&gt;
&lt;details class="toc" open&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#2-x-recap"&gt;2.x recap&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#unified-tag-api-entry-and-global-tags"&gt;Unified tag API + entry and global tags&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#search-enabled-by-default"&gt;Search enabled by default&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#statistics"&gt;Statistics&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#user-added-entries"&gt;User-added entries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#twitter-support"&gt;Twitter support&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#read-time"&gt;Read time&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#improved-duplicate-handling"&gt;Improved duplicate handling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#memory-usage-improvements"&gt;Memory usage improvements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#multithreading"&gt;Multithreading&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#typing"&gt;Typing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#new-python-versions"&gt;New Python versions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#other-changes"&gt;Other changes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#what-is-reader"&gt;What is reader?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#why-make-your-own-feed-reader"&gt;Why make your own feed reader?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="2-x-recap"&gt;2.x recap&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#2-x-recap" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="internal" href="/reader-2-0"&gt;2.0&lt;/a&gt; was released over a year ago;
let's have look at what happened since.&lt;/p&gt;
&lt;h3 id="unified-tag-api-entry-and-global-tags"&gt;Unified tag API + entry and global tags&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#unified-tag-api-entry-and-global-tags" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- 2.11 --&gt;

&lt;p&gt;Tags and metadata are now the same thing, generic &lt;a class="external" href="https://reader.readthedocs.io/en/stable/guide.html#resource-tags"&gt;resource tags&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;default&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;default&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;value&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;value&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;{&amp;#39;one&amp;#39;: &amp;#39;value&amp;#39;, &amp;#39;two&amp;#39;: None}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This means you can filter by metadata keys, and attach values to tags.&lt;/p&gt;
&lt;p&gt;Even better, tags aren't just for feeds anymore –
you can add tags to entries, and to a global namespace.&lt;/p&gt;
&lt;h3 id="search-enabled-by-default"&gt;Search enabled by default&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#search-enabled-by-default" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- 2.5 --&gt;

&lt;p&gt;&lt;a class="external" href="https://reader.readthedocs.io/en/stable/guide.html#full-text-search"&gt;Full-text search&lt;/a&gt; works out of the box:
no extra dependencies, no setup needed.&lt;/p&gt;
&lt;h3 id="statistics"&gt;Statistics&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#statistics" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- 2.5 --&gt;

&lt;p&gt;There are now statistics on feed and user activity,
to give you a better understanding of how you consume content.&lt;/p&gt;
&lt;p&gt;First, you can get the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/guide.html#counting-things"&gt;average number of entries&lt;/a&gt; per day
for the last 1, 3, 12 months,
so you know how often a feed publishes new entries,
and how that changed over time –
think &lt;a class="external" href="https://en.wikipedia.org/wiki/Sparkline"&gt;sparklines&lt;/a&gt;: &lt;code&gt;36 entries ▄▃▁ (4.0, 2.0, 0.6)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Second, &lt;em&gt;reader&lt;/em&gt; records the time when an entry
was last &lt;a class="external" href="https://reader.readthedocs.io/en/stable/guide.html#entry-flags"&gt;marked as read or important&lt;/a&gt;.
This will allow you to see how you engage with new entries
– I'm still working on how to translate this data into a useful summary.&lt;/p&gt;
&lt;p&gt;A nice side-effect of knowing when entry flags changed
is that it's possible to tell
if an entry was &lt;em&gt;explicitly&lt;/em&gt; marked as unimportant
(entries are unimportant by default).&lt;/p&gt;
&lt;h3 id="user-added-entries"&gt;User-added entries&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#user-added-entries" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- 2.5 --&gt;

&lt;p&gt;You can now &lt;a class="external" href="https://reader.readthedocs.io/en/stable/api.html#reader.Reader.add_entry"&gt;add entries&lt;/a&gt; to existing feeds.
This is useful when you want to keep track of an article
that is not in the feed anymore because it &amp;quot;fell off the end&amp;quot;.&lt;/p&gt;
&lt;p&gt;It can also be used to build bookmarking / read later functionality
similar to that of &lt;a class="external" href="https://tt-rss.org/wiki/ShareAnything"&gt;Tiny Tiny RSS&lt;/a&gt;;
&lt;a class="external" href="https://github.com/lemon24/reader/issues/222"&gt;extracting content&lt;/a&gt; from arbitrary pages would be pretty helpful here.&lt;/p&gt;
&lt;h3 id="twitter-support"&gt;Twitter support&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#twitter-support" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- 2.14 --&gt;

&lt;p&gt;You can now &lt;a class="external" href="https://reader.readthedocs.io/en/stable/plugins.html#twitter"&gt;follow Twitter accounts&lt;/a&gt;
(experimental, requires a Twitter account).&lt;/p&gt;
&lt;h3 id="read-time"&gt;Read time&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#read-time" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- 2.14 --&gt;

&lt;p&gt;The new &lt;a class="external" href="https://reader.readthedocs.io/en/latest/plugins.html#reader-readtime"&gt;readtime&lt;/a&gt; plugin calculates the entry read time during feed updates.&lt;/p&gt;
&lt;p&gt;This makes available to any &lt;em&gt;reader&lt;/em&gt; user
a feature that was only available in the web app,
&lt;em&gt;and&lt;/em&gt; makes the web app faster.&lt;/p&gt;
&lt;h3 id="improved-duplicate-handling"&gt;Improved duplicate handling&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#improved-duplicate-handling" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- 2.5 --&gt;

&lt;p&gt;&lt;a class="external" href="https://reader.readthedocs.io/en/stable/plugins.html#reader-entry-dedupe"&gt;Duplicate handling&lt;/a&gt; got significantly better:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;False negatives are reduced by using approximate string matching
and heuristics to detect truncated content.&lt;/li&gt;
&lt;li&gt;You can trigger entry deduplication manually,
for the existing entries of a feed
– just add the &lt;code&gt;.reader.dedupe.once&lt;/code&gt; tag to the feed,
and wait for the next update.
Also, you can deduplicate entries by title alone, ignoring content.&lt;/li&gt;
&lt;li&gt;Old duplicates are deleted instead of marked as read/unimportant.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="memory-usage-improvements"&gt;Memory usage improvements&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#memory-usage-improvements" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- 2.11 --&gt;

&lt;p&gt;&lt;code&gt;reader update&lt;/code&gt; uses about 22% less memory.&lt;/p&gt;
&lt;p&gt;The main change is not to &lt;em&gt;reader&lt;/em&gt; itself,
but was contributed upstream to &lt;a class="external" href="https://github.com/kurtmckee/feedparser/pull/302"&gt;feedparser&lt;/a&gt;:
instead of reading the whole feed in memory to detect encoding,
use a prefix of the feed, and decode the rest on the fly.
The result is a 35% decrease in &lt;code&gt;update_feeds()&lt;/code&gt; maximum RSS
when compared to baseline!&lt;/p&gt;
&lt;p&gt;You can find more details &lt;a class="internal" href="/reader-2-11#memory-usage-improvements"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="multithreading"&gt;Multithreading&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#multithreading" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- 2.16 --&gt;

&lt;p&gt;You can now use the same Reader object from &lt;a class="external" href="https://reader.readthedocs.io/en/stable/guide.html#threading"&gt;multiple threads&lt;/a&gt;.
So, you can do stuff like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;Thread&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update_feeds&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Also, you can &lt;a class="external" href="https://reader.readthedocs.io/en/stable/guide.html#lifecycle"&gt;reuse&lt;/a&gt; Reader objects after closing.&lt;/p&gt;
&lt;h3 id="typing"&gt;Typing&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#typing" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- 2.14 --&gt;

&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; has had type annotations for most of its existence;
starting with 2.14, user code can use them too.&lt;/p&gt;
&lt;h3 id="new-python-versions"&gt;New Python versions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#new-python-versions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;!-- 2.5 --&gt;

&lt;!-- 2.14 --&gt;

&lt;p&gt;Over the course of 2.x,
&lt;em&gt;reader&lt;/em&gt; got support for Python 3.10, and PyPy 3.8 and 3.9,
and dropped support for Python 3.7.&lt;/p&gt;
&lt;h3 id="other-changes"&gt;Other changes&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#other-changes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Aside from the changes mentioned above,
a lot of convenience methods, arguments, and attributes were added.
Among the more notable ones, now you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;filter feeds in the same way both when &lt;a class="external" href="https://reader.readthedocs.io/en/stable/api.html#reader.Reader.get_feeds"&gt;getting&lt;/a&gt; and when &lt;a class="external" href="https://reader.readthedocs.io/en/stable/api.html#reader.Reader.update_feeds"&gt;updating&lt;/a&gt; feeds
– including by tags&lt;/li&gt;
&lt;li&gt;run arbitrary actions &lt;a class="external" href="https://reader.readthedocs.io/en/stable/api.html#reader.Reader.before_feed_update_hooks"&gt;before&lt;/a&gt; and &lt;a class="external" href="https://reader.readthedocs.io/en/stable/api.html#reader.Reader.after_feed_update_hooks"&gt;after&lt;/a&gt; updating feeds&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/reader-3-0&amp;t=reader%203.0%20released%20%E2%80%93%20multithreading"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=reader%203.0%20released%20%E2%80%93%20multithreading%20https%3A//death.andgravity.com/reader-3-0"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/reader-3-0&amp;title=reader%203.0%20released%20%E2%80%93%20multithreading"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/reader-3-0"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=reader%203.0%20released%20%E2%80%93%20multithreading&amp;url=https%3A//death.andgravity.com/reader-3-0&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;h2 id="what-is-reader"&gt;What is &lt;em&gt;reader&lt;/em&gt;?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;reader&lt;/strong&gt; takes care
of the core functionality required by a feed reader,
so you can focus on what makes &lt;strong&gt;yours&lt;/strong&gt; different.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-2-0/reader.png" alt="reader in action" /&gt;
&lt;em&gt;reader&lt;/em&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retrieve, store, and manage &lt;strong&gt;Atom&lt;/strong&gt;, &lt;strong&gt;RSS&lt;/strong&gt;, and &lt;strong&gt;JSON&lt;/strong&gt; feeds&lt;ul&gt;
&lt;li&gt;and even follow &lt;strong&gt;Twitter&lt;/strong&gt; accounts&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;mark articles as read or important&lt;/li&gt;
&lt;li&gt;add arbitrary tags/metadata to feeds and articles&lt;/li&gt;
&lt;li&gt;filter feeds and articles&lt;/li&gt;
&lt;li&gt;full-text search articles&lt;/li&gt;
&lt;li&gt;get statistics on feed and user activity&lt;/li&gt;
&lt;li&gt;write plugins to extend its functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...all these with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stable, clearly documented API&lt;/li&gt;
&lt;li&gt;excellent test coverage&lt;/li&gt;
&lt;li&gt;fully typed Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find out more, check out the &lt;a class="external" href="https://github.com/lemon24/reader"&gt;GitHub repo&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/"&gt;docs&lt;/a&gt;,
or give the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/tutorial.html"&gt;tutorial&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2 id="why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-use-a-feed-reader-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Have you been unhappy with existing feed readers and wanted to make your own, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never knew where to start?&lt;/li&gt;
&lt;li&gt;it seemed like too much work?&lt;/li&gt;
&lt;li&gt;you don't like writing backend code?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you already working with &lt;a class="external" href="https://github.com/kurtmckee/feedparser/pull/302"&gt;feedparser&lt;/a&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;want an easier way to store, filter, sort and search feeds and entries?&lt;/li&gt;
&lt;li&gt;want to get back type-annotated objects instead of dicts?&lt;/li&gt;
&lt;li&gt;want to restrict or deny file-system access?&lt;/li&gt;
&lt;li&gt;want to change the way feeds are retrieved by using &lt;a class="external" href="https://requests.readthedocs.io"&gt;Requests&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to also support &lt;a class="external" href="https://jsonfeed.org/"&gt;JSON Feed&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to follow &lt;a class="external" href="https://reader.readthedocs.io/en/stable/plugins.html#twitter"&gt;Twitter&lt;/a&gt; accounts?&lt;/li&gt;
&lt;li&gt;want to support custom information sources?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... while still supporting all the feed types feedparser does?&lt;/p&gt;
&lt;p&gt;If you answered yes to any of the above, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;h2 id="why-make-your-own-feed-reader"&gt;Why make your own feed reader?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-make-your-own-feed-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;have full control over your data&lt;/li&gt;
&lt;li&gt;control what features it has or doesn't have&lt;/li&gt;
&lt;li&gt;decide how much you pay for it&lt;/li&gt;
&lt;li&gt;make sure it doesn't get closed while you're still using it&lt;/li&gt;
&lt;li&gt;really, it's &lt;a class="external" href="https://rachelbythebay.com/w/2011/10/26/fred/"&gt;easier than you think&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously, this may not be your cup of tea, but if it is, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
</content>
    <link href="https://death.andgravity.com/reader-3-0" rel="alternate"/>
    <published>2022-09-16T18:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/reader-2-14">
    <id>https://death.andgravity.com/reader-2-14</id>
    <title>reader 2.14 released – Twitter, read time</title>
    <updated>2022-07-01T18:18:18.180000+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;I'm happy to announce version 2.14 of &lt;strong&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/strong&gt;, a Python feed reader library.&lt;/p&gt;
&lt;h2 id="what-s-new"&gt;What's new?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-new" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Here are the most important changes since &lt;a class="internal" href="/reader-2-11"&gt;reader 2.11&lt;/a&gt;!&lt;/p&gt;
&lt;h3 id="twitter-support"&gt;Twitter support&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#twitter-support" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;You can now use &lt;em&gt;reader&lt;/em&gt; to &lt;a class="external" href="https://reader.readthedocs.io/en/latest/plugins.html#twitter"&gt;follow Twitter accounts&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You still need a Twitter account to get a bearer token,
but after that, &lt;em&gt;https://twitter.com/user&lt;/em&gt; works like any other feed.&lt;/p&gt;
&lt;p&gt;Each thread corresponds to an entry.
Threads are rendered as HTML,
but you can access the original JSON too,
in case you want to do your own rendering.&lt;/p&gt;
&lt;p&gt;Here's what it looks like in the web app:&lt;/p&gt;
&lt;div class="columns"&gt;&lt;div class="column col-sm-6 col-xs-12"&gt;

&lt;figure class="figure"&gt;
&lt;img class="img-responsive" src="/_file/reader-2-14/twitter-one.png" alt="thread with quote" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
thread with quote
&lt;/figcaption&gt;
&lt;/figure&gt;



&lt;/div&gt;
&lt;div class="column col-sm-6 col-xs-12"&gt;

&lt;figure class="figure"&gt;
&lt;img class="img-responsive" src="/_file/reader-2-14/twitter-two.png" alt="thread with media" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
thread with media
&lt;/figcaption&gt;
&lt;/figure&gt;



&lt;/div&gt;
&lt;/div&gt;

&lt;h3 id="read-time"&gt;Read time&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#read-time" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The new &lt;a class="external" href="https://reader.readthedocs.io/en/latest/plugins.html#reader-readtime"&gt;readtime&lt;/a&gt; plugin calculates the read time
of an entry during feed update.&lt;/p&gt;
&lt;p&gt;This makes available to any &lt;em&gt;reader&lt;/em&gt; user
a feature that was only available in the web app,
&lt;em&gt;and&lt;/em&gt; makes the web app faster.&lt;/p&gt;
&lt;h3 id="typing"&gt;Typing&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#typing" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;You can now type check code that uses &lt;em&gt;reader&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; has had type annotations for most of its existence,
but user code would fail type checking
because &lt;em&gt;reader&lt;/em&gt; did not explicitly declare it.&lt;/p&gt;
&lt;h3 id="python-versions"&gt;Python versions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#python-versions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; 2.14 drops support for Python 3.7, and adds support for PyPy 3.9.&lt;/p&gt;
&lt;h3 id="bug-fixes"&gt;Bug fixes&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bug-fixes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; now skips RSS entries that have no &lt;code&gt;&amp;lt;guid&amp;gt;&lt;/code&gt; or &lt;code&gt;&amp;lt;link&amp;gt;&lt;/code&gt;
with a warning, instead of failing the entire feed.
Thanks to &lt;a class="external" href="https://github.com/mirekdlugosz"&gt;Mirek Długosz&lt;/a&gt; for the pull request.&lt;/p&gt;
&lt;p&gt;For more details, see the full &lt;a class="external" href="https://reader.readthedocs.io/en/latest/changelog.html"&gt;changelog&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/reader-2-14&amp;t=reader%202.14%20released%20%E2%80%93%20Twitter%2C%20read%20time"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=reader%202.14%20released%20%E2%80%93%20Twitter%2C%20read%20time%20https%3A//death.andgravity.com/reader-2-14"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/reader-2-14&amp;title=reader%202.14%20released%20%E2%80%93%20Twitter%2C%20read%20time"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/reader-2-14"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=reader%202.14%20released%20%E2%80%93%20Twitter%2C%20read%20time&amp;url=https%3A//death.andgravity.com/reader-2-14&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;h2 id="what-is-reader"&gt;What is &lt;em&gt;reader&lt;/em&gt;?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;reader&lt;/strong&gt; takes care
of the core functionality required by a feed reader,
so you can focus on what makes &lt;strong&gt;yours&lt;/strong&gt; different.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-2-0/reader.png" alt="reader in action" /&gt;
&lt;em&gt;reader&lt;/em&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retrieve, store, and manage &lt;strong&gt;Atom&lt;/strong&gt;, &lt;strong&gt;RSS&lt;/strong&gt;, and &lt;strong&gt;JSON&lt;/strong&gt; feeds&lt;/li&gt;
&lt;li&gt;mark articles as read or important&lt;/li&gt;
&lt;li&gt;add arbitrary metadata to feeds and articles&lt;/li&gt;
&lt;li&gt;filter feeds and articles&lt;/li&gt;
&lt;li&gt;full-text search articles&lt;/li&gt;
&lt;li&gt;get statistics on feed and user activity&lt;/li&gt;
&lt;li&gt;write plugins to extend its functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...all these with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stable, clearly documented API&lt;/li&gt;
&lt;li&gt;excellent test coverage&lt;/li&gt;
&lt;li&gt;fully typed Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find out more, check out the &lt;a class="external" href="https://github.com/lemon24/reader"&gt;GitHub repo&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/"&gt;docs&lt;/a&gt;,
or give the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/tutorial.html"&gt;tutorial&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2 id="why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-use-a-feed-reader-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Have you been unhappy with existing feed readers and wanted to make your own, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never knew where to start?&lt;/li&gt;
&lt;li&gt;it seemed like too much work?&lt;/li&gt;
&lt;li&gt;you don't like writing backend code?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you already working with &lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/"&gt;feedparser&lt;/a&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;want an easier way to store, filter, sort and search feeds and entries?&lt;/li&gt;
&lt;li&gt;want to get back type-annotated objects instead of dicts?&lt;/li&gt;
&lt;li&gt;want to restrict or deny file-system access?&lt;/li&gt;
&lt;li&gt;want to change the way feeds are retrieved by using &lt;a class="external" href="https://requests.readthedocs.io"&gt;Requests&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to also support &lt;a class="external" href="https://jsonfeed.org/"&gt;JSON Feed&lt;/a&gt;?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... while still supporting all the feed types feedparser does?&lt;/p&gt;
&lt;p&gt;If you answered yes to any of the above, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;h2 id="why-make-your-own-feed-reader"&gt;Why make your own feed reader?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-make-your-own-feed-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;have full control over your data&lt;/li&gt;
&lt;li&gt;control what features it has or doesn't have&lt;/li&gt;
&lt;li&gt;decide how much you pay for it&lt;/li&gt;
&lt;li&gt;make sure it doesn't get closed while you're still using it&lt;/li&gt;
&lt;li&gt;really, it's &lt;a class="external" href="https://rachelbythebay.com/w/2011/10/26/fred/"&gt;easier than you think&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously, this may not be your cup of tea, but if it is, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
</content>
    <link href="https://death.andgravity.com/reader-2-14" rel="alternate"/>
    <published>2022-07-01T18:18:18+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/f-re">
    <id>https://death.andgravity.com/f-re</id>
    <title>The unreasonable effectiveness of f‍-‍strings and re.VERBOSE</title>
    <updated>2022-06-20T15:02:01+00:00</updated>
    <content type="html">&lt;p&gt;... in which we look at one or two ways to make life easier
when working with Python regular expressions.&lt;/p&gt;
&lt;p&gt;tl;dr: &lt;strong&gt;You can compose verbose regular expressions using f‍-‍strings.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Here's a real-world example – instead of this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;1&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;((?:\(\s*)?[A-Z]*H\d+[a-z]*(?:\s*\+\s*[A-Z]*H\d+[a-z]*)*(?:\s*[\):+])?)(.*?)(?=(?:\(\s*)?[A-Z]*H\d+[a-z]*(?:\s*\+\s*[A-Z]*H\d+[a-z]*)*(?:\s*[\):+])?(?![^\w\s])|$)&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;... do this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;[A-Z]*H  # prefix&lt;/span&gt;
&lt;span class="s2"&gt;\d+      # digits&lt;/span&gt;
&lt;span class="s2"&gt;[a-z]*   # suffix&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;multicode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;fr&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;(?: \( \s* )?               # maybe open paren and maybe space&lt;/span&gt;
&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;                      # one code&lt;/span&gt;
&lt;span class="s2"&gt;(?: \s* \+ \s* &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; )*    # maybe followed by other codes, plus-separated&lt;/span&gt;
&lt;span class="s2"&gt;(?: \s* [\):+] )?           # maybe space and maybe close paren or colon or plus&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;fr&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;( &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;multicode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; )             # code (capture)&lt;/span&gt;
&lt;span class="s2"&gt;( .*? )                     # message (capture): everything ...&lt;/span&gt;
&lt;span class="s2"&gt;(?=                         # ... up to (but excluding) ...&lt;/span&gt;
&lt;span class="s2"&gt;    &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;multicode&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;             # ... the next code&lt;/span&gt;
&lt;span class="s2"&gt;        (?! [^\w\s] )       # (but not when followed by punctuation)&lt;/span&gt;
&lt;span class="s2"&gt;    | $                     # ... or the end&lt;/span&gt;
&lt;span class="s2"&gt;)&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;details&gt;
&lt;summary&gt;
For comparison, the same pattern without f&amp;zwj;-&amp;zwj;strings (click to expand).
&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;
&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;
&lt;span class="normal"&gt;52&lt;/span&gt;
&lt;span class="normal"&gt;53&lt;/span&gt;
&lt;span class="normal"&gt;54&lt;/span&gt;
&lt;span class="normal"&gt;55&lt;/span&gt;
&lt;span class="normal"&gt;56&lt;/span&gt;
&lt;span class="normal"&gt;57&lt;/span&gt;
&lt;span class="normal"&gt;58&lt;/span&gt;
&lt;span class="normal"&gt;59&lt;/span&gt;
&lt;span class="normal"&gt;60&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;(                       # code (capture)&lt;/span&gt;
&lt;span class="s2"&gt;    # BEGIN multicode&lt;/span&gt;

&lt;span class="s2"&gt;    (?: \( \s* )?       # maybe open paren and maybe space&lt;/span&gt;

&lt;span class="s2"&gt;    # code&lt;/span&gt;
&lt;span class="s2"&gt;    [A-Z]*H  # prefix&lt;/span&gt;
&lt;span class="s2"&gt;    \d+      # digits&lt;/span&gt;
&lt;span class="s2"&gt;    [a-z]*   # suffix&lt;/span&gt;

&lt;span class="s2"&gt;    (?:                 # maybe followed by other codes,&lt;/span&gt;
&lt;span class="s2"&gt;        \s* \+ \s*      # ... plus-separated&lt;/span&gt;

&lt;span class="s2"&gt;        # code&lt;/span&gt;
&lt;span class="s2"&gt;        [A-Z]*H  # prefix&lt;/span&gt;
&lt;span class="s2"&gt;        \d+      # digits&lt;/span&gt;
&lt;span class="s2"&gt;        [a-z]*   # suffix&lt;/span&gt;
&lt;span class="s2"&gt;    )*&lt;/span&gt;

&lt;span class="s2"&gt;    (?: \s* [\):+] )?   # maybe space and maybe close paren or colon or plus&lt;/span&gt;

&lt;span class="s2"&gt;    # END multicode&lt;/span&gt;
&lt;span class="s2"&gt;)&lt;/span&gt;

&lt;span class="s2"&gt;( .*? )                 # message (capture): everything ...&lt;/span&gt;

&lt;span class="s2"&gt;(?=                     # ... up to (but excluding) ...&lt;/span&gt;
&lt;span class="s2"&gt;    # ... the next code&lt;/span&gt;

&lt;span class="s2"&gt;    # BEGIN multicode&lt;/span&gt;

&lt;span class="s2"&gt;    (?: \( \s* )?       # maybe open paren and maybe space&lt;/span&gt;

&lt;span class="s2"&gt;    # code&lt;/span&gt;
&lt;span class="s2"&gt;    [A-Z]*H  # prefix&lt;/span&gt;
&lt;span class="s2"&gt;    \d+      # digits&lt;/span&gt;
&lt;span class="s2"&gt;    [a-z]*   # suffix&lt;/span&gt;

&lt;span class="s2"&gt;    (?:                 # maybe followed by other codes,&lt;/span&gt;
&lt;span class="s2"&gt;        \s* \+ \s*      # ... plus-separated&lt;/span&gt;

&lt;span class="s2"&gt;        # code&lt;/span&gt;
&lt;span class="s2"&gt;        [A-Z]*H  # prefix&lt;/span&gt;
&lt;span class="s2"&gt;        \d+      # digits&lt;/span&gt;
&lt;span class="s2"&gt;        [a-z]*   # suffix&lt;/span&gt;
&lt;span class="s2"&gt;    )*&lt;/span&gt;

&lt;span class="s2"&gt;    (?: \s* [\):+] )?   # maybe space and maybe close paren or colon or plus&lt;/span&gt;

&lt;span class="s2"&gt;    # END multicode&lt;/span&gt;

&lt;span class="s2"&gt;        # (but not when followed by punctuation)&lt;/span&gt;
&lt;span class="s2"&gt;        (?! [^\w\s] )&lt;/span&gt;

&lt;span class="s2"&gt;    # ... or the end&lt;/span&gt;
&lt;span class="s2"&gt;    | $&lt;/span&gt;
&lt;span class="s2"&gt;)&lt;/span&gt;

&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;It's better than the non-verbose one,
but even with careful formatting and comments,
the repetition makes it pretty hard to follow
– and wait until you have to change something!&lt;/p&gt;
&lt;/details&gt;

&lt;p&gt;Read on for details and some caveats.&lt;/p&gt;
&lt;h2 id="prerequisites"&gt;Prerequisites&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#prerequisites" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Formatted string literals (&lt;a class="external" href="https://docs.python.org/3/glossary.html#term-f-string"&gt;f‍-‍strings&lt;/a&gt;) were added in Python 3.6&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;,
and provide a way to embed expressions inside string literals,
using a syntax similar to that of &lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#str.format"&gt;str.format()&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;world&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Hello, &lt;/span&gt;&lt;span class="si"&gt;{name}&lt;/span&gt;&lt;span class="s2"&gt;!&amp;quot;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;Hello, world!&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Hello, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;!&amp;quot;&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;Hello, world!&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Verbose regular expressions (&lt;a class="external" href="https://docs.python.org/3/library/re.html#re.VERBOSE"&gt;re.VERBOSE&lt;/a&gt;) have been around since forever&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;,
and allow writing regular expressions
with non-significant whitespace and comments:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;H1 code (AH2b+EUH3) fancy code&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[A-Z]*H\d+[a-z]*&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;H1&amp;#39;, &amp;#39;AH2b&amp;#39;, &amp;#39;EUH3&amp;#39;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;[A-Z]*H  # prefix&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;\d+      # digits&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;[a-z]*   # suffix&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;H1&amp;#39;, &amp;#39;AH2b&amp;#39;, &amp;#39;EUH3&amp;#39;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="the-one-weird-trick"&gt;The &amp;quot;one weird trick&amp;quot;&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-one-weird-trick" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Once you see it, it's obvious
– you can use f‍-‍strings to compose regular expressions:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;multicode&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;fr&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;(?: \( )?         # maybe open paren&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;            # one code&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;(?: \+ &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;code&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; )*  # maybe other codes, plus-separated&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;(?: \) )?         # maybe close paren&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;multicode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;H1&amp;#39;, &amp;#39;(AH2b+EUH3)&amp;#39;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It's so obvious, it only took me three years to do it
after I started using Python 3.6+,
despite using both features during all that time.&lt;/p&gt;
&lt;p&gt;Of course, there's any number of libraries
for building regular expressions;
the benefit of this is that it has zero dependencies,
and zero extra things you need to learn.&lt;/p&gt;



&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        Liking this so far? Here&amp;#39;s another article you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/stdlib"&gt;
            Learn by reading code: Python standard library design decisions explained
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="caveats"&gt;Caveats&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#caveats" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id="hashes-and-spaces-need-to-be-escaped"&gt;Hashes and spaces need to be escaped&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#hashes-and-spaces-need-to-be-escaped" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Because a hash is used to mark the start of a comment,
and spaces are mostly ignored,
you have to represent them in some other way.&lt;/p&gt;
&lt;p&gt;The documentation of &lt;a class="external" href="https://docs.python.org/3/library/re.html#re.VERBOSE"&gt;re.VERBOSE&lt;/a&gt; is quite helpful:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When a line contains a &lt;code&gt;#&lt;/code&gt; that is not in a character class and is not preceded by an unescaped backslash, all characters from the leftmost such &lt;code&gt;#&lt;/code&gt; through the end of the line are ignored.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;That is, this won't work as the non-verbose version:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\d+#\d+&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;1#23a&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;1#23&amp;#39;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\d+ # \d+&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;1#23a&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;1&amp;#39;, &amp;#39;23&amp;#39;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... but these will:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\d+ [#] \d+&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;1#23a&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;1#23&amp;#39;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\d+ \# \d+&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;1#23a&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;1#23&amp;#39;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The same is true for spaces:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\d+ [ ] \d+&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;1 23a&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;1 23&amp;#39;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;\d+ \  \d+&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;1 23a&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;1 23&amp;#39;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="hashes-need-extra-care"&gt;Hashes need extra care&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#hashes-need-extra-care" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;When composing regexes,
ending a pattern on the same line as a comment
might accidentally comment the following line in the enclosing pattern:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;1 # comment&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;onetwo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; 2&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onetwo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;0123&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;1&amp;#39;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onetwo&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;1 # comment 2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This can be avoided by always ending the pattern on a new line:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;1 # comment&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;onetwo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; 2&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onetwo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;0123&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VERBOSE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;12&amp;#39;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;While a bit cumbersome,
in real life most patterns would span multiple lines anyway,
so it's not really an issue.&lt;/p&gt;
&lt;p&gt;(Note that this is only needed if you use comments.)&lt;/p&gt;
&lt;h3 id="brace-quantifiers-need-to-be-escaped"&gt;Brace quantifiers need to be escaped&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#brace-quantifiers-need-to-be-escaped" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Because f‍-‍strings already use braces for replacements,
to represent brace quantifiers you must double the braces:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;m&lt;/span&gt;&lt;span class="si"&gt;{2}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;entire mm but only two of mmm&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;mm&amp;#39;, &amp;#39;mm&amp;#39;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;letter&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;m&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;letter&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;{{&lt;/span&gt;&lt;span class="s2"&gt;2&lt;/span&gt;&lt;span class="se"&gt;}}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;entire mm but only two of mmm&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;mm&amp;#39;, &amp;#39;mm&amp;#39;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="i-don-t-control-the-flags"&gt;I don't control the flags&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#i-don-t-control-the-flags" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Maybe you'd like to use verbose regexes,
but don't control the flags passed to the &lt;a class="external" href="https://docs.python.org/3/library/re.html#functions"&gt;re functions&lt;/a&gt;
(for example, because you're passing the regex to an API).&lt;/p&gt;
&lt;p&gt;Worry not! The regular expression &lt;a class="external" href="https://docs.python.org/3/library/re.html#regular-expression-syntax"&gt;syntax&lt;/a&gt; supports inline flags:&lt;/p&gt;
&lt;blockquote&gt;

&lt;dl&gt;
&lt;dt&gt;&lt;code&gt;(?aiLmsux)&lt;/code&gt;&lt;/dt&gt;
&lt;dd&gt;(One or more letters [...]) The group matches the empty string; the letters set the corresponding flags: [...] &lt;a class="external" href="https://docs.python.org/3/library/re.html#re.VERBOSE"&gt;re.X&lt;/a&gt; (verbose), for the entire regular expression. [...] This is useful if you wish to include the flags as part of the regular expression, instead of passing a flag argument to the &lt;a class="external" href="https://docs.python.org/3/library/re.html#re.compile"&gt;re.compile()&lt;/a&gt; function. Flags should be used first in the expression string.&lt;/dd&gt;
&lt;dt&gt;&lt;code&gt;(?aiLmsux-imsx:...)&lt;/code&gt;&lt;/dt&gt;
&lt;dd&gt;[...] The letters set or remove the corresponding flags [...] for the part of the expression. [...]&lt;/dd&gt;
&lt;/dl&gt;
&lt;/blockquote&gt;

&lt;p&gt;So, you can do this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;onetwo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;(?x)&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;1 # look, ma&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;2 # no flags&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onetwo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;0123&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;12&amp;#39;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... or this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;onetwo&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;(?x:&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;    1 # verbose until the close paren&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;)2&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;findall&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;onetwo&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;0123&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;12&amp;#39;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/f-re&amp;t=The%20unreasonable%20effectiveness%20of%20f%E2%80%8D-%E2%80%8Dstrings%20and%20re.VERBOSE"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=The%20unreasonable%20effectiveness%20of%20f%E2%80%8D-%E2%80%8Dstrings%20and%20re.VERBOSE%20https%3A//death.andgravity.com/f-re"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/f-re&amp;title=The%20unreasonable%20effectiveness%20of%20f%E2%80%8D-%E2%80%8Dstrings%20and%20re.VERBOSE"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/f-re"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=The%20unreasonable%20effectiveness%20of%20f%E2%80%8D-%E2%80%8Dstrings%20and%20re.VERBOSE&amp;url=https%3A//death.andgravity.com/f-re&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;h3 id="bonus-i-don-t-use-python"&gt;Bonus: I don't use Python&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bonus-i-don-t-use-python" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Lots of other languages support the inline verbose flag, too!
You can build a pattern in whichever language is more convenient,
and use it in any other one.&lt;sup class="footnote-ref" id="fnref-3"&gt;&lt;a href="#fn-3"&gt;3&lt;/a&gt;&lt;/sup&gt; Languages like...&lt;/p&gt;
&lt;p&gt;C (with &lt;a class="external" href="https://en.wikipedia.org/wiki/Perl_Compatible_Regular_Expressions"&gt;PCRE&lt;/a&gt; – and by extension, C++, PHP, and many others):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;0123&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pcregrep&lt;span class="w"&gt; &lt;/span&gt;-o&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;(?x)&lt;/span&gt;
&lt;span class="s1"&gt;1 2  # such inline&lt;/span&gt;
&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;details&gt;
&lt;summary&gt;
... yeah, the C version is actually really long, click to expand.
&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="C"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;(?x)&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;1 2  # much verbose&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;0123&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;subject_length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;strlen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;errornumber&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;PCRE2_SIZE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;erroroffset&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="n"&gt;pcre2_code&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pcre2_compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PCRE2_SPTR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;pattern&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;PCRE2_ZERO_TERMINATED&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;errornumber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;erroroffset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;pcre2_match_data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;match_data&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pcre2_match_data_create_from_pattern&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;pcre2_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PCRE2_SPTR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;subject_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;match_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;NULL&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="n"&gt;PCRE2_SIZE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;ovector&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pcre2_get_ovector_pointer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;match_data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;PCRE2_SPTR&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;substring_start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PCRE2_SPTR&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;subject&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ovector&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="kt"&gt;size_t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;substring_length&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ovector&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ovector&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="n"&gt;printf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;%.*s&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;substring_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kt"&gt;char&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="n"&gt;substring_start&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;/details&gt;

&lt;p&gt;C#:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="C#"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WriteLine&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;@&amp;quot;(?x)&lt;/span&gt;
&lt;span class="s"&gt;1 2  # wow&lt;/span&gt;
&lt;span class="s"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;Match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;0123&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;grep (only the GNU one):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;0123&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;-Po&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;(?x) 1 2  # no line&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Java (and by extension, lots of JVM languages, like Scala):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Java"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Pattern&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;(?x)\n&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;1 2  # much class\n&amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;var&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;matcher&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;0123&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;find&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;group&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Perl:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Perl"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="s"&gt;&amp;quot;0123&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=~&lt;/span&gt;&lt;span class="sr"&gt; /(?x)(&lt;/span&gt;
&lt;span class="sr"&gt;1 2  # no scare&lt;/span&gt;
&lt;span class="sr"&gt;)/&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;\n&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;PostgreSQL:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="PostgreSQL SQL dialect"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;select&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;substring&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;0123&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;$$(?x)&lt;/span&gt;
&lt;span class="s"&gt;    1 2  # such declarative&lt;/span&gt;
&lt;span class="s"&gt;    $$&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Ruby:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Ruby"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;puts&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="sr"&gt;/(?x)&lt;/span&gt;
&lt;span class="sr"&gt;1 2  # nice&lt;/span&gt;
&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;0123&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Rust:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Rust"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Regex&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="s"&gt;r&amp;quot;(?x)&lt;/span&gt;
&lt;span class="s"&gt;    1 2  # much safe&lt;/span&gt;
&lt;span class="s"&gt;    &amp;quot;&lt;/span&gt;
&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;unwrap&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="fm"&gt;println!&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;{}&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;0123&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="n"&gt;unwrap&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="n"&gt;as_str&lt;/span&gt;&lt;span class="p"&gt;());&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Swift:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Swift"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;string&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;0123&amp;quot;&lt;/span&gt;
&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;range&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;of&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s"&gt;    (?x)&lt;/span&gt;
&lt;span class="s"&gt;    1 2  # omg hi&lt;/span&gt;
&lt;span class="s"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;regularExpression&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="bp"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;range&lt;/span&gt;&lt;span class="p"&gt;!])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notable languages that don't support inline verbose flags out of the box:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;C (regex.h – POSIX regular expressions)&lt;/li&gt;
&lt;li&gt;C++ (regex)&lt;/li&gt;
&lt;li&gt;Go (regexp)&lt;/li&gt;
&lt;li&gt;Javascript&lt;/li&gt;
&lt;li&gt;Lua&lt;/li&gt;
&lt;/ul&gt;



&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        If you&amp;#39;ve made it this far, you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/aosa"&gt;
            Struggling to structure code in larger programs? Great resources a beginner might not find so easily
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="bonus-define"&gt;Bonus: DEFINE&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bonus-define" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;small&gt;(update)&lt;/small&gt;
Interestingly, &lt;a class="external" href="https://perldoc.perl.org/perlre#(DEFINE)"&gt;Perl&lt;/a&gt;, &lt;a class="external" href="https://www.pcre.org/original/doc/html/pcrepattern.html#SEC21"&gt;PCRE&lt;/a&gt;, and
the &lt;a class="external" href="https://pypi.org/project/regex/#added-define-hg-issue-152"&gt;regex&lt;/a&gt; Python library
all support reusing subpatterns &lt;em&gt;without&lt;/em&gt; string interpolation.&lt;sup class="footnote-ref" id="fnref-4"&gt;&lt;a href="#fn-4"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;To define a named subpattern, use the DEFINE pseudo-condition:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;(?(DEFINE)
    (?&amp;lt;name&amp;gt; subpattern )
    ...
)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;To use it, do a &amp;quot;subroutine&amp;quot; call:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;(?&amp;amp;subpattern)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The example at the beginning of the article would then look like this:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;(?(DEFINE)
    (?&amp;lt;code&amp;gt;
        [A-Z]*H  # prefix
        \d+      # digits
        [a-z]*   # suffix
    )
    (?&amp;lt;multicode&amp;gt;
        (?: \( \s* )?               # maybe open paren and maybe space
        (?&amp;amp;code)                    # one code
        (?: \s* \+ \s* (?&amp;amp;code) )*  # maybe followed by other codes, plus-separated
        (?: \s* [\):+] )?           # maybe space and maybe close paren or colon or plus
    )
)

( (?&amp;amp;multicode) )           # code (capture)
( .*? )                     # message (capture): everything ...
(?=                         # ... up to (but excluding) ...
    (?&amp;amp;multicode)           # ... the next code
        (?! [^\w\s] )       # (but not when followed by punctuation)
    | $                     # ... or the end
)
&lt;/code&gt;&lt;/pre&gt;

&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;In &lt;a class="external" href="https://peps.python.org/pep-0498/"&gt;PEP 498 – Literal String Interpolation&lt;/a&gt;. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;That is, since at least &lt;a class="external" href="https://docs.python.org/release/1.5.2p2/lib/Contents_of_Module_re.html#l2h-573"&gt;Python 1.5.2&lt;/a&gt;, released in 1998 –
for all except a tiny minority of Python users, that's &lt;em&gt;before&lt;/em&gt; forever. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-3"&gt;&lt;p&gt;If they both support inline flags,
they likely share most other features. &lt;a href="#fnref-3" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-4"&gt;&lt;p&gt;Thanks to &lt;a class="external" href="https://github.com/webstrand"&gt;webstrand&lt;/a&gt;
and &lt;a class="external" href="https://github.com/learnbyexample"&gt;asicsp&lt;/a&gt;
for &lt;a class="external" href="https://news.ycombinator.com/item?id=31486773"&gt;pointing it out&lt;/a&gt;! &lt;a href="#fnref-4" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/f-re" rel="alternate"/>
    <summary>... in which we look at one or two ways to make life easier when working with Python regular expressions.</summary>
    <published>2022-05-18T22:24:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/reader-2-11">
    <id>https://death.andgravity.com/reader-2-11</id>
    <title>reader 2.11 released – metadata is tags</title>
    <updated>2022-03-21T18:28:11+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;I'm happy to announce version 2.11 of &lt;strong&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/strong&gt;, a Python feed reader library.&lt;/p&gt;
&lt;h2 id="what-s-new"&gt;What's new?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-new" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Quite a lot happened since &lt;a class="internal" href="/reader-2-5"&gt;reader 2.5&lt;/a&gt;!&lt;/p&gt;
&lt;h3 id="unified-tag-api-entry-and-global-tags"&gt;Unified tag API + entry and global tags&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#unified-tag-api-entry-and-global-tags" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Tags and metadata are now the same thing, generic &lt;a class="external" href="https://reader.readthedocs.io/en/stable/guide.html#resource-tags"&gt;resource tags&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;default&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;default&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;value&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;value&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_tag&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_tags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;{&amp;#39;one&amp;#39;: &amp;#39;value&amp;#39;, &amp;#39;two&amp;#39;: None}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This means you can filter by metadata keys, and attach values to tags.&lt;/p&gt;
&lt;p&gt;Even better, tags aren't just for feeds&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt; anymore –
you can add tags to entries, and to a global namespace.&lt;/p&gt;
&lt;h3 id="memory-usage-improvements"&gt;Memory usage improvements&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#memory-usage-improvements" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;reader update&lt;/code&gt; uses about 22% less memory, owed to two changes.&lt;/p&gt;
&lt;p&gt;The first one is not in &lt;em&gt;reader&lt;/em&gt; itself,
but was contributed to &lt;a class="external" href="https://feedparser.readthedocs.io/"&gt;feedparser&lt;/a&gt;:
instead of reading the whole feed in memory to detect encoding,
use a prefix of the feed, and decode the rest on the fly.&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;The result is a ~20% decrease in &lt;code&gt;update_feeds()&lt;/code&gt; maximum resident set size
(35%, when compared to baseline!).&lt;/p&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; will vendor the patched feedparser
until the change is released upstream,
so you can reap the benefits now.&lt;/p&gt;
&lt;p&gt;The second one is parsing feeds serially,
using workers only to retrieve them.
Since parsing time is mostly spent in pure Python code,
there's no speed-up from doing it in parallel –
but each thread takes up extra memory.&lt;/p&gt;
&lt;p&gt;This decreased &lt;code&gt;update_feeds()&lt;/code&gt; memory usage by another ~20%
when using more than one worker
(but only on Linux; on macOS it's less notable).&lt;/p&gt;
&lt;h3 id="bug-fixes"&gt;Bug fixes&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bug-fixes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The way &lt;em&gt;reader&lt;/em&gt; checked SQLite has JSON support was somewhat brittle,
causing it to fail on &lt;a class="external" href="https://www.sqlite.org/releaselog/3_38_0.html"&gt;SQLite 3.38&lt;/a&gt;; &lt;em&gt;reader&lt;/em&gt; 2.11 fixes this.&lt;/p&gt;
&lt;h3 id="usability-improvements"&gt;Usability improvements&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#usability-improvements" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Among a number of smaller API improvements, now you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;filter feeds in the same way both when &lt;a class="external" href="https://reader.readthedocs.io/en/stable/api.html#reader.Reader.get_feeds"&gt;getting&lt;/a&gt; and when &lt;a class="external" href="https://reader.readthedocs.io/en/stable/api.html#reader.Reader.update_feeds"&gt;updating&lt;/a&gt; feeds
– including by tags&lt;/li&gt;
&lt;li&gt;run arbitrary actions &lt;a class="external" href="https://reader.readthedocs.io/en/stable/api.html#reader.Reader.before_feed_update_hooks"&gt;before updating a feed&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;add an existing feed without getting an exception&lt;/li&gt;
&lt;li&gt;delete a missing feed or entry without getting an exception&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For more details, see the full &lt;a class="external" href="https://reader.readthedocs.io/en/latest/changelog.html"&gt;changelog&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/reader-2-11&amp;t=reader%202.11%20released%20%E2%80%93%20metadata%20is%20tags"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=reader%202.11%20released%20%E2%80%93%20metadata%20is%20tags%20https%3A//death.andgravity.com/reader-2-11"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/reader-2-11&amp;title=reader%202.11%20released%20%E2%80%93%20metadata%20is%20tags"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/reader-2-11"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=reader%202.11%20released%20%E2%80%93%20metadata%20is%20tags&amp;url=https%3A//death.andgravity.com/reader-2-11&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;h2 id="what-is-reader"&gt;What is &lt;em&gt;reader&lt;/em&gt;?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;reader&lt;/strong&gt; takes care
of the core functionality required by a feed reader,
so you can focus on what makes &lt;strong&gt;yours&lt;/strong&gt; different.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-2-0/reader.png" alt="reader in action" /&gt;
&lt;em&gt;reader&lt;/em&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retrieve, store, and manage &lt;strong&gt;Atom&lt;/strong&gt;, &lt;strong&gt;RSS&lt;/strong&gt;, and &lt;strong&gt;JSON&lt;/strong&gt; feeds&lt;/li&gt;
&lt;li&gt;mark articles as read or important&lt;/li&gt;
&lt;li&gt;add arbitrary metadata to feeds and articles&lt;/li&gt;
&lt;li&gt;filter feeds and articles&lt;/li&gt;
&lt;li&gt;full-text search articles&lt;/li&gt;
&lt;li&gt;get statistics on feed and user activity&lt;/li&gt;
&lt;li&gt;write plugins to extend its functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...all these with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stable, clearly documented API&lt;/li&gt;
&lt;li&gt;excellent test coverage&lt;/li&gt;
&lt;li&gt;fully typed Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find out more, check out the &lt;a class="external" href="https://github.com/lemon24/reader"&gt;GitHub repo&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/"&gt;docs&lt;/a&gt;,
or give the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/tutorial.html"&gt;tutorial&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2 id="why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-use-a-feed-reader-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Have you been unhappy with existing feed readers and wanted to make your own, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never knew where to start?&lt;/li&gt;
&lt;li&gt;it seemed like too much work?&lt;/li&gt;
&lt;li&gt;you don't like writing backend code?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you already working with &lt;a class="external" href="https://feedparser.readthedocs.io/"&gt;feedparser&lt;/a&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;want an easier way to store, filter, sort and search feeds and entries?&lt;/li&gt;
&lt;li&gt;want to get back type-annotated objects instead of dicts?&lt;/li&gt;
&lt;li&gt;want to restrict or deny file-system access?&lt;/li&gt;
&lt;li&gt;want to change the way feeds are retrieved by using &lt;a class="external" href="https://requests.readthedocs.io"&gt;Requests&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to also support &lt;a class="external" href="https://jsonfeed.org/"&gt;JSON Feed&lt;/a&gt;?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... while still supporting all the feed types feedparser does?&lt;/p&gt;
&lt;p&gt;If you answered yes to any of the above, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;h2 id="why-make-your-own-feed-reader"&gt;Why make your own feed reader?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-make-your-own-feed-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;have full control over your data&lt;/li&gt;
&lt;li&gt;control what features it has or doesn't have&lt;/li&gt;
&lt;li&gt;decide how much you pay for it&lt;/li&gt;
&lt;li&gt;make sure it doesn't get closed while you're still using it&lt;/li&gt;
&lt;li&gt;really, it's &lt;a class="external" href="https://rachelbythebay.com/w/2011/10/26/fred/"&gt;easier than you think&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously, this may not be your cup of tea, but if it is, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;The old feed-specific tag and metadata methods are still available,
but are &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-2-8"&gt;deprecated&lt;/a&gt; and will be removed in version 3.0. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;If you'd like to read more about the whole thing, &lt;a class="internal" href="/about#contact"&gt;drop me a line&lt;/a&gt;. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/reader-2-11" rel="alternate"/>
    <published>2022-03-18T18:35:10+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/yaml-unknown-tag">
    <id>https://death.andgravity.com/yaml-unknown-tag</id>
    <title>yaml: could not determine a constructor for the tag</title>
    <updated>2022-02-22T22:02:22+00:00</updated>
    <content type="html">&lt;p&gt;So you're trying to read some YAML using &lt;a class="external" href="https://github.com/yaml/pyyaml"&gt;PyYAML&lt;/a&gt;,
and get an exception like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!!python/tuple [0,0]&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;yaml.constructor.ConstructorError&lt;/span&gt;: &lt;span class="n"&gt;could not determine a constructor for the tag &amp;#39;tag:yaml.org,2002:python/tuple&amp;#39;&lt;/span&gt;
&lt;span class="x"&gt;  in &amp;quot;&amp;lt;unicode string&amp;gt;&amp;quot;, line 1, column 1:&lt;/span&gt;
&lt;span class="x"&gt;    !!python/tuple [0,0]&lt;/span&gt;
&lt;span class="x"&gt;    ^&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... or like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!GetAZs us-east-1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;yaml.constructor.ConstructorError&lt;/span&gt;: &lt;span class="n"&gt;could not determine a constructor for the tag &amp;#39;!GetAZs&amp;#39;&lt;/span&gt;
&lt;span class="x"&gt;  in &amp;quot;&amp;lt;unicode string&amp;gt;&amp;quot;, line 1, column 1:&lt;/span&gt;
&lt;span class="x"&gt;    !GetAZs us-east-1&lt;/span&gt;
&lt;span class="x"&gt;    ^&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="what-does-it-mean"&gt;What does it mean?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-does-it-mean" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;First, a bit of background.&lt;/p&gt;
&lt;p&gt;On top of basic types (strings, integers, sequences, and so on),
YAML can represent native and user-defined data structures.
To denote the type of a node, you mark it with an explicit &lt;a class="external" href="https://yaml.org/spec/1.2.2/#tags"&gt;tag&lt;/a&gt;.
Even basic types end up with a tag; the following are all equivalent:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;[implicit]&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;implicit&amp;#39;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!!seq [global, shorthand]&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;global&amp;#39;, &amp;#39;shorthand&amp;#39;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!&amp;lt;tag:yaml.org,2002:seq&amp;gt; [global, full]&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;global&amp;#39;, &amp;#39;full&amp;#39;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The errors above both mean that the loader encountered an explicit tag,
but doesn't know how to &lt;em&gt;construct&lt;/em&gt; objects with that tag.&lt;/p&gt;
&lt;h2 id="why-does-this-happen"&gt;Why does this happen?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-does-this-happen" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;!!python/tuple&lt;/code&gt; is a language-specific tag
corresponding to a Python native data structure (a tuple).
However, &lt;code&gt;safe_load()&lt;/code&gt; resolves only basic YAML tags,
known to be safe for untrusted input.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;!GetAZs&lt;/code&gt; is an application-specific tag
(in this case, specific to AWS CloudFormation).
There's no way for PyYAML to know about it without being told explicitly.&lt;/p&gt;
&lt;p&gt;This is by design – from the &lt;a class="external" href="https://yaml.org/spec/1.2.2/#332-resolved-tags"&gt;spec&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;That said, &lt;strong&gt;tag resolution is specific to the application&lt;/strong&gt;. YAML processors should therefore provide a mechanism allowing the application to override and expand these default tag resolution rules.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="what-now"&gt;What now?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-now" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id="python-specific-tags"&gt;Python-specific tags&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#python-specific-tags" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;For Python-specific tags,
you can use &lt;code&gt;full_load()&lt;/code&gt;,
which resolves all tags &lt;em&gt;except&lt;/em&gt; those known to be unsafe;
this includes all the  tags listed
&lt;a class="external" href="https://pyyaml.org/wiki/PyYAMLDocumentation#yaml-tags-and-python-types"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You could also use &lt;code&gt;unsafe_load()&lt;/code&gt;, but most of the time it's not what you want:&lt;/p&gt;
&lt;section class="admonition warning"&gt;
&lt;p class="admonition-title"&gt;Warning&lt;/p&gt;
&lt;p&gt;&lt;code&gt;yaml.unsafe_load()&lt;/code&gt; is &lt;strong&gt;unsafe&lt;/strong&gt; for &lt;strong&gt;untrusted data&lt;/strong&gt;,
because it allows &lt;strong&gt;running arbitrary code&lt;/strong&gt;.
Consider using &lt;code&gt;safe_load()&lt;/code&gt; or &lt;code&gt;full_load()&lt;/code&gt; instead.&lt;/p&gt;
&lt;p&gt;For example, you can do this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unsafe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!!python/object/new:os.system [echo WOOSH. YOU HAVE been compromised]&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;WOOSH. YOU HAVE been compromised&lt;/span&gt;
&lt;span class="go"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;There were a bunch of &lt;a class="external" href="https://www.cvedetails.com/vulnerability-list/vendor_id-13115/Pyyaml.html"&gt;CVEs&lt;/a&gt; about it.&lt;/p&gt;
&lt;/section&gt;
&lt;h3 id="application-specific-tags"&gt;Application-specific tags&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#application-specific-tags" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;For application-specific tags,
you can define a constructor for the tag:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;GetAZs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;region&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SafeLoader&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;construct_GetAZs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GetAZs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;construct_scalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;!GetAZs&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;construct_GetAZs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here, we're wrapping the value in a dataclass,
to indicate is isn't just a simple string.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;We are subclassing SafeLoader because calling &lt;code&gt;add_constructor()&lt;/code&gt; on it
would modify it in-place, for &lt;em&gt;everyone&lt;/em&gt;, which isn't necessarily great;
imagine getting a GetAZs from &lt;code&gt;safe_load()&lt;/code&gt;,
when you were expecting only built-in types.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;To use it, pass the loader class to &lt;code&gt;load()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!GetAZs us-east-1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;GetAZs(region=&amp;#39;us-east-1&amp;#39;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Of course, you don't have to store the value,
you can &lt;em&gt;do something&lt;/em&gt; with it
– after all, that's what CloudFormation does:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;KNOWN_AZS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;us-east-1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;us-east-1a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;us-east-1b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;us-east-1c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;us-east-1d&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;us-east-1e&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;eu-west-1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;eu-west-1a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;eu-west-1b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;eu-west-1c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;construct_GetAZs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;construct_scalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;KNOWN_AZS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;constructor&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ConstructorError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;GetAZs got unknown region &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;start_mark&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;KNOWN_AZS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;!GetAZs&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;construct_GetAZs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!GetAZs us-east-1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;us-east-1a&amp;#39;, &amp;#39;us-east-1b&amp;#39;, &amp;#39;us-east-1c&amp;#39;, &amp;#39;us-east-1d&amp;#39;, &amp;#39;us-east-1e&amp;#39;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="but-i-don-t-know-the-tags-in-advance"&gt;But I don't know the tags in advance&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#but-i-don-t-know-the-tags-in-advance" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;For the above to work, you need to register constructors for each expected tag.&lt;/p&gt;
&lt;p&gt;But sometimes
you don't know the tags in advance,
or there's too many of them,
or you just want to access the data, without caring what it means
(for example, because you just want to change a little thing and write it back out).&lt;/p&gt;
&lt;p&gt;YAML allows you to register a catch-all constructor for unknown tags
...but you still need to implement some sort of generic wrapper to go with it.&lt;/p&gt;
&lt;p&gt;Luckily, I've already written &lt;strong&gt;&lt;a class="internal" href="/any-yaml"&gt;a whole article&lt;/a&gt;&lt;/strong&gt; on how to do that,
complete with code:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!GetAZs us-east-1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;Tagged(&amp;#39;!GetAZs&amp;#39;, &amp;#39;us-east-1&amp;#39;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It works with arbitrarily nested YAML:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;Properties:&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;  ImageId: !FindInMap [RegionMap, !Ref &amp;#39;AWS::Region&amp;#39;, HVM64]&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="go"&gt;{&lt;/span&gt;
&lt;span class="go"&gt;    &amp;#39;Properties&amp;#39;: {&lt;/span&gt;
&lt;span class="go"&gt;        &amp;#39;ImageId&amp;#39;: Tagged(&lt;/span&gt;
&lt;span class="go"&gt;            &amp;#39;!FindInMap&amp;#39;,&lt;/span&gt;
&lt;span class="go"&gt;            [&amp;#39;RegionMap&amp;#39;, Tagged(&amp;#39;!Ref&amp;#39;, &amp;#39;AWS::Region&amp;#39;), &amp;#39;HVM64&amp;#39;]&lt;/span&gt;
&lt;span class="go"&gt;        )&lt;/span&gt;
&lt;span class="go"&gt;    }&lt;/span&gt;
&lt;span class="go"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... allows you to ignore tags most of the time:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Properties&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ImageId&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;HVMG2&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... and can output tagged YAML too:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dumper&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Dumper&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;Properties:&lt;/span&gt;
&lt;span class="go"&gt;  ImageId: !FindInMap&lt;/span&gt;
&lt;span class="go"&gt;  - RegionMap&lt;/span&gt;
&lt;span class="go"&gt;  - !Ref &amp;#39;AWS::Region&amp;#39;&lt;/span&gt;
&lt;span class="go"&gt;  - HVMG2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Check it out: &lt;a class="internal" href="/any-yaml"&gt;Dealing with YAML with arbitrary tags in Python&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/yaml-unknown-tag&amp;t=yaml%3A%20could%20not%20determine%20a%20constructor%20for%20the%20tag"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=yaml%3A%20could%20not%20determine%20a%20constructor%20for%20the%20tag%20https%3A//death.andgravity.com/yaml-unknown-tag"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/yaml-unknown-tag&amp;title=yaml%3A%20could%20not%20determine%20a%20constructor%20for%20the%20tag"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/yaml-unknown-tag"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=yaml%3A%20could%20not%20determine%20a%20constructor%20for%20the%20tag&amp;url=https%3A//death.andgravity.com/yaml-unknown-tag&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


</content>
    <link href="https://death.andgravity.com/yaml-unknown-tag" rel="alternate"/>
    <summary>... in which you'll find out what "could not determine a constructor for the tag" PyYAML errors mean, why do they happen, and what you can do about it.</summary>
    <published>2022-02-22T22:02:22+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/any-yaml">
    <id>https://death.andgravity.com/any-yaml</id>
    <title>Dealing with YAML with arbitrary tags in Python</title>
    <updated>2022-02-07T15:21:00+00:00</updated>
    <content type="html">&lt;p&gt;... in which we use &lt;a class="external" href="https://github.com/yaml/pyyaml"&gt;PyYAML&lt;/a&gt; to &lt;em&gt;safely&lt;/em&gt; read and write YAML with &lt;em&gt;any&lt;/em&gt; tags,
in a way that's as straightforward as interacting with built-in types.&lt;/p&gt;
&lt;p&gt;If you're in a hurry,
you can find the code &lt;a class="anchor" href="#conclusion"&gt;at the end&lt;/a&gt;.&lt;/p&gt;
&lt;details class="toc"&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#why-is-this-useful"&gt;Why is this useful?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#a-note-on-pyyaml-extensibility"&gt;A note on PyYAML extensibility&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#preserving-tags"&gt;Preserving tags&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#constructing-unknown-objects"&gt;Constructing unknown objects&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#a-better-wrapper"&gt;A better wrapper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#representing-tagged-objects"&gt;Representing tagged objects&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#unhashable-keys"&gt;Unhashable keys&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#constructing-pairs"&gt;Constructing pairs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#representing-pairs"&gt;Representing pairs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#conclusion"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#bonus-hashable-wrapper"&gt;Bonus: hashable wrapper&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#bonus-broken-yaml"&gt;Bonus: broken YAML&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="why-is-this-useful"&gt;Why is this useful?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-is-this-useful" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;People mostly use YAML as a friendlier alternative to JSON&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;,
but it can do way more.&lt;/p&gt;
&lt;p&gt;Among others, it can &lt;em&gt;natively&lt;/em&gt; represent user-defined and native data structures.&lt;/p&gt;
&lt;p&gt;Say you need to read &lt;em&gt;(or write)&lt;/em&gt; an AWS CloudFormation &lt;a class="external" href="https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-ec2.html"&gt;template&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="YAML"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nt"&gt;EC2Instance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;AWS::EC2::Instance&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;Properties&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;ImageId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!FindInMap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nv"&gt;AWSRegionArch2AMI&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="kt"&gt;!Ref&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;#39;AWS::Region&amp;#39;&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="kt"&gt;!FindInMap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;AWSInstanceType2Arch&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!Ref&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;InstanceType&lt;/span&gt;&lt;span class="p p-Indicator"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;Arch&lt;/span&gt;&lt;span class="p p-Indicator"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p p-Indicator"&gt;]&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nt"&gt;InstanceType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;!Ref&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="l l-Scalar l-Scalar-Plain"&gt;InstanceType&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;yaml.constructor.ConstructorError&lt;/span&gt;: &lt;span class="n"&gt;could not determine a constructor for the tag &amp;#39;!FindInMap&amp;#39;&lt;/span&gt;
&lt;span class="x"&gt;  in &amp;quot;&amp;lt;unicode string&amp;gt;&amp;quot;, line 4, column 14:&lt;/span&gt;
&lt;span class="x"&gt;        ImageId: !FindInMap [&lt;/span&gt;
&lt;span class="x"&gt;                 ^&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... or, you need to &lt;em&gt;safely&lt;/em&gt; read untrusted YAML
that represents Python objects:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="YAML"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kt"&gt;!!python/object/new:module.Class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;{&lt;/span&gt;&lt;span class="nt"&gt; attribute&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;value&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p p-Indicator"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;safe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;yaml.constructor.ConstructorError&lt;/span&gt;: &lt;span class="n"&gt;could not determine a constructor for the tag &amp;#39;tag:yaml.org,2002:python/object/new:module.Class&amp;#39;&lt;/span&gt;
&lt;span class="x"&gt;  in &amp;quot;&amp;lt;unicode string&amp;gt;&amp;quot;, line 1, column 1:&lt;/span&gt;
&lt;span class="x"&gt;    !!python/object/new:module.Class ...&lt;/span&gt;
&lt;span class="x"&gt;    ^&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;section class="admonition warning"&gt;
&lt;p class="admonition-title"&gt;Warning&lt;/p&gt;
&lt;p&gt;Historically, &lt;code&gt;yaml.load(thing)&lt;/code&gt; was &lt;strong&gt;unsafe&lt;/strong&gt; for &lt;strong&gt;untrusted data&lt;/strong&gt;,
because it allowed &lt;strong&gt;running arbitrary code&lt;/strong&gt;.
Consider using &lt;code&gt;safe_load()&lt;/code&gt; instead.&lt;/p&gt;
 &lt;details&gt;
 &lt;summary&gt;Details.&lt;/summary&gt;

&lt;p&gt;For example, you could do this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!!python/object/new:os.system [echo WOOSH. YOU HAVE been compromised]&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;WOOSH. YOU HAVE been compromised&lt;/span&gt;
&lt;span class="go"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;There were a bunch of &lt;a class="external" href="https://www.cvedetails.com/vulnerability-list/vendor_id-13115/Pyyaml.html"&gt;CVEs&lt;/a&gt; about it.&lt;/p&gt;
&lt;p&gt;To address the issue, &lt;code&gt;load()&lt;/code&gt; requires an explicit &lt;code&gt;Loader&lt;/code&gt; since PyYAML 6.
Also, version 5 added two new functions and corresponding loaders:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;full_load()&lt;/code&gt; resolves all tags except those known to be unsafe
(note that this was broken before 5.4, and thus vulnerable)&lt;/li&gt;
&lt;li&gt;&lt;code&gt;unsafe_load()&lt;/code&gt; resolves all tags, even those known to be unsafe
(the old &lt;code&gt;load()&lt;/code&gt; behavior)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;code&gt;safe_load()&lt;/code&gt; resolves only basic tags, remaining the safest.&lt;/p&gt;
 &lt;/details&gt;

&lt;/section&gt;
&lt;hr /&gt;
&lt;p&gt;Can I just get the data, without it being turned into objects?&lt;/p&gt;
&lt;p&gt;You can! The YAML spec &lt;a class="external" href="https://yaml.org/spec/1.2.2/#334-available-tags"&gt;says&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;In a given processing environment, there need not be an available native type corresponding to a given tag. If a node’s tag is unavailable, a YAML processor will not be able to construct a native data structure for it. In this case, &lt;strong&gt;a complete representation may still be composed&lt;/strong&gt; and an application may wish to use this representation directly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And PyYAML obliges:&lt;/p&gt;
&lt;!--

text = """\
one: !myscalar string
two: !mysequence [1, 2]
"""
--&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;one: !myscalar string&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;two: !mysequence [1, 2]&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;compose&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;MappingNode(&lt;/span&gt;
&lt;span class="go"&gt;    tag=&amp;#39;tag:yaml.org,2002:map&amp;#39;,&lt;/span&gt;
&lt;span class="go"&gt;    value=[&lt;/span&gt;
&lt;span class="go"&gt;        (&lt;/span&gt;
&lt;span class="go"&gt;            ScalarNode(tag=&amp;#39;tag:yaml.org,2002:str&amp;#39;, value=&amp;#39;one&amp;#39;),&lt;/span&gt;
&lt;span class="go"&gt;            ScalarNode(tag=&amp;#39;!myscalar&amp;#39;, value=&amp;#39;string&amp;#39;),&lt;/span&gt;
&lt;span class="go"&gt;        ),&lt;/span&gt;
&lt;span class="go"&gt;        (&lt;/span&gt;
&lt;span class="go"&gt;            ScalarNode(tag=&amp;#39;tag:yaml.org,2002:str&amp;#39;, value=&amp;#39;two&amp;#39;),&lt;/span&gt;
&lt;span class="go"&gt;            SequenceNode(&lt;/span&gt;
&lt;span class="go"&gt;                tag=&amp;#39;!mysequence&amp;#39;,&lt;/span&gt;
&lt;span class="go"&gt;                value=[&lt;/span&gt;
&lt;span class="go"&gt;                    ScalarNode(tag=&amp;#39;tag:yaml.org,2002:int&amp;#39;, value=&amp;#39;1&amp;#39;),&lt;/span&gt;
&lt;span class="go"&gt;                    ScalarNode(tag=&amp;#39;tag:yaml.org,2002:int&amp;#39;, value=&amp;#39;2&amp;#39;),&lt;/span&gt;
&lt;span class="go"&gt;                ],&lt;/span&gt;
&lt;span class="go"&gt;            ),&lt;/span&gt;
&lt;span class="go"&gt;        ),&lt;/span&gt;
&lt;span class="go"&gt;    ],&lt;/span&gt;
&lt;span class="go"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;serialize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;one: !myscalar &amp;#39;string&amp;#39;&lt;/span&gt;
&lt;span class="go"&gt;two: !mysequence [1, 2]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... the spec didn't say the representation has to be concise. ¯\_(ツ)_/¯&lt;/p&gt;
&lt;p&gt;Here's how YAML processing works, to give you an idea what we're looking at:&lt;/p&gt;
&lt;figure class="figure" id="yaml-processing-overview-diagram"&gt;
&lt;img class="img-responsive" src="/_file/any-yaml/overview2.svg" alt="YAML Processing Overview Diagram" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
&lt;a class="external" href="https://yaml.org/spec/1.2.2/#31-processes"&gt;YAML Processing Overview&lt;/a&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;The output of &lt;code&gt;compose()&lt;/code&gt; above is the representation (node graph).&lt;/p&gt;
&lt;p&gt;From that, &lt;code&gt;safe_load()&lt;/code&gt; does its best to construct objects,
but it can't do anything for tags it doesn't know about.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;There must be a better way!&lt;/p&gt;
&lt;p&gt;Thankfully, the spec also &lt;a class="external" href="https://yaml.org/spec/1.2.2/#332-resolved-tags"&gt;says&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;That said, tag resolution is specific to the application. YAML processors should therefore provide &lt;strong&gt;a mechanism allowing the application to override and expand&lt;/strong&gt; these default tag resolution rules.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;We'll use this mechanism to convert tagged nodes to almost-native types,
while preserving the tags.&lt;/p&gt;
&lt;h2 id="a-note-on-pyyaml-extensibility"&gt;A note on PyYAML extensibility&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#a-note-on-pyyaml-extensibility" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;PyYAML is a bit unusual.&lt;/p&gt;
&lt;p&gt;For each processing direction, you have a corresponding Loader/Dumper class.&lt;/p&gt;
&lt;p&gt;For each processing step,
you can add callbacks,
stored in class-level registries.&lt;/p&gt;
&lt;p&gt;The callbacks are method-like –
they receive the Loader/Dumper as the first argument:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Dice&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;namedtuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Dice&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;a b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;dice_representer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dumper&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dumper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;represent_scalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;u&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;!dice&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;u&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;d&lt;/span&gt;&lt;span class="si"&gt;%s&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Dumper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_representer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Dice&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dice_representer&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You may notice the &lt;code&gt;add_...()&lt;/code&gt; methods modify the class in-place,
for &lt;em&gt;everyone&lt;/em&gt;,
which isn't necessarily great;
imagine getting a Dice from &lt;code&gt;safe_load()&lt;/code&gt;,
when you were expecting only built-in types.&lt;/p&gt;
&lt;p&gt;We can avoid this by subclassing,
since the registry is copied from the parent.
Note that because of how &lt;a class="external" href="https://github.com/yaml/pyyaml/blob/6.0/lib/yaml/representer.py#L65-L69"&gt;copying is implemented&lt;/a&gt;,
registries from two direct parents are &lt;em&gt;not&lt;/em&gt; merged –
you only get the registry of the first parent in the &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-method-resolution-order"&gt;MRO&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;So, we'll start by subclassing SafeLoader/Dumper:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;4&lt;/span&gt;
&lt;span class="normal"&gt;5&lt;/span&gt;
&lt;span class="normal"&gt;6&lt;/span&gt;
&lt;span class="normal"&gt;7&lt;/span&gt;
&lt;span class="normal"&gt;8&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SafeLoader&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Dumper&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SafeDumper&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;h2 id="preserving-tags"&gt;Preserving tags&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#preserving-tags" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id="constructing-unknown-objects"&gt;Constructing unknown objects&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#constructing-unknown-objects" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;For now, we can use named tuples for objects with unknown tags,
since &lt;a class="internal" href="/namedtuples#the-data-is-naturally-a-tuple"&gt;they are naturally&lt;/a&gt;
tag/value pairs:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Tagged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Tag or no tag, all YAML nodes are either a scalar, a sequence, or a mapping.
For unknown tags, we delegate construction to the loader's default constructors,
and wrap the resulting value:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;construct_undefined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ScalarNode&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;construct_scalar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SequenceNode&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;construct_sequence&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nodes&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MappingNode&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;construct_mapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;unexpected node: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Tagged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;construct_undefined&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Constructors are registered by tag, with None meaning &amp;quot;unknown&amp;quot;.&lt;/p&gt;
&lt;p&gt;Things look much better already:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;{&lt;/span&gt;
&lt;span class="go"&gt;    &amp;#39;one&amp;#39;: Tagged(tag=&amp;#39;!myscalar&amp;#39;, value=&amp;#39;string&amp;#39;),&lt;/span&gt;
&lt;span class="go"&gt;    &amp;#39;two&amp;#39;: Tagged(tag=&amp;#39;!mysequence&amp;#39;, value=[1, 2]),&lt;/span&gt;
&lt;span class="go"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="a-better-wrapper"&gt;A better wrapper&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#a-better-wrapper" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;That's nice,
but every time we use any value,
we have to check if it's tagged,
and then go through &lt;code&gt;value&lt;/code&gt; if is:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;!myscalar&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;STRING&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We could &lt;em&gt;subclass&lt;/em&gt; the Python types corresponding to core YAML tags
(str, list, and so on),
and add a &lt;code&gt;tag&lt;/code&gt; attribute to each.
We could subclass most of them, anyway
– neither &lt;code&gt;bool&lt;/code&gt; nor &lt;code&gt;NoneType&lt;/code&gt; &lt;em&gt;can&lt;/em&gt; be subclassed.&lt;/p&gt;
&lt;p&gt;Or, we could &lt;em&gt;wrap&lt;/em&gt; tagged objects
in a class with the same interface,
that delegates method calls and attribute access to the wrapee,
with a &lt;code&gt;tag&lt;/code&gt; attribute on top.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;This is known as the &lt;a class="external" href="https://python-patterns.guide/gang-of-four/decorator-pattern/"&gt;decorator pattern&lt;/a&gt; design pattern
(not to be confused with Python &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-decorator"&gt;decorators&lt;/a&gt;).
&lt;a class="internal" href="/caching-methods#but-i-don-t-instantiate-the-client"&gt;Caching and retrying methods dynamically&lt;/a&gt;
is another interesting use for it.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;Doing this naively entails writing one wrapper per type,
with one wrapper method per method and one property per attribute.
That's even worse than subclassing!&lt;/p&gt;
&lt;p&gt;There must be a better way!&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Of course, this is Python, so there is.&lt;/p&gt;
&lt;p&gt;We can use an object proxy instead (also known as &amp;quot;dynamic wrapper&amp;quot;).
While &lt;a class="external" href="https://python-patterns.guide/gang-of-four/decorator-pattern/#caveat-wrapping-doesnt-actually-work"&gt;they're not perfect&lt;/a&gt; in general,
the one &lt;a class="external" href="https://wrapt.readthedocs.io/en/latest/wrappers.html#object-proxy"&gt;wrapt&lt;/a&gt; provides is damn near perfect enough&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Tagged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wrapt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ObjectProxy&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;

    &lt;span class="c1"&gt;# tell wrapt to set the attribute on the proxy, not the wrapped object&lt;/span&gt;
    &lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;wrapped&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;wrapped&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;tag&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__repr__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__wrapped__&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;)&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;{&lt;/span&gt;
&lt;span class="go"&gt;    &amp;#39;one&amp;#39;: Tagged(&amp;#39;!myscalar&amp;#39;, &amp;#39;string&amp;#39;),&lt;/span&gt;
&lt;span class="go"&gt;    &amp;#39;two&amp;#39;: Tagged(&amp;#39;!mysequence&amp;#39;, [1, 2]),&lt;/span&gt;
&lt;span class="go"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The proxy behaves identically to the proxied object:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;!myscalar&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;upper&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;STRING&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;str&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;...up to and including fancy things like &lt;a class="external" href="https://docs.python.org/3/library/functions.html#isinstance"&gt;isinstance()&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;True&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tagged&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;True&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And &lt;em&gt;now&lt;/em&gt; you don't have to care about tags if you don't want to.&lt;/p&gt;
&lt;h3 id="representing-tagged-objects"&gt;Representing tagged objects&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#representing-tagged-objects" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The trip back is exactly the same,
but much shorter:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;represent_tagged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Tagged&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
    &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;represent_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;__wrapped__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tag&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;

&lt;span class="n"&gt;Dumper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_representer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Tagged&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;represent_tagged&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Representers are registered by type.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Tagged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;!hello&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;world&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Dumper&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Dumper&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;!hello &amp;#39;world&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;details&gt;
&lt;summary&gt;Let's mark the occasion with some tests.&lt;/summary&gt;

&lt;p&gt;Since we still have stuff to do, we parametrize the tests from the start.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;BASIC_TEXT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;one: !myscalar string&lt;/span&gt;
&lt;span class="s2"&gt;two: !mymapping&lt;/span&gt;
&lt;span class="s2"&gt;  three: !mysequence [1, 2]&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;BASIC_DATA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Tagged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;!myscalar&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;string&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Tagged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;!mymapping&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;three&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Tagged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;!mysequence&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])}),&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="n"&gt;DATA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASIC_TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BASIC_DATA&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Loading works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;text, data&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;DATA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And dumping works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;text&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;DATA&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_roundtrip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Dumper&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Dumper&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;... but only for known types:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_dump_error&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;representer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;RepresenterError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;Dumper&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Dumper&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;/details&gt;

&lt;h2 id="unhashable-keys"&gt;Unhashable keys&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#unhashable-keys" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Let's try &lt;a class="external" href="https://pyyaml.org/wiki/PyYAMLDocumentation#block-mappings"&gt;an example&lt;/a&gt; from the PyYAML documentation:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;? !!python/tuple [0,0]&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;: The Hero&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;? !!python/tuple [1,0]&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;: Treasure&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;? !!python/tuple [1,1]&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;: The Dragon&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;!--

[^7]: For this specific example, we could've used `yaml.FullLoader`,
  which does support tuples,
  but that doesn't really address the root of the problem.

--&gt;

&lt;p&gt;This is supposed to result in something like:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unsafe_load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;{(0, 0): &amp;#39;The Hero&amp;#39;, (1, 0): &amp;#39;Treasure&amp;#39;, (1, 1): &amp;#39;The Dragon&amp;#39;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Instead, we get:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;unhashable type: &amp;#39;list&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That's because the keys are tagged lists, and neither type is &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-hashable"&gt;hashable&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!!python/tuple [0,0]&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;Tagged(&amp;#39;tag:yaml.org,2002:python/tuple&amp;#39;, [0, 0])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This limitation comes from how Python dicts are implemented,&lt;sup class="footnote-ref" id="fnref-3"&gt;&lt;a href="#fn-3"&gt;3&lt;/a&gt;&lt;/sup&gt;
not from YAML;
quoting from the &lt;a class="external" href="https://yaml.org/spec/1.2.2/#mapping"&gt;spec&lt;/a&gt; again:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The content of a mapping node is an unordered set of key/value node pairs, with the restriction that each of the keys is unique. &lt;strong&gt;YAML places no further restrictions on the nodes.&lt;/strong&gt; In particular, &lt;strong&gt;keys may be arbitrary nodes&lt;/strong&gt;, the same node may be used as the value of several key/value pairs and a mapping could even contain itself as a key or a value.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h3 id="constructing-pairs"&gt;Constructing pairs&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#constructing-pairs" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;What now?&lt;/p&gt;
&lt;p&gt;Same strategy as before: wrap the things we can't handle.&lt;/p&gt;
&lt;p&gt;Specifically,
whenever we have a mapping with unhashable keys,
we return a list of pairs instead.
To tell it apart from plain lists,
we use a subclass:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Pairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__repr__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;(&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__repr__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;)&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Again, we let the loader do most of the work:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;53&lt;/span&gt;
&lt;span class="normal"&gt;54&lt;/span&gt;
&lt;span class="normal"&gt;55&lt;/span&gt;
&lt;span class="normal"&gt;56&lt;/span&gt;
&lt;span class="normal"&gt;57&lt;/span&gt;
&lt;span class="normal"&gt;58&lt;/span&gt;
&lt;span class="normal"&gt;59&lt;/span&gt;
&lt;span class="normal"&gt;60&lt;/span&gt;
&lt;span class="normal"&gt;61&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;construct_mapping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;construct_pairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;node&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Pairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;construct_mapping&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;construct_mapping&lt;/span&gt;
&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_constructor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;tag:yaml.org,2002:map&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;construct_mapping&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We set &lt;code&gt;construct_mapping&lt;/code&gt; so that any other Loader constructor
wanting to make a mapping gets to use it
(like our own &lt;code&gt;construct_undefined()&lt;/code&gt; above).
Don't be fooled by the assignment,
it's a method like any other&lt;sup class="footnote-ref" id="fnref-4"&gt;&lt;a href="#fn-4"&gt;4&lt;/a&gt;&lt;/sup&gt;
...but we're changing the class from outside anyway,
so it's best to stay consistent.&lt;/p&gt;
&lt;p&gt;Note that overriding &lt;code&gt;construct_mapping()&lt;/code&gt; is not enough:
we have to register the constructor explictly,
otherwise SafeDumper's &lt;code&gt;construct_mapping()&lt;/code&gt; will be used
(since that's what was in the registry before).&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;In case you're wondering,
this feature is orthogonal from handling unknown tags;
we could have used different classes for them.
However, as mentioned before,
the constructor registry breaks multiple inheritance,
so we couldn't use the two features &lt;em&gt;together&lt;/em&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;Anyway, it works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;Pairs(&lt;/span&gt;
&lt;span class="go"&gt;    [&lt;/span&gt;
&lt;span class="go"&gt;        (Tagged(&amp;#39;tag:yaml.org,2002:python/tuple&amp;#39;, [0, 0]), &amp;#39;The Hero&amp;#39;),&lt;/span&gt;
&lt;span class="go"&gt;        (Tagged(&amp;#39;tag:yaml.org,2002:python/tuple&amp;#39;, [1, 0]), &amp;#39;Treasure&amp;#39;),&lt;/span&gt;
&lt;span class="go"&gt;        (Tagged(&amp;#39;tag:yaml.org,2002:python/tuple&amp;#39;, [1, 1]), &amp;#39;The Dragon&amp;#39;),&lt;/span&gt;
&lt;span class="go"&gt;    ]&lt;/span&gt;
&lt;span class="go"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="representing-pairs"&gt;Representing pairs&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#representing-pairs" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Like before, the trip back is short and uneventful:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;64&lt;/span&gt;
&lt;span class="normal"&gt;65&lt;/span&gt;
&lt;span class="normal"&gt;66&lt;/span&gt;
&lt;span class="normal"&gt;67&lt;/span&gt;
&lt;span class="normal"&gt;68&lt;/span&gt;
&lt;span class="normal"&gt;69&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;represent_pairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Pairs&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;
    &lt;span class="n"&gt;node&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;represent_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;node&lt;/span&gt;

&lt;span class="n"&gt;Dumper&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_representer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Pairs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;represent_pairs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Pairs&lt;/span&gt;&lt;span class="p"&gt;([([],&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)]),&lt;/span&gt; &lt;span class="n"&gt;Dumper&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Dumper&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;[]: one&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;details&gt;
&lt;summary&gt;Let's test this more thoroughly.&lt;/summary&gt;

&lt;p&gt;Because the tests are parametrized, we just need to add more data:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;UNHASHABLE_TEXT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;[0,0]: one&lt;/span&gt;
&lt;span class="s2"&gt;!key &lt;/span&gt;&lt;span class="si"&gt;{0: 1}&lt;/span&gt;&lt;span class="s2"&gt;: {[]: !value three}&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;UNHASHABLE_DATA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Pairs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Tagged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;!key&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;}),&lt;/span&gt; &lt;span class="n"&gt;Pairs&lt;/span&gt;&lt;span class="p"&gt;([([],&lt;/span&gt; &lt;span class="n"&gt;Tagged&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;!value&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;three&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))])),&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;DATA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;BASIC_TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;BASIC_DATA&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="hll"&gt;    &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;UNHASHABLE_TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;UNHASHABLE_DATA&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;/details&gt;

&lt;h2 id="conclusion"&gt;Conclusion&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#conclusion" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;YAML is extensible by design.
I hope that besides what it says on the tin,
this article shed some light on how to customize PyYAML for your own purposes,
and that you've learned at least one new Python thing.&lt;/p&gt;
&lt;p&gt;You can get the code &lt;a class="attachment" href="/_file/any-yaml/any_yaml.py"&gt;here&lt;/a&gt;,
and the tests &lt;a class="attachment" href="/_file/any-yaml/test_any_yaml.py"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/any-yaml&amp;t=Dealing%20with%20YAML%20with%20arbitrary%20tags%20in%20Python"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Dealing%20with%20YAML%20with%20arbitrary%20tags%20in%20Python%20https%3A//death.andgravity.com/any-yaml"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/any-yaml&amp;title=Dealing%20with%20YAML%20with%20arbitrary%20tags%20in%20Python"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/any-yaml"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Dealing%20with%20YAML%20with%20arbitrary%20tags%20in%20Python&amp;url=https%3A//death.andgravity.com/any-yaml&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;h2 id="bonus-hashable-wrapper"&gt;Bonus: hashable wrapper&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bonus-hashable-wrapper" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;You may be asking, why not make the wrapper hashable?&lt;/p&gt;
&lt;p&gt;Most unhashable (data) objects are that for a reason: because they're mutable.&lt;/p&gt;
&lt;p&gt;We have two options:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Make the wrapper hash change with the content.
This this will break dictionaries in strange and unexpected ways
(and other things too) –
the language &lt;a class="external" href="https://docs.python.org/3/reference/datamodel.html#object.__hash__"&gt;requires&lt;/a&gt; mutable objects to be unhashable.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Make the wrapper hash &lt;em&gt;not&lt;/em&gt; change with the content,
and wrappers equal only to themselves –
that's what user-defined classes do by default anyway.&lt;/p&gt;
&lt;p&gt;This works, but it's not very useful,
because equal values don't compare equal anymore
(&lt;code&gt;data != load(dump(data))&lt;/code&gt;).
Also, it means you can only get things from a dict
if you already have the object used as key:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;Hashable&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]):&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Hashable&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;KeyError&lt;/span&gt;: &lt;span class="n"&gt;Hashable([1])&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I'd file this under &amp;quot;strange and unexpected&amp;quot; too.&lt;/p&gt;
&lt;p&gt;(You can find the code for the example above &lt;a class="attachment" href="/_file/any-yaml/hashable_wrapper.py"&gt;here&lt;/a&gt;.)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="bonus-broken-yaml"&gt;Bonus: broken YAML&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bonus-broken-yaml" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;We can venture even farther, into arguably broken YAML.
Let's look at some examples.&lt;/p&gt;
&lt;p&gt;First, there are undefined &lt;a class="external" href="https://yaml.org/spec/1.2.2/#6822-tag-prefixes"&gt;tag prefixes&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;!m!xyz x&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;yaml.parser.ParserError&lt;/span&gt;: &lt;span class="n"&gt;while parsing a node&lt;/span&gt;
&lt;span class="x"&gt;found undefined tag handle &amp;#39;!m!&amp;#39;&lt;/span&gt;
&lt;span class="x"&gt;  in &amp;quot;&amp;lt;unicode string&amp;gt;&amp;quot;, line 1, column 1:&lt;/span&gt;
&lt;span class="x"&gt;    !m!xyz x&lt;/span&gt;
&lt;span class="x"&gt;    ^&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A valid version:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;%TAG !m! !my-&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;---&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;!m!xyz x&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;Tagged(&amp;#39;!my-xyz&amp;#39;, &amp;#39;x&amp;#39;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Second, there are undefined &lt;a class="external" href="https://yaml.org/spec/1.2.2/#71-alias-nodes"&gt;aliases&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;two: *anchor&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;yaml.composer.ComposerError&lt;/span&gt;: &lt;span class="n"&gt;found undefined alias &amp;#39;anchor&amp;#39;&lt;/span&gt;
&lt;span class="x"&gt;  in &amp;quot;&amp;lt;unicode string&amp;gt;&amp;quot;, line 1, column 6:&lt;/span&gt;
&lt;span class="x"&gt;    two: *anchor&lt;/span&gt;
&lt;span class="x"&gt;         ^&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A valid version:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;yaml&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;one: &amp;amp;anchor [1]&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;two: *anchor&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;Loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;{&amp;#39;one&amp;#39;: [1], &amp;#39;two&amp;#39;: [1]}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It's likely possible to handle these in a way similar to how we handled undefined tags,
but we'd have to go deeper –
the exceptions hint to which
&lt;a class="anchor" href="#yaml-processing-overview-diagram"&gt;processing step&lt;/a&gt;
to look at.&lt;/p&gt;
&lt;p&gt;Since I haven't actually encountered them in real life, we'll &amp;quot;save them for later&amp;quot; :)&lt;/p&gt;
&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;Of which YAML is actually a superset. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;&lt;a class="external" href="https://www.python.org/dev/peps/pep-0020/"&gt;Timothy 20:9&lt;/a&gt;. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-3"&gt;&lt;p&gt;Using a &lt;em&gt;hash&lt;/em&gt; table. For nice explanation of how it all works,
complete with a pure-Python implementation,
check out Raymond Hettinger's talk
&lt;a class="external" href="https://www.youtube.com/watch?v=npw4s1QTmPg"&gt;Modern Python Dictionaries: A confluence of a dozen great ideas&lt;/a&gt;
(&lt;a class="external" href="https://gist.github.com/gerrymanoim/3519567d6afae5361032c032e0cc44f2"&gt;code&lt;/a&gt;). &lt;a href="#fnref-3" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-4"&gt;&lt;p&gt;Almost. The zero argument form of &lt;a class="external" href="https://docs.python.org/3/library/functions.html#super"&gt;super()&lt;/a&gt; won't work
for methods defined outside of a class definition,
but we're not using it here. &lt;a href="#fnref-4" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/any-yaml" rel="alternate"/>
    <summary>... in which we use PyYAML to safely read and write YAML with any tags, in a way that's as straightforward as interacting with built-in types.</summary>
    <published>2022-01-23T10:53:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/reader-2-5">
    <id>https://death.andgravity.com/reader-2-5</id>
    <title>reader 2.5 released – usage statistics</title>
    <updated>2021-11-01T07:25:00+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;I'm happy to announce version 2.5 of &lt;strong&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/strong&gt;, a Python feed reader library.&lt;/p&gt;
&lt;h2 id="what-s-new"&gt;What's new?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-new" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Here are the most important changes since &lt;a class="internal" href="/reader-2-0"&gt;reader 2.0&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="search-enabled-by-default"&gt;Search enabled by default&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#search-enabled-by-default" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a class="external" href="https://reader.readthedocs.io/en/stable/guide.html#full-text-search"&gt;Full-text search&lt;/a&gt; works out of the box:
no extra dependencies, no setup needed.&lt;/p&gt;
&lt;h3 id="statistics"&gt;Statistics&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#statistics" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;There are now statistics on feed and user activity,
to give you a better understanding of how you consume content.&lt;/p&gt;
&lt;p&gt;First, you can get the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/guide.html#counting-things"&gt;average number of entries&lt;/a&gt; per day
for the last 1, 3, 12 months,
so you know how often a feed publishes new entries,
and how that changed over time –
think &lt;a class="external" href="https://en.wikipedia.org/wiki/Sparkline"&gt;sparklines&lt;/a&gt;: &lt;code&gt;36 entries ▄▃▁ (4.0, 2.0, 0.6)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Second, &lt;em&gt;reader&lt;/em&gt; records the time when an entry
was last &lt;a class="external" href="https://reader.readthedocs.io/en/stable/guide.html#entry-flags"&gt;marked as read or important&lt;/a&gt;.
This will allow you to see how you engage with new entries
– I'm still working on how to translate this data into a useful summary.&lt;/p&gt;
&lt;p&gt;A nice side-effect of knowing when entry flags changed
is that now it's possible to tell
if an entry was &lt;em&gt;explicitly&lt;/em&gt; marked as unimportant
(new entries are unimportant by default).&lt;/p&gt;
&lt;h3 id="improved-duplicate-handling"&gt;Improved duplicate handling&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#improved-duplicate-handling" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a class="external" href="https://reader.readthedocs.io/en/stable/plugins.html#reader-entry-dedupe"&gt;Duplicate handling&lt;/a&gt; got significantly better:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;False negatives are reduced by using approximate string matching
and heuristics to detect truncated content.&lt;/li&gt;
&lt;li&gt;You can trigger entry deduplication manually,
for the existing entries of a feed
– just add the &lt;code&gt;.reader.dedupe.once&lt;/code&gt; tag to the feed,
and wait for the next update.
Also, you can deduplicate entries by title alone, ignoring content.&lt;/li&gt;
&lt;li&gt;Old duplicates are deleted instead of marked as read/unimportant.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="user-added-entries"&gt;User-added entries&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#user-added-entries" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;You can now &lt;a class="external" href="https://reader.readthedocs.io/en/stable/api.html#reader.Reader.add_entry"&gt;add entries&lt;/a&gt; to existing feeds.
This is useful when you want to keep track of an article
that is not in the feed anymore because it &amp;quot;fell off the end&amp;quot;.&lt;/p&gt;
&lt;p&gt;It can also be used to build bookmarking / read later functionality
similar to that of &lt;a class="external" href="https://tt-rss.org/wiki/ShareAnything"&gt;Tiny Tiny RSS&lt;/a&gt;;
&lt;a class="external" href="https://github.com/lemon24/reader/issues/222"&gt;extracting content&lt;/a&gt; from arbitrary pages would be pretty helpful here.&lt;/p&gt;
&lt;h3 id="new-python-versions"&gt;New Python versions&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#new-python-versions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;em&gt;reader&lt;/em&gt; now supports Python 3.10 and PyPy 3.8.&lt;/p&gt;
&lt;h3 id="other-changes"&gt;Other changes&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#other-changes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Aside from the changes mentioned above, I
added a &lt;a class="external" href="https://reader.readthedocs.io/en/stable/api.html#reader.Reader.after_feed_update_hooks"&gt;new plugin hook&lt;/a&gt;,
added a few convenience methods and attributes,
updated the web application and plugins to take advantage of the new features,
and fixed a few minor bugs.&lt;/p&gt;
&lt;p&gt;See the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-2-5"&gt;changelog&lt;/a&gt; for details.&lt;/p&gt;
&lt;h2 id="what-is-reader"&gt;What is &lt;em&gt;reader&lt;/em&gt;?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;reader&lt;/strong&gt; takes care
of the core functionality required by a feed reader,
so you can focus on what makes &lt;strong&gt;yours&lt;/strong&gt; different.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-2-0/reader.png" alt="reader in action" /&gt;
&lt;em&gt;reader&lt;/em&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retrieve, store, and manage &lt;strong&gt;Atom&lt;/strong&gt;, &lt;strong&gt;RSS&lt;/strong&gt;, and &lt;strong&gt;JSON&lt;/strong&gt; feeds&lt;/li&gt;
&lt;li&gt;mark entries as read or important&lt;/li&gt;
&lt;li&gt;add tags and metadata to feeds&lt;/li&gt;
&lt;li&gt;filter feeds and articles&lt;/li&gt;
&lt;li&gt;full-text search articles&lt;/li&gt;
&lt;li&gt;get statistics on feed and user activity&lt;/li&gt;
&lt;li&gt;write plugins to extend its functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...all these with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a stable, clearly documented API&lt;/li&gt;
&lt;li&gt;excellent test coverage&lt;/li&gt;
&lt;li&gt;fully typed Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find out more, check out the &lt;a class="external" href="https://github.com/lemon24/reader"&gt;GitHub repo&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/"&gt;docs&lt;/a&gt;,
or give the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/tutorial.html"&gt;tutorial&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2 id="why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-use-a-feed-reader-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Have you been unhappy with existing feed readers and wanted to make your own, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never knew where to start?&lt;/li&gt;
&lt;li&gt;it seemed like too much work?&lt;/li&gt;
&lt;li&gt;you don't like writing backend code?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you already working with &lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/"&gt;feedparser&lt;/a&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;want an easier way to store, filter, sort and search feeds and entries?&lt;/li&gt;
&lt;li&gt;want to get back type-annotated objects instead of dicts?&lt;/li&gt;
&lt;li&gt;want to restrict or deny file-system access?&lt;/li&gt;
&lt;li&gt;want to change the way feeds are retrieved by using &lt;a class="external" href="https://requests.readthedocs.io"&gt;Requests&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to also support &lt;a class="external" href="https://jsonfeed.org/"&gt;JSON Feed&lt;/a&gt;?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... while still supporting all the feed types feedparser does?&lt;/p&gt;
&lt;p&gt;If you answered yes to any of the above, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;h2 id="why-make-your-own-feed-reader"&gt;Why make your own feed reader?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-make-your-own-feed-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;have full control over your data&lt;/li&gt;
&lt;li&gt;control what features it has or doesn't have&lt;/li&gt;
&lt;li&gt;decide how much you pay for it&lt;/li&gt;
&lt;li&gt;make sure it doesn't get closed while you're still using it&lt;/li&gt;
&lt;li&gt;really, it's &lt;a class="external" href="https://rachelbythebay.com/w/2011/10/26/fred/"&gt;easier than you think&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously, this may not be your cup of tea, but if it is, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
</content>
    <link href="https://death.andgravity.com/reader-2-5" rel="alternate"/>
    <published>2021-10-29T12:55:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/query-builder-how">
    <id>https://death.andgravity.com/query-builder-how</id>
    <title>Write an SQL query builder in 150 lines of Python!</title>
    <updated>2021-08-27T13:47:00+00:00</updated>
    <content type="html">&lt;p&gt;&lt;strong&gt;&lt;a class="internal" href="/own-query-builder"&gt;Previously&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is the fourth article &lt;a class="internal" href="/query-builder"&gt;in a series&lt;/a&gt; about
writing an SQL query builder for my feed &lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt; library.&lt;/p&gt;
&lt;p&gt;Today, we'll dive into the code by &lt;strong&gt;rewriting it from scratch&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Think of it as part walk-through, part tutorial;
along the way, we'll explore:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;API design&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;knowing &lt;strong&gt;when to be lazy&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;worse&lt;/strong&gt; and &lt;strong&gt;better&lt;/strong&gt; ways of doing things&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As you read this, keep in mind it is a story, thus &lt;em&gt;linear&lt;/em&gt; by necessity.
Development was decidedly &lt;em&gt;not&lt;/em&gt; so:
I tried things out, I changed my mind multiple times,
and I &lt;a class="external" href="https://programmingisterrible.com/post/176657481103/repeat-yourself-do-more-than-one-thing-and"&gt;rewrote everything&lt;/a&gt; once.
Even now, there are other equally-good or better implementations;
this one is simply &lt;em&gt;good enough&lt;/em&gt;.&lt;/p&gt;
&lt;details class="toc"&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#what-are-we-trying-to-build"&gt;What are we trying to build?&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#trade-offs"&gt;Trade-offs&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#a-minimal-plausible-solution"&gt;A minimal plausible solution&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#data-representation"&gt;Data representation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#classes"&gt;Classes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#adding-things"&gt;Adding things&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#output"&gt;Output&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#tests"&gt;Tests&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#separators"&gt;Separators&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#aliases"&gt;Aliases&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#subqueries"&gt;Subqueries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#joins"&gt;Joins&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#distinct"&gt;Distinct&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#more-tests"&gt;More tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#more-init"&gt;More init&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#bonus-things-that-didn-t-make-the-cut"&gt;Bonus: things that didn't make the cut&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#insert-update-delete"&gt;Insert / update / delete&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#arbitrary-strings-as-subqueries"&gt;Arbitrary strings as subqueries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#query-objects-as-subqueries"&gt;Query objects as subqueries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#union-intersect-except"&gt;Union / intersect / except&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="what-are-we-trying-to-build"&gt;What are we trying to build?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-are-we-trying-to-build" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;We want a way of building SQL strings that takes care of formatting:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;table&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;builder.Query object at 0x7fc953e60640&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;SELECT&lt;/span&gt;
&lt;span class="go"&gt;    one&lt;/span&gt;
&lt;span class="go"&gt;FROM&lt;/span&gt;
&lt;span class="go"&gt;    table&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... and allows us to add parts incrementally:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;condition&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;builder.Query object at 0x7fc953e60640&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;SELECT&lt;/span&gt;
&lt;span class="go"&gt;    one,&lt;/span&gt;
&lt;span class="go"&gt;    two&lt;/span&gt;
&lt;span class="go"&gt;FROM&lt;/span&gt;
&lt;span class="go"&gt;    table&lt;/span&gt;
&lt;span class="go"&gt;WHERE&lt;/span&gt;
&lt;span class="go"&gt;    condition&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;While not required,
I recommend reading the previous articles
to get a better idea of &lt;a class="internal" href="/query-builder-why"&gt;the problem we're trying to solve&lt;/a&gt;,
and &lt;a class="internal" href="/own-query-builder#background"&gt;the context we're solving it in&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In short, whatever we build &lt;a class="internal" href="/own-query-builder#requirements-and-existing-libraries"&gt;should&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;support SELECT with conditional WITH, WHERE, ORDER BY, JOIN etc.&lt;/li&gt;
&lt;li&gt;expose the names of the result columns (for scrolling window queries)&lt;/li&gt;
&lt;li&gt;be easy to use, understand and maintain&lt;/li&gt;
&lt;/ul&gt;
&lt;section class="admonition attention"&gt;
&lt;p class="admonition-title"&gt;Attention&lt;/p&gt;
&lt;p&gt;This query builder is not directly comparable with that of an ORM.
Instead, it is an alternative to building &lt;em&gt;plain SQL&lt;/em&gt; strings by hand.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The caveats that apply to plain SQL apply to it as well:&lt;/strong&gt;
Using user-supplied values directly in an SQL query
exposes you to &lt;a class="external" href="https://en.wikipedia.org/wiki/SQL_injection"&gt;SQL injection&lt;/a&gt; attacks.
Instead, use &lt;a class="external" href="https://en.wikipedia.org/wiki/SQL_injection#Parameterized_statements"&gt;parametrized queries&lt;/a&gt; whenever possible,
and &lt;a class="external" href="https://en.wikipedia.org/wiki/SQL_injection#Escaping"&gt;escaping&lt;/a&gt; only as a last resort.&lt;/p&gt;
&lt;/section&gt;
&lt;h3 id="trade-offs"&gt;Trade-offs&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#trade-offs" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Our solution does not exist in a void;
it exists to be used by my feed &lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt; library.&lt;/p&gt;
&lt;p&gt;Notably, we're &lt;em&gt;not&lt;/em&gt; making a general-purpose library with external users
whose needs we're trying to anticipate;
there's exactly one user with a pretty well-defined use case,
and strict backwards compatibility is not necessary.&lt;/p&gt;
&lt;p&gt;This allows us to make some upfront decisions to help with maintainability:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No needless customization. We can change the code directly if we need to.&lt;/li&gt;
&lt;li&gt;No other features except the known requirements.
We can add new ones when we need them.&lt;/li&gt;
&lt;li&gt;No effort to support other syntax than SQLite.&lt;/li&gt;
&lt;li&gt;No extensive testing.
We can rely on the exising comprehensive functional tests.&lt;/li&gt;
&lt;li&gt;No SQL validation. The database does this already.&lt;ul&gt;
&lt;li&gt;However, it would be nice to get at least a little error checking.
No need for custom exceptions, any kind is acceptable –
they should come up only during development and testing anyway.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="a-minimal-plausible-solution"&gt;A minimal plausible solution&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#a-minimal-plausible-solution" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id="data-representation"&gt;Data representation&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#data-representation" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;As mentioned &lt;a class="internal" href="/own-query-builder#the-first-prototype"&gt;before&lt;/a&gt;,
my prototype was based on the idea that
&lt;em&gt;queries can be represented as plain data structures&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Looking at a nicely formatted query,
a natural representation may reveal itself:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="SQL"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;two&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;another&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;condition&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;See it?&lt;/p&gt;
&lt;p&gt;It's a mapping with a list of strings for each clause:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;SELECT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;FROM&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;table&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="s1"&gt;&amp;#39;WHERE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;condition&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;another-condition&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Let's use this as our starting model, and make ourselves a query builder.&lt;/p&gt;
&lt;h3 id="classes"&gt;Classes&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#classes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;We start with a class:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="n"&gt;keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;WITH&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;SELECT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;FROM&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;WHERE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;GROUP BY&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;HAVING&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;ORDER BY&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;LIMIT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We use a class because most of the time
we don't want to interact with the underlying data structure,
since it's more likely to change.
We're not subclassing &lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#mapping-types-dict"&gt;dict&lt;/a&gt;,
since that would unintentionally expose its methods (and thus, behavior),
and we may need those names for something else.&lt;/p&gt;
&lt;p&gt;Also, a class allows us to reduce verbosity:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# we want&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;table&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# not&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SELECT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;FROM&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;table&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We use class variables for &amp;quot;static&amp;quot; data
instead of hardcoding or module variables
so it's easy to override (more on that later).&lt;/p&gt;
&lt;p&gt;We don't customize anything in &lt;code&gt;__init__()&lt;/code&gt; fow now;
if we need more clauses, we can add them to &lt;code&gt;keywords&lt;/code&gt; directly.
Adding all known keywords to &lt;code&gt;data&lt;/code&gt; upfront gets us free error checking:
&lt;code&gt;data[keyword]&lt;/code&gt; raises KeyError for unknown keywords.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;Unless specified otherwise,
I'll use &lt;em&gt;clause&lt;/em&gt; and &lt;em&gt;keyword&lt;/em&gt; to mean &amp;quot;item in &lt;code&gt;self.data&lt;/code&gt;&amp;quot;,
not &amp;quot;SQL clause or keyword in general&amp;quot;.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;We could use &lt;a class="external" href="https://docs.python.org/3/library/dataclasses.html"&gt;dataclasses&lt;/a&gt;,
but of the generated magic methods, we'd only use &lt;code&gt;__repr__()&lt;/code&gt;,
and its output would be too long to be useful anyway.&lt;/p&gt;
&lt;h3 id="adding-things"&gt;Adding things&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#adding-things" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Next, we add code for adding string fragments to each clause:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_clean_up&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__getattr__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isupper&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;_&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_clean_up&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;textwrap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rstrip&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;add()&lt;/code&gt; is roughly equivalent to &lt;code&gt;data[keyword]​.extend(args)&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The main difference is that
we dedent the arguments and remove trailing whitespace.
This is intentional:
we clean everything up and make as many choices when &lt;em&gt;adding things&lt;/em&gt;,
so we don't have to care about that when &lt;em&gt;generating output&lt;/em&gt;,
and so error checking happens as early as possible.&lt;/p&gt;
&lt;p&gt;Also, &lt;code&gt;add()&lt;/code&gt; returns &lt;code&gt;self&lt;/code&gt; to enable &lt;a class="external" href="https://en.wikipedia.org/wiki/Method_chaining"&gt;method chaining&lt;/a&gt;: &lt;code&gt;query​.add(...)​.add(...)&lt;/code&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/reference/datamodel.html#object.__getattr__"&gt;&lt;code&gt;__getattr__()&lt;/code&gt;&lt;/a&gt; is called when an attribute does not exist,
and allows us to return &lt;em&gt;something&lt;/em&gt; instead of getting the default AttributeError.&lt;/p&gt;
&lt;p&gt;What we return is a &lt;code&gt;KEYWORD(*args)&lt;/code&gt; callable made on the fly
by wrapping &lt;code&gt;add()&lt;/code&gt; in a &lt;a class="external" href="https://docs.python.org/3/library/functools.html#functools.partial"&gt;partial&lt;/a&gt;;
a &lt;a class="external" href="https://en.wikipedia.org/wiki/Closure_(computer_programming)"&gt;closure&lt;/a&gt; capturing &lt;code&gt;name&lt;/code&gt; would be functionally equivalent.&lt;/p&gt;
&lt;p&gt;Requiring the keywords to be uppercase is a stylistic choice,
but does have advantages:
it signals to the reader these are special &amp;quot;methods&amp;quot;,
and avoids shadowing dunder methods like &lt;code&gt;__deepcopy__()&lt;/code&gt; without extra checks.&lt;/p&gt;
&lt;p&gt;To indicate the attribute really doesn't exist,
we need to raise AttributeError;
we let &lt;a class="external" href="https://docs.python.org/3/library/functions.html#getattr"&gt;getattr()&lt;/a&gt; do it for us
(the parent &lt;a class="external" href="https://docs.python.org/3/library/functions.html#object"&gt;object&lt;/a&gt; doesn't have a custom &lt;code&gt;__getattr__()&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;We &lt;em&gt;could&lt;/em&gt; store the partial on the instance,
which would side-step &lt;code&gt;__getattr__()&lt;/code&gt; on subsequent calls,
so we only make one partial per keyword;
we could do it in &lt;code&gt;__init__()&lt;/code&gt;, and not use &lt;code&gt;__getattr__()&lt;/code&gt; at all;
we could even use &lt;a class="external" href="https://docs.python.org/3/library/functools.html#functools.partialmethod"&gt;partialmethod&lt;/a&gt;,
so there's only one per keyword per class!
Or we can do nothing – they're likely premature optimization,
and what we're doing now is more flexible anyway.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;I said error checking happens as early as possible;
that's &lt;em&gt;almost&lt;/em&gt; true:
if you look carefully at the code,
you may notice &lt;code&gt;query​.ESLECT&lt;/code&gt; doesn't raise an exception
until called – &lt;code&gt;query​.ESLECT()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Doing most of the work in &lt;code&gt;add()&lt;/code&gt; does have some benefits, though:
we can use it with &lt;a class="external" href="https://docs.python.org/3/library/functools.html#functools.partial"&gt;partial&lt;/a&gt; and get chaining for free,
and it's an escape hatch for when we want
to use a &amp;quot;keyword&amp;quot; that's not a Python identifier
(this will be useful later).&lt;/p&gt;
&lt;h3 id="output"&gt;Output&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#output" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Finally, we turn the query into SQL:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;18&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="n"&gt;default_separator&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;,&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;
&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;
&lt;span class="normal"&gt;52&lt;/span&gt;
&lt;span class="normal"&gt;53&lt;/span&gt;
&lt;span class="normal"&gt;54&lt;/span&gt;
&lt;span class="normal"&gt;55&lt;/span&gt;
&lt;span class="normal"&gt;56&lt;/span&gt;
&lt;span class="normal"&gt;57&lt;/span&gt;
&lt;span class="normal"&gt;58&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__str__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_lines&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_lines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
            &lt;span class="k"&gt;yield from&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_lines_keyword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_lines_keyword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_indent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_separator&lt;/span&gt;

            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;

    &lt;span class="n"&gt;_indent&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;functools&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partial&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;textwrap&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;    &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The only output API is &lt;a class="external" href="https://docs.python.org/3/library/functions.html#func-str"&gt;str()&lt;/a&gt;;
being the standard way of turning objects into strings in Python,
it requires zero effort to learn.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;str(query)&lt;/code&gt; calls &lt;code&gt;__str__&lt;/code&gt;, which delegates to &lt;code&gt;_lines()&lt;/code&gt;.
We use a generator mainly because it allows us to write
&lt;code&gt;yield line&lt;/code&gt; instead of &lt;code&gt;rv.append(line)&lt;/code&gt;,
making for somewhat cleaner code.&lt;/p&gt;
&lt;p&gt;Another benefit of a generator is that it's lazy,
so we can pass it around
without having to build intermediary lists in memory;
for example, to a file's &lt;a class="external" href="https://docs.python.org/3/library/io.html#io.IOBase.writelines"&gt;writelines()&lt;/a&gt; method,
or in &lt;code&gt;yield from&lt;/code&gt; in another generator
(e.g. for nested subqueries).
We don't need it here,
but it's useful when generating a lot of values.&lt;/p&gt;
&lt;p&gt;We split the logic for individual clauses into &lt;code&gt;_lines_keyword()&lt;/code&gt;,
because we'll keep adding stuff to it.
(I initially left everything in &lt;code&gt;_lines()&lt;/code&gt;,
and refactored when things got too complicated;
no need to do that now.)&lt;/p&gt;
&lt;p&gt;Since we'll want to indent things in the same way in more than one place,
we make it a static &amp;quot;method&amp;quot; using &lt;a class="external" href="https://docs.python.org/3/library/functools.html#functools.partial"&gt;partial&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You may notice we're not sorting the clauses in any way;
dicts guarantee insertion order in Python 3.6+&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;,
and we built &lt;code&gt;data&lt;/code&gt; from &lt;code&gt;keywords&lt;/code&gt;, so the order is preserved.&lt;/p&gt;
&lt;h3 id="tests"&gt;Tests&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#tests" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Let's add a simple test to make sure we don't break already working stuff:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_query_simple&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;select&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;from-one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;from-two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;        SELECT&lt;/span&gt;
&lt;span class="sd"&gt;            select&lt;/span&gt;
&lt;span class="sd"&gt;        FROM&lt;/span&gt;
&lt;span class="sd"&gt;            from-one,&lt;/span&gt;
&lt;span class="sd"&gt;            from-two&lt;/span&gt;
&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We'll keep adding to it with each feature.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;For a minimal solution, we are done. We've &amp;quot;spent&amp;quot; 62 lines, or 38 statements.&lt;/p&gt;
&lt;p&gt;The code so far:
&lt;a class="attachment" href="/_file/query-builder-how/00-begin/builder.py"&gt;builder.py&lt;/a&gt;,
&lt;a class="attachment" href="/_file/query-builder-how/00-begin/test_builder.py"&gt;test_builder.py&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="separators"&gt;Separators&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#separators" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;At this point, WHERE doesn't really make sense:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;WHERE&lt;/span&gt;
&lt;span class="go"&gt;    a,&lt;/span&gt;
&lt;span class="go"&gt;    b&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We fix it by special-casing separators for a few clauses:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;18&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="n"&gt;separators&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;AND&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;HAVING&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;AND&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;
&lt;span class="normal"&gt;52&lt;/span&gt;
&lt;span class="normal"&gt;53&lt;/span&gt;
&lt;span class="normal"&gt;54&lt;/span&gt;
&lt;span class="normal"&gt;55&lt;/span&gt;
&lt;span class="normal"&gt;56&lt;/span&gt;
&lt;span class="normal"&gt;57&lt;/span&gt;
&lt;span class="normal"&gt;58&lt;/span&gt;
&lt;span class="normal"&gt;59&lt;/span&gt;
&lt;span class="normal"&gt;60&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_lines_keyword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_indent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="hll"&gt;                &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_separator&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;
&lt;/span&gt;            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We could've used &lt;a class="external" href="https://docs.python.org/3/library/collections.html#collections.defaultdict"&gt;defaultdict&lt;/a&gt; instead of using &lt;code&gt;default_separator&lt;/code&gt;,
but then we'd have to remember non-comma separators need a space: &lt;code&gt;' AND'&lt;/code&gt;;
putting it in code means we don't have to remember anything.&lt;/p&gt;
&lt;p&gt;Also, we could've put the separator on a new line:
&lt;code&gt;'one\n​AND​ ​two'&lt;/code&gt; vs. &lt;code&gt;'one​ ​AND\n​two'&lt;/code&gt;.
While slightly &lt;a class="external" href="https://docs.telemetry.mozilla.org/concepts/sql_style.html#boolean-at-the-beginning-of-line"&gt;better style&lt;/a&gt;,
it makes code more complicated for little benefit,
and makes it less obvious that AND is just another separator.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;We add WHERE to the test.&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_query_simple&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;select&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;from-one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;from-two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;where-one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;where-two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;        SELECT&lt;/span&gt;
&lt;span class="sd"&gt;            select&lt;/span&gt;
&lt;span class="sd"&gt;        FROM&lt;/span&gt;
&lt;span class="sd"&gt;            from-one,&lt;/span&gt;
&lt;span class="sd"&gt;            from-two&lt;/span&gt;
&lt;span class="hll"&gt;&lt;span class="sd"&gt;        WHERE&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;&lt;span class="sd"&gt;            where-one AND&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;&lt;span class="sd"&gt;            where-two&lt;/span&gt;
&lt;/span&gt;&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;/details&gt;

&lt;p&gt;The code so far:
&lt;a class="attachment" href="/_file/query-builder-how/01-separators/builder.py"&gt;builder.py&lt;/a&gt;,
&lt;a class="attachment" href="/_file/query-builder-how/01-separators/test_builder.py"&gt;test_builder.py&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="aliases"&gt;Aliases&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#aliases" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;One of the requirements is that it should be possible to implement
&lt;a class="internal" href="/query-builder-why#intermission-scrolling-window-queries"&gt;scrolling window queries&lt;/a&gt;
on top.
For this, code needs to get the result column names
– the SELECT expressions &lt;em&gt;or&lt;/em&gt; their aliases –
and add them to a generated WHERE condition.&lt;/p&gt;
&lt;p&gt;Parsing the result column is straightforward only for simple cases:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;  &lt;span class="s1"&gt;&amp;#39;column&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;  &lt;span class="s1"&gt;&amp;#39;column AS alias&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;  &lt;span class="s1"&gt;&amp;#39;column as alias&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;  &lt;span class="s1"&gt;&amp;#39;(SELECT column FROM table AS another-table)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rpartition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; AS &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SELECT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;column&amp;#39;, &amp;#39;alias&amp;#39;, &amp;#39;column as alias&amp;#39;, &amp;#39;another-table)&amp;#39;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;An acceptable compromise is using pairs of strings for aliased columns.
Since the column expression might be quite long,
we'll make the alias the first thing in the pair.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alias&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;SELECT&lt;/span&gt;
&lt;span class="go"&gt;    one AS alias,&lt;/span&gt;
&lt;span class="go"&gt;    two&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As mentioned earlier,
we store everything in a standard way
to keep output code simpler.
A plain 2-tuple is a decent choice,
but a &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-named-tuple"&gt;named tuple&lt;/a&gt; is more readable.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;66&lt;/span&gt;
&lt;span class="normal"&gt;67&lt;/span&gt;
&lt;span class="normal"&gt;68&lt;/span&gt;
&lt;span class="normal"&gt;69&lt;/span&gt;
&lt;span class="normal"&gt;70&lt;/span&gt;
&lt;span class="normal"&gt;71&lt;/span&gt;
&lt;span class="normal"&gt;72&lt;/span&gt;
&lt;span class="normal"&gt;73&lt;/span&gt;
&lt;span class="normal"&gt;74&lt;/span&gt;
&lt;span class="normal"&gt;75&lt;/span&gt;
&lt;span class="normal"&gt;76&lt;/span&gt;
&lt;span class="normal"&gt;77&lt;/span&gt;
&lt;span class="normal"&gt;78&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;_Thing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;

    &lt;span class="nd"&gt;@classmethod&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;from_arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;invalid arg: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_clean_up&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;_clean_up&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Conveniently, this gives us a place where to convert the string-or-pair:
the &lt;code&gt;from_arg()&lt;/code&gt; alternate constructor.
We could've made it a stand-alone function,
but this way it's easier to see what type is being returned.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;Note that we use an empty string to mean &amp;quot;no alias&amp;quot;.
In general, it's a good idea to distinguish this kind of absence by using None,
since the empty string may be a valid input,
and None can prevent some bugs – e.g. you can't concatenate None to a string.
Here, an empty string cannot be a valid alias, and we use format strings,
so we don't bother.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;Using it is just a one-line change to &lt;code&gt;add()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="hll"&gt;            &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_Thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;On output, we have two concerns:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;there may or may not be an alias&lt;/li&gt;
&lt;li&gt;the order differs depending on the keyword:
you have &lt;code&gt;SELECT expr AS column-alias&lt;/code&gt;,
but &lt;code&gt;WITH table-name AS (stmt)&lt;/code&gt;
(we treat the CTE table name as an alias)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;We can model this with mostly-empty &lt;a class="external" href="https://docs.python.org/3/library/collections.html#collections.defaultdict"&gt;defaultdict&lt;/a&gt;s with per-clause format strings:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="n"&gt;formats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{value}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;defaultdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{value}&lt;/span&gt;&lt;span class="s1"&gt; AS &lt;/span&gt;&lt;span class="si"&gt;{alias}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;WITH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{alias}&lt;/span&gt;&lt;span class="s1"&gt; AS &lt;/span&gt;&lt;span class="si"&gt;{value}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;... and choose the right defaultdict using the alias's boolean value:&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;55&lt;/span&gt;
&lt;span class="normal"&gt;56&lt;/span&gt;
&lt;span class="normal"&gt;57&lt;/span&gt;
&lt;span class="normal"&gt;58&lt;/span&gt;
&lt;span class="normal"&gt;59&lt;/span&gt;
&lt;span class="normal"&gt;60&lt;/span&gt;
&lt;span class="normal"&gt;61&lt;/span&gt;
&lt;span class="normal"&gt;62&lt;/span&gt;
&lt;span class="normal"&gt;63&lt;/span&gt;
&lt;span class="normal"&gt;64&lt;/span&gt;
&lt;span class="normal"&gt;65&lt;/span&gt;
&lt;span class="normal"&gt;66&lt;/span&gt;
&lt;span class="normal"&gt;67&lt;/span&gt;
&lt;span class="normal"&gt;68&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_lines_keyword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="hll"&gt;            &lt;span class="nb"&gt;format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_indent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_separator&lt;/span&gt;

            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;details&gt;
&lt;summary&gt;We add an aliased expression to the test.&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_query_simple&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;select-one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alias&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;select-two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;from-one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;from-two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;where-one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;where-two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;        SELECT&lt;/span&gt;
&lt;span class="sd"&gt;            select-one,&lt;/span&gt;
&lt;span class="hll"&gt;&lt;span class="sd"&gt;            select-two AS alias&lt;/span&gt;
&lt;/span&gt;&lt;span class="sd"&gt;        FROM&lt;/span&gt;
&lt;span class="sd"&gt;            from-one,&lt;/span&gt;
&lt;span class="sd"&gt;            from-two&lt;/span&gt;
&lt;span class="sd"&gt;        WHERE&lt;/span&gt;
&lt;span class="sd"&gt;            where-one AND&lt;/span&gt;
&lt;span class="sd"&gt;            where-two&lt;/span&gt;
&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;/details&gt;

&lt;p&gt;The code so far:
&lt;a class="attachment" href="/_file/query-builder-how/02-aliases/builder.py"&gt;builder.py&lt;/a&gt;,
&lt;a class="attachment" href="/_file/query-builder-how/02-aliases/test_builder.py"&gt;test_builder.py&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="subqueries"&gt;Subqueries&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#subqueries" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Currently, WITH is still a little broken:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WITH&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;table-name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;SELECT 1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="go"&gt;WITH&lt;/span&gt;
&lt;span class="go"&gt;    table-name AS SELECT 1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Since common table expressions always have the SELECT statement paranthesized,
we'd like to have it out of the box, with proper indentation:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="SQL"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;WITH&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A simple way of handling this is to change the WITH format string to
&lt;code&gt;'{alias} AS (\n{indented}\n)'&lt;/code&gt;,
where &lt;code&gt;indented&lt;/code&gt; is the value, but indented.&lt;sup class="footnote-ref" id="fnref-3"&gt;&lt;a href="#fn-3"&gt;3&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;This kinda works, but is limited in usefulness;
for instance, we can't easily build something like this on top:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alias&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;SELECT 1&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;is_subquery&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Instead, let's keep refining our model,
and use a flag to mark subqueries:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;73&lt;/span&gt;
&lt;span class="normal"&gt;74&lt;/span&gt;
&lt;span class="normal"&gt;75&lt;/span&gt;
&lt;span class="normal"&gt;76&lt;/span&gt;
&lt;span class="normal"&gt;77&lt;/span&gt;
&lt;span class="normal"&gt;78&lt;/span&gt;
&lt;span class="normal"&gt;79&lt;/span&gt;
&lt;span class="normal"&gt;80&lt;/span&gt;
&lt;span class="normal"&gt;81&lt;/span&gt;
&lt;span class="normal"&gt;82&lt;/span&gt;
&lt;span class="normal"&gt;83&lt;/span&gt;
&lt;span class="normal"&gt;84&lt;/span&gt;
&lt;span class="normal"&gt;85&lt;/span&gt;
&lt;span class="normal"&gt;86&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;_Thing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;span class="hll"&gt;    &lt;span class="n"&gt;is_subquery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/span&gt;
    &lt;span class="nd"&gt;@classmethod&lt;/span&gt;
&lt;span class="hll"&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;from_arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;
        &lt;span class="k"&gt;elif&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;invalid arg: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_clean_up&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;_clean_up&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We can then check if a clause always has subqueries,
and set the flag accordingly:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;28&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="n"&gt;subquery_keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;WITH&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="hll"&gt;        &lt;span class="n"&gt;kwargs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subquery_keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;            &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_subquery&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;
&lt;/span&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="hll"&gt;            &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_Thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Using it for output is just an extra if:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;61&lt;/span&gt;
&lt;span class="normal"&gt;62&lt;/span&gt;
&lt;span class="normal"&gt;63&lt;/span&gt;
&lt;span class="normal"&gt;64&lt;/span&gt;
&lt;span class="normal"&gt;65&lt;/span&gt;
&lt;span class="normal"&gt;66&lt;/span&gt;
&lt;span class="normal"&gt;67&lt;/span&gt;
&lt;span class="normal"&gt;68&lt;/span&gt;
&lt;span class="normal"&gt;69&lt;/span&gt;
&lt;span class="normal"&gt;70&lt;/span&gt;
&lt;span class="normal"&gt;71&lt;/span&gt;
&lt;span class="normal"&gt;72&lt;/span&gt;
&lt;span class="normal"&gt;73&lt;/span&gt;
&lt;span class="normal"&gt;74&lt;/span&gt;
&lt;span class="normal"&gt;75&lt;/span&gt;
&lt;span class="normal"&gt;76&lt;/span&gt;
&lt;span class="normal"&gt;77&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_lines_keyword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="nb"&gt;format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="hll"&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_subquery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;(&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_indent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;)&amp;#39;&lt;/span&gt;
&lt;/span&gt;            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_indent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_separator&lt;/span&gt;

            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;details&gt;
&lt;summary&gt;We add WITH to our test.&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_query_simple&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WITH&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alias&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;with&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/span&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;select-one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alias&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;select-two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;from-one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;from-two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;where-one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;where-two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="hll"&gt;&lt;span class="sd"&gt;        WITH&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;&lt;span class="sd"&gt;            alias AS (&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;&lt;span class="sd"&gt;                with&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;&lt;span class="sd"&gt;            )&lt;/span&gt;
&lt;/span&gt;&lt;span class="sd"&gt;        SELECT&lt;/span&gt;
&lt;span class="sd"&gt;            select-one,&lt;/span&gt;
&lt;span class="sd"&gt;            select-two AS alias&lt;/span&gt;
&lt;span class="sd"&gt;        FROM&lt;/span&gt;
&lt;span class="sd"&gt;            from-one,&lt;/span&gt;
&lt;span class="sd"&gt;            from-two&lt;/span&gt;
&lt;span class="sd"&gt;        WHERE&lt;/span&gt;
&lt;span class="sd"&gt;            where-one AND&lt;/span&gt;
&lt;span class="sd"&gt;            where-two&lt;/span&gt;
&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;/details&gt;

&lt;p&gt;The code so far:
&lt;a class="attachment" href="/_file/query-builder-how/03-subqueries/builder.py"&gt;builder.py&lt;/a&gt;,
&lt;a class="attachment" href="/_file/query-builder-how/03-subqueries/test_builder.py"&gt;test_builder.py&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="joins"&gt;Joins&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#joins" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;One clause that's entirely missing is JOIN.
And it's important, changing your mind about what you're selecting from
&lt;a class="internal" href="/query-builder-why#composition-and-reuse"&gt;happens quite often&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;JOIN is a bit more complicated,
mostly because it has different forms – JOIN, LEFT JOIN and so on;
SQLite supports at least 10 variations.&lt;/p&gt;
&lt;p&gt;I initially treated any keyword containing &lt;code&gt;JOIN&lt;/code&gt; as a separate keyword,
and &lt;a class="external" href="https://github.com/lemon24/reader/blob/1.11/src/reader/_sql_utils.py#L97-L103"&gt;dealt with it during output&lt;/a&gt;.
This has a few drawbacks, though;
aside from making the code more complicated,
it reorders the tables:
&lt;code&gt;query​.JOIN('a')​.LEFT_JOIN('b')​.JOIN('c')&lt;/code&gt;
results in &lt;code&gt;JOIN a JOIN c LEFT JOIN b&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;A better solution is to refine our model even further.&lt;/p&gt;
&lt;p&gt;Take a look at these railroad diagrams for the SELECT statement:&lt;/p&gt;
&lt;figure class="figure"&gt;
&lt;img class="img-responsive" src="/_file/query-builder-how/select-core.svg" alt="select-core (FROM clause)" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
&lt;a class="external" href="https://www.sqlite.org/syntax/select-core.html"&gt;select-core&lt;/a&gt; (FROM clause)
&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure class="figure"&gt;
&lt;img class="img-responsive" src="/_file/query-builder-how/join-clause.svg" alt="join-clause" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
&lt;a class="external" href="https://www.sqlite.org/syntax/join-clause.html"&gt;join-clause&lt;/a&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;figure class="figure"&gt;
&lt;img class="img-responsive" src="/_file/query-builder-how/join-operator.svg" alt="join-operator" /&gt;&lt;figcaption class="figure-caption text-center text-small"&gt;
&lt;a class="external" href="https://www.sqlite.org/syntax/join-operator.html"&gt;join-operator&lt;/a&gt;
&lt;/figcaption&gt;
&lt;/figure&gt;

&lt;p&gt;You may notice &lt;code&gt;table-or-subquery&lt;/code&gt; followed by &lt;code&gt;,&lt;/code&gt; in FROM
is actually a subset of &lt;code&gt;table-or-subquery&lt;/code&gt; followed by &lt;code&gt;join-operator&lt;/code&gt; in &lt;code&gt;join-clause&lt;/code&gt;.
That is, for SQLite, a comma is just another join operator.&lt;/p&gt;
&lt;p&gt;Put the other way around, &lt;em&gt;a join operator is just another separator&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Because our separators come &lt;em&gt;after&lt;/em&gt; things, not before,
we'll model join operators separately, as &lt;em&gt;fake keywords&lt;/em&gt;
(that is, not used to index into &lt;code&gt;data&lt;/code&gt;).&lt;/p&gt;
&lt;p&gt;First, let's set them:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;82&lt;/span&gt;
&lt;span class="normal"&gt;83&lt;/span&gt;
&lt;span class="normal"&gt;84&lt;/span&gt;
&lt;span class="normal"&gt;85&lt;/span&gt;
&lt;span class="normal"&gt;86&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;_Thing&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;span class="hll"&gt;    &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;    &lt;span class="n"&gt;is_subquery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;29&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="n"&gt;fake_keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;JOIN&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;FROM&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;
&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;
&lt;span class="normal"&gt;52&lt;/span&gt;
&lt;span class="normal"&gt;53&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fake_keyword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_resolve_fakes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;        &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="n"&gt;kwargs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fake_keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;            &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fake_keyword&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subquery_keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_subquery&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_Thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;

&lt;span class="hll"&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_resolve_fakes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;real&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fake_keywords&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;real&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;We could've probably just hardcoded this in &lt;code&gt;add()&lt;/code&gt;
(&lt;code&gt;if 'JOIN' in keyword: ...&lt;/code&gt;),
but doing it like this makes it easier to see at a glance that
&amp;quot;JOIN is a fake FROM&amp;quot;.&lt;/p&gt;
&lt;p&gt;Using &lt;code&gt;keyword&lt;/code&gt; as a separator is relatively straightforward:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;63&lt;/span&gt;
&lt;span class="normal"&gt;64&lt;/span&gt;
&lt;span class="normal"&gt;65&lt;/span&gt;
&lt;span class="normal"&gt;66&lt;/span&gt;
&lt;span class="normal"&gt;67&lt;/span&gt;
&lt;span class="normal"&gt;68&lt;/span&gt;
&lt;span class="normal"&gt;69&lt;/span&gt;
&lt;span class="normal"&gt;70&lt;/span&gt;
&lt;span class="normal"&gt;71&lt;/span&gt;
&lt;span class="normal"&gt;72&lt;/span&gt;
&lt;span class="normal"&gt;73&lt;/span&gt;
&lt;span class="normal"&gt;74&lt;/span&gt;
&lt;span class="normal"&gt;75&lt;/span&gt;
&lt;span class="normal"&gt;76&lt;/span&gt;
&lt;span class="normal"&gt;77&lt;/span&gt;
&lt;span class="normal"&gt;78&lt;/span&gt;
&lt;span class="normal"&gt;79&lt;/span&gt;
&lt;span class="normal"&gt;80&lt;/span&gt;
&lt;span class="normal"&gt;81&lt;/span&gt;
&lt;span class="normal"&gt;82&lt;/span&gt;
&lt;span class="normal"&gt;83&lt;/span&gt;
&lt;span class="normal"&gt;84&lt;/span&gt;
&lt;span class="normal"&gt;85&lt;/span&gt;
&lt;span class="normal"&gt;86&lt;/span&gt;
&lt;span class="normal"&gt;87&lt;/span&gt;
&lt;span class="normal"&gt;88&lt;/span&gt;
&lt;span class="normal"&gt;89&lt;/span&gt;
&lt;span class="normal"&gt;90&lt;/span&gt;
&lt;span class="normal"&gt;91&lt;/span&gt;
&lt;span class="normal"&gt;92&lt;/span&gt;
&lt;span class="normal"&gt;93&lt;/span&gt;
&lt;span class="normal"&gt;94&lt;/span&gt;
&lt;span class="normal"&gt;95&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_lines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;

&lt;span class="hll"&gt;            &lt;span class="n"&gt;grouped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                &lt;span class="n"&gt;grouped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grouped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                &lt;span class="k"&gt;yield from&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_lines_keyword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_lines_keyword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="hll"&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;/span&gt;
            &lt;span class="nb"&gt;format&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;formats&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;)][&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_subquery&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;(&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_indent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;)&amp;#39;&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_indent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;format&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="hll"&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;                &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;default_separator&lt;/span&gt;

            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Since FROM always comes before JOIN,
we make sure to output the real ones first.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;We add a JOIN to the test.&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_query_simple&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WITH&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alias&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;with&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;select-one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alias&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;select-two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;from-one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;from-two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JOIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;join&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;where-one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;where-two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;        WITH&lt;/span&gt;
&lt;span class="sd"&gt;            alias AS (&lt;/span&gt;
&lt;span class="sd"&gt;                with&lt;/span&gt;
&lt;span class="sd"&gt;            )&lt;/span&gt;
&lt;span class="sd"&gt;        SELECT&lt;/span&gt;
&lt;span class="sd"&gt;            select-one,&lt;/span&gt;
&lt;span class="sd"&gt;            select-two AS alias&lt;/span&gt;
&lt;span class="sd"&gt;        FROM&lt;/span&gt;
&lt;span class="sd"&gt;            from-one,&lt;/span&gt;
&lt;span class="sd"&gt;            from-two&lt;/span&gt;
&lt;span class="hll"&gt;&lt;span class="sd"&gt;        JOIN&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;&lt;span class="sd"&gt;            join&lt;/span&gt;
&lt;/span&gt;&lt;span class="sd"&gt;        WHERE&lt;/span&gt;
&lt;span class="sd"&gt;            where-one AND&lt;/span&gt;
&lt;span class="sd"&gt;            where-two&lt;/span&gt;
&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;/details&gt;

&lt;p&gt;The code so far:
&lt;a class="attachment" href="/_file/query-builder-how/04-join/builder.py"&gt;builder.py&lt;/a&gt;,
&lt;a class="attachment" href="/_file/query-builder-how/04-join/test_builder.py"&gt;test_builder.py&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="distinct"&gt;Distinct&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#distinct" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The final model change is to support SELECT DISTINCT.&lt;/p&gt;
&lt;p&gt;DISTINCT and ALL are flags that apply to the whole clause;
we'll model them as such:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;117&lt;/span&gt;
&lt;span class="normal"&gt;118&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;_FlagList&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;_FlagList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Since most of the time we're OK with the default &lt;code&gt;flag&lt;/code&gt;,
we don't bother setting it in &lt;code&gt;__init__&lt;/code&gt;,
and use a class variable instead.
If we need to customize it,
we can set &lt;code&gt;flag&lt;/code&gt; on the instance, shadowing the class variable.&lt;/p&gt;
&lt;p&gt;A &lt;code&gt;__repr__&lt;/code&gt; showing the flag would be nice,
but it'd only be useful during debugging, so we skip it as well.&lt;/p&gt;
&lt;p&gt;We set the flag based on a known set for each clause;
like with fake keywords, we pull the &amp;quot;parsing&amp;quot; logic into a separate method:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;30&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="n"&gt;flag_keywords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;DISTINCT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;ALL&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;
&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;
&lt;span class="normal"&gt;52&lt;/span&gt;
&lt;span class="normal"&gt;53&lt;/span&gt;
&lt;span class="normal"&gt;54&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fake_keyword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_resolve_fakes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_resolve_flags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;        &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="hll"&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; already has flag: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;            &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;
&lt;/span&gt;
        &lt;span class="n"&gt;kwargs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;fake_keyword&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;fake_keyword&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;subquery_keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;is_subquery&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;arg&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;_Thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;from_arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;62&lt;/span&gt;
&lt;span class="normal"&gt;63&lt;/span&gt;
&lt;span class="normal"&gt;64&lt;/span&gt;
&lt;span class="normal"&gt;65&lt;/span&gt;
&lt;span class="normal"&gt;66&lt;/span&gt;
&lt;span class="normal"&gt;67&lt;/span&gt;
&lt;span class="normal"&gt;68&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_resolve_flags&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;partition&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; &amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flag_keywords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flag_keywords&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;invalid flag for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="si"&gt;!r}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;prefix&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;flag&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Using it for output is again straightforward:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;78&lt;/span&gt;
&lt;span class="normal"&gt;79&lt;/span&gt;
&lt;span class="normal"&gt;80&lt;/span&gt;
&lt;span class="normal"&gt;81&lt;/span&gt;
&lt;span class="normal"&gt;82&lt;/span&gt;
&lt;span class="normal"&gt;83&lt;/span&gt;
&lt;span class="normal"&gt;84&lt;/span&gt;
&lt;span class="normal"&gt;85&lt;/span&gt;
&lt;span class="normal"&gt;86&lt;/span&gt;
&lt;span class="normal"&gt;87&lt;/span&gt;
&lt;span class="normal"&gt;88&lt;/span&gt;
&lt;span class="normal"&gt;89&lt;/span&gt;
&lt;span class="normal"&gt;90&lt;/span&gt;
&lt;span class="normal"&gt;91&lt;/span&gt;
&lt;span class="normal"&gt;92&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;_lines&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

&lt;span class="hll"&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt; &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;flag&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;/span&gt;&lt;span class="hll"&gt;                &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
&lt;/span&gt;
            &lt;span class="n"&gt;grouped&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;grouped&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;grouped&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;yield from&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_lines_keyword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;group&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;details&gt;
&lt;summary&gt;We add a SELECT DISTINCT to our test.&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_query_simple&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WITH&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alias&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;with&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;select-one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alias&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;select-two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;from-one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;from-two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JOIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;join&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;where-one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;where-two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="hll"&gt;        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT_DISTINCT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;select-three&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/span&gt;    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;        WITH&lt;/span&gt;
&lt;span class="sd"&gt;            alias AS (&lt;/span&gt;
&lt;span class="sd"&gt;                with&lt;/span&gt;
&lt;span class="sd"&gt;            )&lt;/span&gt;
&lt;span class="hll"&gt;&lt;span class="sd"&gt;        SELECT DISTINCT&lt;/span&gt;
&lt;/span&gt;&lt;span class="sd"&gt;            select-one,&lt;/span&gt;
&lt;span class="sd"&gt;            select-two AS alias,&lt;/span&gt;
&lt;span class="hll"&gt;&lt;span class="sd"&gt;            select-three&lt;/span&gt;
&lt;/span&gt;&lt;span class="sd"&gt;        FROM&lt;/span&gt;
&lt;span class="sd"&gt;            from-one,&lt;/span&gt;
&lt;span class="sd"&gt;            from-two&lt;/span&gt;
&lt;span class="sd"&gt;        JOIN&lt;/span&gt;
&lt;span class="sd"&gt;            join&lt;/span&gt;
&lt;span class="sd"&gt;        WHERE&lt;/span&gt;
&lt;span class="sd"&gt;            where-one AND&lt;/span&gt;
&lt;span class="sd"&gt;            where-two&lt;/span&gt;
&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;/details&gt;

&lt;p&gt;The code so far:
&lt;a class="attachment" href="/_file/query-builder-how/05-distinct/builder.py"&gt;builder.py&lt;/a&gt;,
&lt;a class="attachment" href="/_file/query-builder-how/05-distinct/test_builder.py"&gt;test_builder.py&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="more-tests"&gt;More tests&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#more-tests" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Our only test isn't all that simple anymore;
maybe it's time to split it in two:
one with a really simple query, and one with a really complicated query.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;... something like this.&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;  6&lt;/span&gt;
&lt;span class="normal"&gt;  7&lt;/span&gt;
&lt;span class="normal"&gt;  8&lt;/span&gt;
&lt;span class="normal"&gt;  9&lt;/span&gt;
&lt;span class="normal"&gt; 10&lt;/span&gt;
&lt;span class="normal"&gt; 11&lt;/span&gt;
&lt;span class="normal"&gt; 12&lt;/span&gt;
&lt;span class="normal"&gt; 13&lt;/span&gt;
&lt;span class="normal"&gt; 14&lt;/span&gt;
&lt;span class="normal"&gt; 15&lt;/span&gt;
&lt;span class="normal"&gt; 16&lt;/span&gt;
&lt;span class="normal"&gt; 17&lt;/span&gt;
&lt;span class="normal"&gt; 18&lt;/span&gt;
&lt;span class="normal"&gt; 19&lt;/span&gt;
&lt;span class="normal"&gt; 20&lt;/span&gt;
&lt;span class="normal"&gt; 21&lt;/span&gt;
&lt;span class="normal"&gt; 22&lt;/span&gt;
&lt;span class="normal"&gt; 23&lt;/span&gt;
&lt;span class="normal"&gt; 24&lt;/span&gt;
&lt;span class="normal"&gt; 25&lt;/span&gt;
&lt;span class="normal"&gt; 26&lt;/span&gt;
&lt;span class="normal"&gt; 27&lt;/span&gt;
&lt;span class="normal"&gt; 28&lt;/span&gt;
&lt;span class="normal"&gt; 29&lt;/span&gt;
&lt;span class="normal"&gt; 30&lt;/span&gt;
&lt;span class="normal"&gt; 31&lt;/span&gt;
&lt;span class="normal"&gt; 32&lt;/span&gt;
&lt;span class="normal"&gt; 33&lt;/span&gt;
&lt;span class="normal"&gt; 34&lt;/span&gt;
&lt;span class="normal"&gt; 35&lt;/span&gt;
&lt;span class="normal"&gt; 36&lt;/span&gt;
&lt;span class="normal"&gt; 37&lt;/span&gt;
&lt;span class="normal"&gt; 38&lt;/span&gt;
&lt;span class="normal"&gt; 39&lt;/span&gt;
&lt;span class="normal"&gt; 40&lt;/span&gt;
&lt;span class="normal"&gt; 41&lt;/span&gt;
&lt;span class="normal"&gt; 42&lt;/span&gt;
&lt;span class="normal"&gt; 43&lt;/span&gt;
&lt;span class="normal"&gt; 44&lt;/span&gt;
&lt;span class="normal"&gt; 45&lt;/span&gt;
&lt;span class="normal"&gt; 46&lt;/span&gt;
&lt;span class="normal"&gt; 47&lt;/span&gt;
&lt;span class="normal"&gt; 48&lt;/span&gt;
&lt;span class="normal"&gt; 49&lt;/span&gt;
&lt;span class="normal"&gt; 50&lt;/span&gt;
&lt;span class="normal"&gt; 51&lt;/span&gt;
&lt;span class="normal"&gt; 52&lt;/span&gt;
&lt;span class="normal"&gt; 53&lt;/span&gt;
&lt;span class="normal"&gt; 54&lt;/span&gt;
&lt;span class="normal"&gt; 55&lt;/span&gt;
&lt;span class="normal"&gt; 56&lt;/span&gt;
&lt;span class="normal"&gt; 57&lt;/span&gt;
&lt;span class="normal"&gt; 58&lt;/span&gt;
&lt;span class="normal"&gt; 59&lt;/span&gt;
&lt;span class="normal"&gt; 60&lt;/span&gt;
&lt;span class="normal"&gt; 61&lt;/span&gt;
&lt;span class="normal"&gt; 62&lt;/span&gt;
&lt;span class="normal"&gt; 63&lt;/span&gt;
&lt;span class="normal"&gt; 64&lt;/span&gt;
&lt;span class="normal"&gt; 65&lt;/span&gt;
&lt;span class="normal"&gt; 66&lt;/span&gt;
&lt;span class="normal"&gt; 67&lt;/span&gt;
&lt;span class="normal"&gt; 68&lt;/span&gt;
&lt;span class="normal"&gt; 69&lt;/span&gt;
&lt;span class="normal"&gt; 70&lt;/span&gt;
&lt;span class="normal"&gt; 71&lt;/span&gt;
&lt;span class="normal"&gt; 72&lt;/span&gt;
&lt;span class="normal"&gt; 73&lt;/span&gt;
&lt;span class="normal"&gt; 74&lt;/span&gt;
&lt;span class="normal"&gt; 75&lt;/span&gt;
&lt;span class="normal"&gt; 76&lt;/span&gt;
&lt;span class="normal"&gt; 77&lt;/span&gt;
&lt;span class="normal"&gt; 78&lt;/span&gt;
&lt;span class="normal"&gt; 79&lt;/span&gt;
&lt;span class="normal"&gt; 80&lt;/span&gt;
&lt;span class="normal"&gt; 81&lt;/span&gt;
&lt;span class="normal"&gt; 82&lt;/span&gt;
&lt;span class="normal"&gt; 83&lt;/span&gt;
&lt;span class="normal"&gt; 84&lt;/span&gt;
&lt;span class="normal"&gt; 85&lt;/span&gt;
&lt;span class="normal"&gt; 86&lt;/span&gt;
&lt;span class="normal"&gt; 87&lt;/span&gt;
&lt;span class="normal"&gt; 88&lt;/span&gt;
&lt;span class="normal"&gt; 89&lt;/span&gt;
&lt;span class="normal"&gt; 90&lt;/span&gt;
&lt;span class="normal"&gt; 91&lt;/span&gt;
&lt;span class="normal"&gt; 92&lt;/span&gt;
&lt;span class="normal"&gt; 93&lt;/span&gt;
&lt;span class="normal"&gt; 94&lt;/span&gt;
&lt;span class="normal"&gt; 95&lt;/span&gt;
&lt;span class="normal"&gt; 96&lt;/span&gt;
&lt;span class="normal"&gt; 97&lt;/span&gt;
&lt;span class="normal"&gt; 98&lt;/span&gt;
&lt;span class="normal"&gt; 99&lt;/span&gt;
&lt;span class="normal"&gt;100&lt;/span&gt;
&lt;span class="normal"&gt;101&lt;/span&gt;
&lt;span class="normal"&gt;102&lt;/span&gt;
&lt;span class="normal"&gt;103&lt;/span&gt;
&lt;span class="normal"&gt;104&lt;/span&gt;
&lt;span class="normal"&gt;105&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_query_simple&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;select&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;from&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JOIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;join&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;where&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;        SELECT&lt;/span&gt;
&lt;span class="sd"&gt;            select&lt;/span&gt;
&lt;span class="sd"&gt;        FROM&lt;/span&gt;
&lt;span class="sd"&gt;            from&lt;/span&gt;
&lt;span class="sd"&gt;        JOIN&lt;/span&gt;
&lt;span class="sd"&gt;            join&lt;/span&gt;
&lt;span class="sd"&gt;        WHERE&lt;/span&gt;
&lt;span class="sd"&gt;            where&lt;/span&gt;
&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_query_complicated&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;Test a complicated query:&lt;/span&gt;

&lt;span class="sd"&gt;    * order between different keywords does not matter&lt;/span&gt;
&lt;span class="sd"&gt;    * arguments of repeated calls get appended, with the order preserved&lt;/span&gt;
&lt;span class="sd"&gt;    * SELECT can receive 2-tuples&lt;/span&gt;
&lt;span class="sd"&gt;    * WHERE and HAVING arguments are separated by AND&lt;/span&gt;
&lt;span class="sd"&gt;    * JOIN arguments are separated by the keyword, and come after plain FROM&lt;/span&gt;
&lt;span class="sd"&gt;    * no-argument keywords have no effect, unless they are flags&lt;/span&gt;

&lt;span class="sd"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OUTER_JOIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;outer join&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JOIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;join&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LIMIT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;limit&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JOIN&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ORDER_BY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;first&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;second&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HAVING&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;having&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;expr&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GROUP_BY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;group by&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;from&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;three&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;four&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;another from&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;where&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ORDER_BY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;third&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;OUTER_JOIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;another outer join&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="c1"&gt;# this isn&amp;#39;t technically valid&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WITH&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;first cte&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;GROUP_BY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;another group by&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;HAVING&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;another having&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WITH&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;fancy&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;second cte&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JOIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;another join&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;another where&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;NATURAL_JOIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;natural join&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT_DISTINCT&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;        WITH&lt;/span&gt;
&lt;span class="sd"&gt;            (&lt;/span&gt;
&lt;span class="sd"&gt;                first cte&lt;/span&gt;
&lt;span class="sd"&gt;            ),&lt;/span&gt;
&lt;span class="sd"&gt;            fancy AS (&lt;/span&gt;
&lt;span class="sd"&gt;                second cte&lt;/span&gt;
&lt;span class="sd"&gt;            )&lt;/span&gt;
&lt;span class="sd"&gt;        SELECT DISTINCT&lt;/span&gt;
&lt;span class="sd"&gt;            one,&lt;/span&gt;
&lt;span class="sd"&gt;            expr AS two,&lt;/span&gt;
&lt;span class="sd"&gt;            three,&lt;/span&gt;
&lt;span class="sd"&gt;            four&lt;/span&gt;
&lt;span class="sd"&gt;        FROM&lt;/span&gt;
&lt;span class="sd"&gt;            from,&lt;/span&gt;
&lt;span class="sd"&gt;            another from&lt;/span&gt;
&lt;span class="sd"&gt;        OUTER JOIN&lt;/span&gt;
&lt;span class="sd"&gt;            outer join&lt;/span&gt;
&lt;span class="sd"&gt;        JOIN&lt;/span&gt;
&lt;span class="sd"&gt;            join&lt;/span&gt;
&lt;span class="sd"&gt;        OUTER JOIN&lt;/span&gt;
&lt;span class="sd"&gt;            another outer join&lt;/span&gt;
&lt;span class="sd"&gt;        JOIN&lt;/span&gt;
&lt;span class="sd"&gt;            another join&lt;/span&gt;
&lt;span class="sd"&gt;        NATURAL JOIN&lt;/span&gt;
&lt;span class="sd"&gt;            natural join&lt;/span&gt;
&lt;span class="sd"&gt;        WHERE&lt;/span&gt;
&lt;span class="sd"&gt;            where AND&lt;/span&gt;
&lt;span class="sd"&gt;            another where&lt;/span&gt;
&lt;span class="sd"&gt;        GROUP BY&lt;/span&gt;
&lt;span class="sd"&gt;            group by,&lt;/span&gt;
&lt;span class="sd"&gt;            another group by&lt;/span&gt;
&lt;span class="sd"&gt;        HAVING&lt;/span&gt;
&lt;span class="sd"&gt;            having AND&lt;/span&gt;
&lt;span class="sd"&gt;            another having&lt;/span&gt;
&lt;span class="sd"&gt;        ORDER BY&lt;/span&gt;
&lt;span class="sd"&gt;            first,&lt;/span&gt;
&lt;span class="sd"&gt;            second,&lt;/span&gt;
&lt;span class="sd"&gt;            third&lt;/span&gt;
&lt;span class="sd"&gt;        LIMIT&lt;/span&gt;
&lt;span class="sd"&gt;            limit&lt;/span&gt;
&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;/details&gt;

&lt;p&gt;The code so far:
&lt;a class="attachment" href="/_file/query-builder-how/06-more-tests/builder.py"&gt;builder.py&lt;/a&gt;,
&lt;a class="attachment" href="/_file/query-builder-how/06-more-tests/test_builder.py"&gt;test_builder.py&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="more-init"&gt;More init&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#more-init" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;One last feature:
I'd like to reuse the formatting logic for paranthesized lists.&lt;/p&gt;
&lt;p&gt;Good thing &lt;code&gt;__init__&lt;/code&gt; doesn't take any arguments yet:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fromkeys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;keywords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_FlagList&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;keyword&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;separators&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Using it looks like:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;(&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt; &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;(&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;OR&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;}))&lt;/span&gt;
&lt;span class="go"&gt;(&lt;/span&gt;
&lt;span class="go"&gt;    one OR&lt;/span&gt;
&lt;span class="go"&gt;    two&lt;/span&gt;
&lt;span class="go"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We could have required &lt;code&gt;data&lt;/code&gt; to have the same structure as the attribute;
however, it would be too verbose to use,
and I'd have to do all the clean up myself;
that's not very &lt;em&gt;convenient&lt;/em&gt;.
Instead, we make it mean
&amp;quot;&lt;code&gt;add()&lt;/code&gt; these strings for these keywords&amp;quot;.&lt;sup class="footnote-ref" id="fnref-4"&gt;&lt;a href="#fn-4"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;We add a separate test for the fancy &lt;code&gt;__init__&lt;/code&gt;.&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;108&lt;/span&gt;
&lt;span class="normal"&gt;109&lt;/span&gt;
&lt;span class="normal"&gt;110&lt;/span&gt;
&lt;span class="normal"&gt;111&lt;/span&gt;
&lt;span class="normal"&gt;112&lt;/span&gt;
&lt;span class="normal"&gt;113&lt;/span&gt;
&lt;span class="normal"&gt;114&lt;/span&gt;
&lt;span class="normal"&gt;115&lt;/span&gt;
&lt;span class="normal"&gt;116&lt;/span&gt;
&lt;span class="normal"&gt;117&lt;/span&gt;
&lt;span class="normal"&gt;118&lt;/span&gt;
&lt;span class="normal"&gt;119&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_query_init&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;(&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;three&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]},&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;(&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;OR&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;        (&lt;/span&gt;
&lt;span class="sd"&gt;            one OR&lt;/span&gt;
&lt;span class="sd"&gt;            two OR&lt;/span&gt;
&lt;span class="sd"&gt;            three&lt;/span&gt;
&lt;span class="sd"&gt;        )&lt;/span&gt;

&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;

&lt;/details&gt;

&lt;hr /&gt;
&lt;p&gt;OK, now we're really done. We've spent 148 lines, or 101 statements.&lt;/p&gt;
&lt;p&gt;The final version:
&lt;a class="attachment" href="/_file/query-builder-how/07-more-init/builder.py"&gt;builder.py&lt;/a&gt;,
&lt;a class="attachment" href="/_file/query-builder-how/07-more-init/test_builder.py"&gt;test_builder.py&lt;/a&gt;.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;Since the code in this article is essentially &lt;a class="external" href="https://github.com/lemon24/reader/blob/15121f667a6f2e388f0072a3fcd715f533883899/src/reader/_sql_utils.py"&gt;the reader one&lt;/a&gt;,
but without type annotations,
feel free to use it under the same BSD 3-Clause &lt;a class="external" href="https://github.com/lemon24/reader/blob/15121f667a6f2e388f0072a3fcd715f533883899/LICENSE"&gt;license&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;hr /&gt;
&lt;!-- TODO: wrapping up / conclusion section --&gt;

&lt;p&gt;That's it for now. :)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/query-builder-how&amp;t=Write%20an%20SQL%20query%20builder%20in%20150%20lines%20of%20Python%21"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Write%20an%20SQL%20query%20builder%20in%20150%20lines%20of%20Python%21%20https%3A//death.andgravity.com/query-builder-how"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/query-builder-how&amp;title=Write%20an%20SQL%20query%20builder%20in%20150%20lines%20of%20Python%21"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/query-builder-how"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Write%20an%20SQL%20query%20builder%20in%20150%20lines%20of%20Python%21&amp;url=https%3A//death.andgravity.com/query-builder-how&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;h2 id="bonus-things-that-didn-t-make-the-cut"&gt;Bonus: things that didn't make the cut&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bonus-things-that-didn-t-make-the-cut" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;When talking about &lt;a class="anchor" href="#trade-offs"&gt;trade-offs&lt;/a&gt;,
I said we'll only add features as needed;
this may seem a bit handwavy –
how can I tell adding them won't make the code explode?&lt;/p&gt;
&lt;p&gt;Because I &lt;em&gt;did&lt;/em&gt; add them;
that's what &lt;a class="internal" href="/own-query-builder#the-second-prototype"&gt;prototyping&lt;/a&gt; was for.
But since they weren't actually used, I removed them –
there's no point in them &lt;a class="external" href="https://en.wikipedia.org/wiki/Software_rot#Unused_code"&gt;rotting away&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Here's how you'd go about implementing a few of them.&lt;/p&gt;
&lt;h3 id="insert-update-delete"&gt;Insert / update / delete&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#insert-update-delete" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Make them flag keywords, to support the &lt;code&gt;OR ABORT/FAIL/...&lt;/code&gt; variants.&lt;/p&gt;
&lt;p&gt;To make VALUES bake in the parentheses, set its &lt;code&gt;format&lt;/code&gt; to &lt;code&gt;({value})&lt;/code&gt;.
That's to add one values tuple at a time.&lt;/p&gt;
&lt;p&gt;To add one column at a time, we could do this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;allow &lt;code&gt;add()&lt;/code&gt;ing INSERT with arbitrary flags&lt;/li&gt;
&lt;li&gt;make &lt;code&gt;INSERT('column', into='table')&lt;/code&gt;
a synonym of &lt;code&gt;add('INSERT INTO table', 'column')&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;classify INSERT and VALUES as &lt;code&gt;parens_keywords&lt;/code&gt;
– like &lt;code&gt;subquery_keywords&lt;/code&gt;, but they apply once per keyword, not per value&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It'd look like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# first insert sets flag&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INSERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;into&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;table&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VALUES&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# later we just add stuff&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;INSERT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;VALUES&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="arbitrary-strings-as-subqueries"&gt;Arbitrary strings as subqueries&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#arbitrary-strings-as-subqueries" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Allow setting &lt;code&gt;add(..., is_subquery=True)&lt;/code&gt;; you'd then do:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;subquery&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_subquery&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;not subquery&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="query-objects-as-subqueries"&gt;Query objects as subqueries&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#query-objects-as-subqueries" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Using Query objects as subqueries
without having to convert them explicitly to strings
would allow changing them &lt;em&gt;after&lt;/em&gt; being &lt;code&gt;add()&lt;/code&gt;ed.&lt;/p&gt;
&lt;p&gt;To do it, we just need to allow &lt;code&gt;_Thing.value&lt;/code&gt; to be a Query,
and override its &lt;code&gt;is_subquery&lt;/code&gt; based on an &lt;a class="external" href="https://docs.python.org/3/library/functions.html#isinstance"&gt;isinstance()&lt;/a&gt; check.&lt;/p&gt;
&lt;h3 id="union-intersect-except"&gt;Union / intersect / except&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#union-intersect-except" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;This one goes a bit meta:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;add a &amp;quot;virtual&amp;quot; COMPOUND keyword&lt;/li&gt;
&lt;li&gt;add a new &lt;code&gt;compound(keyword)&lt;/code&gt; method,
which moves everything except WITH and ORDER BY to a subquery,
and appends the subquery to &lt;code&gt;data['COMPOUND']&lt;/code&gt; with the appropriate fake keyword&lt;/li&gt;
&lt;li&gt;make &lt;code&gt;__getattr__&lt;/code&gt; return a &lt;code&gt;compound()&lt;/code&gt; partial for compound keywords&lt;/li&gt;
&lt;li&gt;special-case COMPOUND in &lt;code&gt;_lines()&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;The guaranteed insertion order was actually added
to the language specification in 3.7,
but in 3.6 both CPython and PyPy had it as an implementation detail. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;This works because &lt;code&gt;False == 0&lt;/code&gt; and &lt;code&gt;True == 1&lt;/code&gt;, and is likely too clever. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-3"&gt;&lt;p&gt;That's &lt;a class="external" href="https://github.com/lemon24/reader/blob/44d1f9af31dcb5bed80b4d798206bd02ae59d127/src/reader/_sql_utils.py#L77-L83"&gt;what I did initially&lt;/a&gt;. &lt;a href="#fnref-3" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-4"&gt;&lt;p&gt;An alternate constructor or a subclass might have been a better choice here.
We'll fix it if we need &lt;code&gt;__init__&lt;/code&gt; for something else. ¯\_(ツ)_/¯ &lt;a href="#fnref-4" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/query-builder-how" rel="alternate"/>
    <summary>This is the fourth article in a series about writing my own SQL query builder. Today, we'll rewrite it from scratch, explore API design, learn when to be lazy, and look at worse and better ways of doing things – all in 150 lines of Python!</summary>
    <published>2021-08-20T16:42:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/namedtuples">
    <id>https://death.andgravity.com/namedtuples</id>
    <title>namedtuple in a post-dataclasses world</title>
    <updated>2021-07-21T14:15:00+00:00</updated>
    <content type="html">&lt;p&gt;&lt;strong&gt;namedtuple&lt;/strong&gt; has been around since forever,&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;
and over time, its convenience saw it used
far outside its originally intended purpose.&lt;/p&gt;
&lt;p&gt;With &lt;strong&gt;dataclasses&lt;/strong&gt; now covering part of those use cases,
what &lt;em&gt;should&lt;/em&gt; one use named tuples for?&lt;/p&gt;
&lt;p&gt;In this article, we take a look at exactly that, with a few examples from real code.&lt;/p&gt;
&lt;details class="toc"&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#what-are-named-tuples-used-for"&gt;What are named tuples used for?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-problems-with-named-tuples"&gt;The problems with named tuples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#what-are-named-tuples-still-good-for"&gt;What are named tuples still good for?&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#the-data-is-naturally-a-tuple"&gt;The data is naturally a tuple&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#you-re-already-using-a-tuple"&gt;You're already using a tuple&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#you-want-consumers-that-do-unpacking-to-fail"&gt;You want consumers that do unpacking to fail&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#memory-and-speed"&gt;Memory and speed&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="what-are-named-tuples-used-for"&gt;What are named tuples used for?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-are-named-tuples-used-for" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a class="external" href="https://docs.python.org/3/library/collections.html#collections.namedtuple"&gt;namedtuple&lt;/a&gt;&lt;/strong&gt; exists in the standard library since Python 2.6,
and allows building tuple subclasses that
also have fields accessible by attribute lookup.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;collections&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;namedtuple&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;Point&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;namedtuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Point&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;x y&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In general, this is useful when wrapping structured data; from the docs:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Named tuples are especially useful for assigning field names to result tuples returned by the csv or sqlite3 modules.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Because of how easy they are to define,
named tuples have also been used for:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;quick-and-dirty temporary data structures,
more readable than plain tuples and regular classes
(you get constructor keyword arguments and a &lt;code&gt;__repr__&lt;/code&gt; for free)&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.python.org/3/glossary.html#term-hashable"&gt;hashable&lt;/a&gt; instances
(to use as dict keys or set members,
or as arguments to functions decorated with e.g. &lt;a class="external" href="https://docs.python.org/3/library/functools.html#functools.lru_cache"&gt;functools.lru_cache&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://docs.python.org/3/glossary.html#term-immutable"&gt;immutable&lt;/a&gt; instances&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;&lt;a class="external" href="https://docs.python.org/3/library/dataclasses.html"&gt;dataclasses&lt;/a&gt;&lt;/strong&gt; was added in Python 3.7,
and allows writing regular classes just as easily,
by generating the required special methods.
With &lt;a class="external" href="https://docs.python.org/3/library/dataclasses.html#frozen-instances"&gt;frozen instances&lt;/a&gt;, it even covers hashable and immutable instances.&lt;/p&gt;
&lt;p&gt;Before dataclasses,
named tuples were used for the last three use cases because
there were no other good alternatives in the standard library –
you &lt;em&gt;can&lt;/em&gt; do it with a normal class definition,
but you have to write all the special methods by hand.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;In case you've never used them, here's a comparison.&lt;/summary&gt;

&lt;p&gt;I'm using &lt;a class="external" href="https://docs.python.org/3/library/typing.html#typing.NamedTuple"&gt;typing.NamedTuple&lt;/a&gt; because it looks similar to dataclasses;
the result is the same as that of the
traditional &lt;a class="external" href="https://docs.python.org/3/library/collections.html#collections.namedtuple"&gt;collections.namedtuple&lt;/a&gt; factory.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="go"&gt;Point(x=1, y=2)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;
&lt;span class="go"&gt;1&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="go"&gt;1&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;[1, 2]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Point&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;
&lt;span class="go"&gt;Point(x=1, y=2)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;
&lt;span class="go"&gt;1&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;&amp;#39;Point&amp;#39; object is not subscriptable&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;&amp;#39;Point&amp;#39; object is not iterable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;h2 id="the-problems-with-named-tuples"&gt;The problems with named tuples&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-problems-with-named-tuples" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="external" href="https://www.python.org/dev/peps/pep-0557/"&gt;PEP 557&lt;/a&gt;&lt;sup class="footnote-ref" id="fnref-3"&gt;&lt;a href="#fn-3"&gt;3&lt;/a&gt;&lt;/sup&gt; explains why sometimes namedtuple &lt;a class="external" href="https://www.python.org/dev/peps/pep-0557/#why-not-just-use-namedtuple"&gt;isn't good enough&lt;/a&gt;;
in summary:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The instances are always iterable;
this can make it difficult to add fields,
because adding a new field will break code that uses unpacking.&lt;ul&gt;
&lt;li&gt;Also, if used as return value in a backwards-compatible API,
it means the result must remain iterable/indexable forever,
even if you later stop using namedtuple.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;Instances can be accidentally compared with any other tuple.&lt;/li&gt;
&lt;li&gt;There's no mutable version (in the standard library).&lt;/li&gt;
&lt;li&gt;Fields can't be combined by inheritance.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="what-are-named-tuples-still-good-for"&gt;What are named tuples still good for?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-are-named-tuples-still-good-for" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;With the drawbacks mentioned above,
and with dataclasses covering a lot of their (maybe unintended) use cases,
are named tuples good for anything anymore?&lt;/p&gt;
&lt;p&gt;As you'd expect, the answer is &lt;em&gt;yes&lt;/em&gt;.&lt;/p&gt;
&lt;h3 id="the-data-is-naturally-a-tuple"&gt;The data is naturally a tuple&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-data-is-naturally-a-tuple" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Named tuples remain perfect for their originally intended purpose:
ordered, structured data.&lt;/p&gt;
&lt;p&gt;Some examples:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;rows returned by a database query&lt;/li&gt;
&lt;li&gt;the result of parsing a binary file format&lt;/li&gt;
&lt;li&gt;pairs of things, like HTTP headers
(a dict is not always appropriate,
since the same header can appear more than once,
and the order does matter in some cases)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Pairs of things are interesting,
because both unpacking and attribute access are valid usage patterns.&lt;/p&gt;
&lt;p&gt;For example, for my feed reader library
I use a named tuple to model &lt;a class="external" href="https://reader.readthedocs.io/en/stable/api.html#reader.UpdateResult"&gt;the result of a feed update&lt;/a&gt;,
a (feed URL, update details or exception) pair.&lt;/p&gt;
&lt;p&gt;This makes it easier to make sense of what a value &lt;em&gt;means&lt;/em&gt;
in interactive sessions or when debugging;
compare the named and unnamed versions:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update_feeds_iter&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;
&lt;span class="go"&gt;UpdateResult(url=&amp;#39;http://antirez.com/rss&amp;#39;, value=None)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;tuple&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;(&amp;#39;http://antirez.com/rss&amp;#39;, None)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Also, the distinct class allows having
a docstring that users can look at with &lt;a class="external" href="https://docs.python.org/3/library/functions.html#help"&gt;help()&lt;/a&gt;,
and better semantics via properties
(&lt;code&gt;error&lt;/code&gt;/&lt;code&gt;ok&lt;/code&gt; &lt;a class="external" href="https://github.com/lemon24/reader/issues/204#issuecomment-780553373"&gt;here&lt;/a&gt;).&lt;/p&gt;
&lt;h3 id="you-re-already-using-a-tuple"&gt;You're already using a tuple&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#you-re-already-using-a-tuple" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;You're already using a tuple, and want to make &lt;em&gt;new&lt;/em&gt; code more readable:
a namedtuple gets you this, but guarantees you won't break &lt;em&gt;old&lt;/em&gt; code.&lt;/p&gt;
&lt;p&gt;Some people argue that wherever you return a non-trivial tuple,
you should be returning a namedtuple instead. I tend to agree.&lt;/p&gt;
&lt;h3 id="you-want-consumers-that-do-unpacking-to-fail"&gt;You want consumers that do unpacking to fail&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#you-want-consumers-that-do-unpacking-to-fail" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;In some cases, you &lt;em&gt;want&lt;/em&gt; consumers that do unpacking to fail.&lt;/p&gt;
&lt;p&gt;For example, in my feed reader library,
I use a named tuple to group &lt;a class="external" href="https://github.com/lemon24/reader/blob/2.0/src/reader/_types.py#L292"&gt;arguments related to filtering&lt;/a&gt;,
because there's a lot of them,
and they get passed around quite a bit before being used
(I cover why in more detail &lt;a class="internal" href="/more-arguments#counter-example-data-classes"&gt;here&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;I know all arguments should always be handled, so
I &lt;a class="external" href="https://github.com/lemon24/reader/blob/2.0/src/reader/_storage.py#L1261"&gt;use unpacking&lt;/a&gt; specifically because
I want the code to fail when a new one is added –
if I used attribute access,
the code would silently succeed.
This is no substitute for tests,
but the early warning is nice,
especially in a larger code base.&lt;/p&gt;
&lt;h3 id="memory-and-speed"&gt;Memory and speed&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#memory-and-speed" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Last, but not least, named tuples are useful if you care about memory or speed;
&lt;em&gt;they are much smaller&lt;/em&gt; and faster than the equivalent (data)class.
In most cases, the difference doesn't matter,
but it can become noticeable if you create millions of instances.&lt;/p&gt;
&lt;p&gt;Setting &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-slots"&gt;__slots__&lt;/a&gt; helps with memory, but doesn't really help with speed.&lt;/p&gt;
&lt;p&gt;Here's a quick comparison:&lt;/p&gt;
&lt;table class="table"&gt;
&lt;thead&gt;
&lt;tr&gt;
  &lt;th style="text-align:left"&gt;&lt;/th&gt;
  &lt;th style="text-align:right"&gt;cls(1, 2)&lt;/th&gt;
  &lt;th style="text-align:right"&gt;obj.a&lt;/th&gt;
  &lt;th style="text-align:right"&gt;hash(obj)&lt;/th&gt;
  &lt;th style="text-align:right"&gt;size&lt;/th&gt;
  &lt;th style="text-align:right"&gt;total size&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
  &lt;td style="text-align:left"&gt;dataclass&lt;/td&gt;
  &lt;td style="text-align:right"&gt;846.9&lt;/td&gt;
  &lt;td style="text-align:right"&gt;49.7&lt;/td&gt;
  &lt;td style="text-align:right"&gt;361.7&lt;/td&gt;
  &lt;td style="text-align:right"&gt;152&lt;/td&gt;
  &lt;td style="text-align:right"&gt;320&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td style="text-align:left"&gt;dataclass + slots&lt;/td&gt;
  &lt;td style="text-align:right"&gt;709.1&lt;/td&gt;
  &lt;td style="text-align:right"&gt;45.5&lt;/td&gt;
  &lt;td style="text-align:right"&gt;342.5&lt;/td&gt;
  &lt;td style="text-align:right"&gt;48&lt;/td&gt;
  &lt;td style="text-align:right"&gt;104&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td style="text-align:left"&gt;namedtuple&lt;/td&gt;
  &lt;td style="text-align:right"&gt;465.3&lt;/td&gt;
  &lt;td style="text-align:right"&gt;43.2&lt;/td&gt;
  &lt;td style="text-align:right"&gt;99.6&lt;/td&gt;
  &lt;td style="text-align:right"&gt;56&lt;/td&gt;
  &lt;td style="text-align:right"&gt;112&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td style="text-align:left"&gt;dataobject + gc&lt;/td&gt;
  &lt;td style="text-align:right"&gt;150.3&lt;/td&gt;
  &lt;td style="text-align:right"&gt;43.1&lt;/td&gt;
  &lt;td style="text-align:right"&gt;104.1&lt;/td&gt;
  &lt;td style="text-align:right"&gt;48&lt;/td&gt;
  &lt;td style="text-align:right"&gt;48&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
  &lt;td style="text-align:left"&gt;dataobject&lt;/td&gt;
  &lt;td style="text-align:right"&gt;136.4&lt;/td&gt;
  &lt;td style="text-align:right"&gt;45.1&lt;/td&gt;
  &lt;td style="text-align:right"&gt;106.5&lt;/td&gt;
  &lt;td style="text-align:right"&gt;32&lt;/td&gt;
  &lt;td style="text-align:right"&gt;32&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;code&gt;cls(1, 2)&lt;/code&gt;, &lt;code&gt;obj.a&lt;/code&gt;, &lt;code&gt;hash(obj)&lt;/code&gt; are timings for that expression,
in nanoseconds.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;size&lt;/em&gt; is the &lt;a class="external" href="https://docs.python.org/3/library/sys.html#sys.getsizeof"&gt;sys.getsizeof&lt;/a&gt; of the object itself
plus that of its &lt;code&gt;__dict__&lt;/code&gt; (if any), excluding the actual values.
&lt;em&gt;total size&lt;/em&gt; includes the values, as returned by Pympler's &lt;a class="external" href="https://pympler.readthedocs.io/en/stable/library/asizeof.html#pympler.asizeof.asizeof"&gt;asizeof&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I ran this with 64-bit CPython 3.8 on macOS;
Linux looks roughly the same.&lt;/p&gt;
&lt;p&gt;When increasing the number of fields, &lt;code&gt;obj.a&lt;/code&gt; remains constant,
while the other timings increase proportionally.
The slots dataclass is always 8 bytes smaller than the namedtuple.&lt;/p&gt;
&lt;p&gt;For the &lt;em&gt;dataobject&lt;/em&gt; rows I used &lt;a class="external" href="https://bitbucket.org/intellimath/recordclass/src/master/README.md"&gt;recordclass&lt;/a&gt;,
which provides dataclass/namedtuple-equivalent types.
The version without &lt;em&gt;gc&lt;/em&gt; doesn't participate in cyclic garbage collection,
so it shouldn't be used for recursive data structures.&lt;/p&gt;
&lt;p&gt;The library still has some rough edges, though:
the documentation is a bit confusing,
and I had to use the (yet unreleased) 0.15 version to get it working;
also, note the wrong &lt;em&gt;total size&lt;/em&gt; (it may be a Pympler bug).
Nevertheless, the numbers are pretty compelling, and
&lt;em&gt;if you have this problem&lt;/em&gt;, it's definitely worth a look.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;The class definitions:&lt;/summary&gt;

&lt;p&gt;For dataclasses, &lt;code&gt;__slots__&lt;/code&gt; must be set explicitly;
this was fixed &lt;a class="external" href="https://bugs.python.org/issue42269"&gt;in Python 3.10&lt;/a&gt;.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;typing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NamedTuple&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dataclasses&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;recordclass&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataobject&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;NT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;DC&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;frozen&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;DS&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="vm"&gt;__slots__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;DO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataobject&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;__options__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readonly&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fast_new&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;DG&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dataobject&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
    &lt;span class="n"&gt;__options__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;readonly&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fast_new&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;gc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;hr /&gt;
&lt;p&gt;That's it for now. :)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/namedtuples&amp;t=namedtuple%20in%20a%20post-dataclasses%20world"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=namedtuple%20in%20a%20post-dataclasses%20world%20https%3A//death.andgravity.com/namedtuples"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/namedtuples&amp;title=namedtuple%20in%20a%20post-dataclasses%20world"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/namedtuples"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=namedtuple%20in%20a%20post-dataclasses%20world&amp;url=https%3A//death.andgravity.com/namedtuples&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;&lt;a class="external" href="https://code.activestate.com/recipes/500261-named-tuples/"&gt;2007&lt;/a&gt;–&lt;a class="external" href="https://docs.python.org/3/whatsnew/2.6.html#new-and-improved-modules"&gt;2008&lt;/a&gt; seems like forever enough these days. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;See &lt;a class="internal" href="/same-arguments#caveat-attribute-changes-are-confusing"&gt;this&lt;/a&gt;
for an example of why you might want immutable instances. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-3"&gt;&lt;p&gt;I also cover PEP 557 and what can be learned from it &lt;a class="external" href="https://death.andgravity.com/stdlib#dataclasses"&gt;here&lt;/a&gt;. &lt;a href="#fnref-3" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/namedtuples" rel="alternate"/>
    <summary>namedtuple has been around since forever, and over time, its convenience saw it used far outside its originally intended purpose. With dataclasses now covering part of those use cases, what should one use named tuples for? In this article, I address this question, and give a few examples from real code.</summary>
    <published>2021-07-21T07:55:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/reader-2-0">
    <id>https://death.andgravity.com/reader-2-0</id>
    <title>reader 2.0 released</title>
    <updated>2021-07-19T14:12:00+00:00</updated>
    <content type="html">&lt;p&gt;Hi there!&lt;/p&gt;
&lt;p&gt;I'm happy to announce version 2.0 of &lt;strong&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/strong&gt;, a Python feed reader library.&lt;/p&gt;
&lt;p&gt;This release brings you a cleaner API,
more consistently named methods and attributes,
timezone-aware datetimes,
and safer defaults.
See the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/changelog.html#version-2-0"&gt;changelog&lt;/a&gt; for details.&lt;/p&gt;
&lt;h2 id="what-is-reader"&gt;What is reader?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-is-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;reader&lt;/strong&gt; takes care
of the core functionality required by a feed reader,
so you can focus on what makes &lt;strong&gt;yours&lt;/strong&gt; different.&lt;/p&gt;
&lt;p&gt;&lt;img class="img-responsive" src="/_file/reader-2-0/reader.png" alt="reader in action" /&gt;
&lt;em&gt;reader&lt;/em&gt; allows you to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;retrieve, store, and manage &lt;strong&gt;Atom&lt;/strong&gt;, &lt;strong&gt;RSS&lt;/strong&gt;, and &lt;strong&gt;JSON&lt;/strong&gt; feeds&lt;/li&gt;
&lt;li&gt;mark entries as read or important&lt;/li&gt;
&lt;li&gt;add tags and metadata to feeds&lt;/li&gt;
&lt;li&gt;filter feeds and articles&lt;/li&gt;
&lt;li&gt;full-text search articles&lt;/li&gt;
&lt;li&gt;write plugins to extend its functionality&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;...all these with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a high-level, stable, clearly documented API&lt;/li&gt;
&lt;li&gt;excellent test coverage&lt;/li&gt;
&lt;li&gt;fully typed Python&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To find out more, check out the &lt;a class="external" href="https://github.com/lemon24/reader"&gt;GitHub repo&lt;/a&gt; and the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/"&gt;docs&lt;/a&gt;,
or give the &lt;a class="external" href="https://reader.readthedocs.io/en/stable/tutorial.html"&gt;tutorial&lt;/a&gt; a try.&lt;/p&gt;
&lt;h2 id="why-use-a-feed-reader-library"&gt;Why use a feed reader library?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-use-a-feed-reader-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Have you been unhappy with existing feed readers and wanted to make your own, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;never knew where to start?&lt;/li&gt;
&lt;li&gt;it seemed like too much work?&lt;/li&gt;
&lt;li&gt;you don't like writing backend code?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Are you already working with &lt;a class="external" href="https://feedparser.readthedocs.io/en/latest/"&gt;feedparser&lt;/a&gt;, but:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;want an easier way to store, filter, sort and search feeds and entries?&lt;/li&gt;
&lt;li&gt;want to get back type-annotated objects instead of dicts?&lt;/li&gt;
&lt;li&gt;want to restrict or deny file-system access?&lt;/li&gt;
&lt;li&gt;want to change the way feeds are retrieved by using &lt;a class="external" href="https://requests.readthedocs.io"&gt;Requests&lt;/a&gt;?&lt;/li&gt;
&lt;li&gt;want to also support &lt;a class="external" href="https://jsonfeed.org/"&gt;JSON Feed&lt;/a&gt;?&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... while still supporting all the feed types feedparser does?&lt;/p&gt;
&lt;p&gt;If you answered yes to any of the above, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
&lt;h2 id="why-make-your-own-feed-reader"&gt;Why make your own feed reader?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-make-your-own-feed-reader" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;have full control over your data&lt;/li&gt;
&lt;li&gt;control what features it has or doesn't have&lt;/li&gt;
&lt;li&gt;decide how much you pay for it&lt;/li&gt;
&lt;li&gt;make sure it doesn't get closed while you're still using it&lt;/li&gt;
&lt;li&gt;really, it's &lt;a class="external" href="https://rachelbythebay.com/w/2011/10/26/fred/"&gt;easier than you think&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Obviously, this may not be your cup of tea, but if it is, &lt;em&gt;reader&lt;/em&gt; can help.&lt;/p&gt;
</content>
    <link href="https://death.andgravity.com/reader-2-0" rel="alternate"/>
    <published>2021-07-18T16:10:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/own-query-builder">
    <id>https://death.andgravity.com/own-query-builder</id>
    <title>Why I wrote my own SQL query builder (in Python)</title>
    <updated>2021-07-13T14:48:00+00:00</updated>
    <content type="html">&lt;p&gt;&lt;strong&gt;&lt;a class="internal" href="/query-builder-why"&gt;Previously&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is the third article &lt;a class="internal" href="/query-builder"&gt;in a series&lt;/a&gt; about
writing an SQL query builder in 150 lines of Python.&lt;/p&gt;
&lt;p&gt;Today, we'll talk about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;why I decided &lt;strong&gt;to write my own&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;the alternatives&lt;/strong&gt; I considered&lt;/li&gt;
&lt;li&gt;why I didn't use &lt;strong&gt;an existing library&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;how I knew it wouldn't become a maintenance burden&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The first part is in rough chronological order.
If you're only interested in the libraries I looked at,
you can find the list &lt;a class="anchor" href="#other-alternatives"&gt;at the end&lt;/a&gt;.&lt;/p&gt;
&lt;details class="toc"&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#background"&gt;Background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-first-prototype"&gt;The first prototype&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#requirements-and-existing-libraries"&gt;Requirements, and existing libraries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-second-prototype"&gt;The second prototype&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#deciding-to-use-my-own"&gt;Deciding to use my own&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#other-alternatives"&gt;Other alternatives&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#do-nothing"&gt;Do nothing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#disable-parts-of-the-query"&gt;Disable parts of the query&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#sqlalchemy-core-peewee-query-builder"&gt;SQLAlchemy Core, Peewee query builder&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#sqlbuilder-pypika-python-sql"&gt;SQLBuilder, PyPika, python-sql&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#sqlbuilder-mini-psycopg2-sql"&gt;sqlbuilder.mini, psycopg2.sql&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#jinjasql-sqlpy-pugsql"&gt;JinjaSQL, SQLpy, PugSQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#pony"&gt;Pony&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="background"&gt;Background&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#background" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;&lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt;&lt;/em&gt; is a Python feed reader library –
it allows users to retrieve, store, and manage &lt;a class="external" href="https://en.wikipedia.org/wiki/Web_feed"&gt;web feeds&lt;/a&gt;
through a high-level API,
without having to deal with feed-related details.&lt;/p&gt;
&lt;p&gt;It is a hobby and learning project,
and I can only spend a limited amount of time on it,
sometimes quite far in between.
It's not necessarily about learning technologies;
rather, it is about library design,
writing code that's maintainable long-term, and showing restraint –
&lt;em&gt;if I were to make the best library I could,
what would it look like?&lt;/em&gt;&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;Because of this,
the conclusions of this article, if any,
may not be &lt;em&gt;directly&lt;/em&gt; applicable to regular &amp;quot;work&amp;quot; projects.
With &lt;em&gt;reader&lt;/em&gt;, I have different constraints over different time scales,
a somewhat different definition of success,
and more freedom in some aspects.&lt;/p&gt;
&lt;p&gt;However, not all projects are the same,
and not all parts of a project are the same.
Sometimes, this kind of long-term thinking can be useful,
and it can actually be achieved
through a combination of planning,
strategical technical debt,
and saying no to, reducing the scope of, or postponing features.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;In the spirit of keeping things maintainable,
the core library is fully typed and has 100% test coverage,
to make refactoring straightforward (if not painless).&lt;/p&gt;
&lt;p&gt;Also, almost from the start,
I put all storage code in a single module,
behind a &lt;a class="internal" href="/more-arguments#data-access-object"&gt;data access object&lt;/a&gt;;
the rest of the library doesn't even know it's talking to a database.&lt;/p&gt;
&lt;p&gt;One of &lt;em&gt;reader's&lt;/em&gt; main features is filtering articles by
article metadata,
user-set metadata (read, important, feed tags),
and full-text search;
the results can be sorted in various ways and paginated.&lt;/p&gt;
&lt;p&gt;In May 2019,
with less than half of the above implemented,
the function building the SQL query was over 100 lines,
and I had already felt the need to add &lt;a class="external" href="https://github.com/lemon24/reader/blob/7b47c88bb4e1388e7c5af1c269fb4a78e227120a/src/reader/_storage.py#L621"&gt;this comment&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This needs some sort of query builder so badly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2 id="the-first-prototype"&gt;The first prototype&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-first-prototype" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I opened &lt;a class="external" href="https://github.com/lemon24/reader/issues/123"&gt;an issue&lt;/a&gt;, and did some research.&lt;/p&gt;
&lt;p&gt;At some point I stumbled upon &lt;a class="external" href="https://sqlbuilder.readthedocs.io/en/latest/#short-manual-for-sqlbuilder-mini"&gt;sqlbuilder.mini&lt;/a&gt;,
which was built around an interesting insight –
&lt;em&gt;queries can be represented as plain data structures&lt;/em&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="s1"&gt;&amp;#39;SELECT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;first_name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="s1"&gt;&amp;#39;FROM&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;author&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="s1"&gt;&amp;#39;WHERE&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;status&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;==&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;P&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;new&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)],&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;(&amp;#39;SELECT first_name FROM author WHERE status == %s&amp;#39;, [&amp;#39;new&amp;#39;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can then modify the query directly:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SELECT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;last_name&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;(&amp;#39;SELECT first_name, last_name FROM author WHERE status == %s&amp;#39;, [&amp;#39;new&amp;#39;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Or via a wrapper that simplifies navigation:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Q&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append_child&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SELECT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;  &lt;span class="c1"&gt;# path&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;age&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# returns self to allow method chaining&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;sqlbuilder.mini.Q object&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sql&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;(&amp;#39;SELECT first_name, last_name, age FROM author WHERE status == %s&amp;#39;, [&amp;#39;new&amp;#39;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;!--

sql = [
    'SELECT', ['first_name'],
    'FROM', ['author'],
    'WHERE', ['status', '==', P('new')],
]
compile(sql)
sql[sql.index('SELECT') + 1].append('last_name')
compile(sql)
sql = Q(sql)
sql.append_child(
    ['SELECT'],  # path
    ['age']
)  # returns self to allow method chaining
compile(sql)

--&gt;

&lt;p&gt;I really liked how simple and flexible this is,
and the choice of not dealing with SQL correctness or dialects –
a middle ground between building strings by hand
and &amp;quot;proper&amp;quot; query builders.
On the other hand,
it seemed too verbose (even with the wrapper),
and the generated SQL wasn't very readable.&lt;/p&gt;
&lt;p&gt;Surely, it would be possible to make this look like &lt;code&gt;sql.SELECT('age')&lt;/code&gt;, right?&lt;/p&gt;
&lt;p&gt;So I made a prototype –
with no real intention of using it, just to see how easy it is to do.
The core was quite short, about 80 lines; my thoughts at the time:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The end result looks nice, but using it would add ~150 lines of code (that need to be tested), and it's less useful for simpler queries.&lt;/p&gt;
&lt;p&gt;Also, it looks nice &lt;em&gt;now&lt;/em&gt;, when I just wrote it; 6 months from now it may be hard to understand.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Afraid I'm &lt;em&gt;too&lt;/em&gt; happy with it, and with my curiosity satisfied,
I did just that: postponed for six months.&lt;/p&gt;
&lt;h2 id="requirements-and-existing-libraries"&gt;Requirements, and existing libraries&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#requirements-and-existing-libraries" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;My main concern with building my own was that over time,
with additions and fixes,
the effort would be &lt;em&gt;greater&lt;/em&gt; than
that of getting an existing library to do what I needed.&lt;/p&gt;
&lt;p&gt;I did two things to deal with this.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;First, I wrote down detailed requirements.&lt;/p&gt;
&lt;p&gt;Whatever I used had to support the following:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;SELECT with conditional WHERE, ORDER BY, JOIN etc.
(&lt;a class="internal" href="/query-builder-why#separation-of-concerns"&gt;example&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;common table expressions (WITH)&lt;/li&gt;
&lt;li&gt;&lt;a class="internal" href="/query-builder-why#intermission-scrolling-window-queries"&gt;scrolling window queries&lt;/a&gt;
(or be possible to build on top)&lt;/li&gt;
&lt;li&gt;arbitrary SQL (so I don't have to use the query builder for everything)&lt;/li&gt;
&lt;li&gt;the order in which you add clauses shouldn't matter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Also, it should be easy to understand and maintain,
and it should be possible to support additional SQL features.&lt;/p&gt;
&lt;p&gt;Because &lt;em&gt;reader&lt;/em&gt; is a library,
I wanted to keep the number of (transitive) dependencies as small as possible,
since any extra dependency gets passed down to the users.&lt;/p&gt;
&lt;p&gt;Both to keep things simple and due to &lt;a class="external" href="https://reader.readthedocs.io/en/latest/dev.html#why-use-sqlite-and-not-sqlalchemy"&gt;historical reasons&lt;/a&gt;,
I did not want to switch to an abstraction layer like SQLAlchemy Core
&lt;em&gt;just&lt;/em&gt; for query building –
I needed (and still need) only SQLite support,
and already had code to deal with stuff like migrations.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Second, I did a slightly more serious survey of existing libraries.&lt;/p&gt;
&lt;p&gt;I didn't feel any of the ones I looked at was ideal,
for at least one of these reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;They came with a full abstraction layer.
This isn't bad in itself,
but meant I had to switch &lt;em&gt;everything&lt;/em&gt;, eventually –
using a mix would make things worse.&lt;/li&gt;
&lt;li&gt;They had too many features.
Usually this is good, but it means there's more of everything:
more features, more documentation to go through,
more concepts to keep in your head,
more things contributors need to know or learn.&lt;/li&gt;
&lt;li&gt;They didn't make things more readable or
more &lt;a class="internal" href="/query-builder-why#composition-and-reuse"&gt;composable&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;They weren't actively maintained.&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;Again, I chose to wait until more features are implemented.&lt;/p&gt;
&lt;h2 id="the-second-prototype"&gt;The second prototype&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-second-prototype" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;By May 2020, most of the features &lt;em&gt;were&lt;/em&gt; implemented.
The function building the query was 150 lines,
with part of it duplicated for search.
At some point, I tried to optimize it to use indexes,
but gave up because trying things simply took too long.&lt;/p&gt;
&lt;p&gt;So, &lt;em&gt;a full year later&lt;/em&gt;,
I made the prototype support all the required features
and a few extra (UNION, nested queries),
and tried it out on the full real queries.&lt;/p&gt;
&lt;p&gt;It didn't take all that long,
and it remained around 100 lines.&lt;/p&gt;
&lt;h2 id="deciding-to-use-my-own"&gt;Deciding to use my own&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#deciding-to-use-my-own" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;At this point, most of the work was already done, and integrating it took less than an hour.&lt;/p&gt;
&lt;p&gt;Excluding the 136 lines of the builder itself with scrolling window query support,
the code went from 1400 to 1300 lines.
I took that as a win, since for the price of 36 lines I was able to reuse the filtering logic.
(Now, one year later, it enabled a lot more reuse, without growing significantly.)&lt;/p&gt;
&lt;p&gt;I ended up keeping it, because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Using an existing library would take too much effort.
(I'll reconsider when the requirements change, though.)&lt;/li&gt;
&lt;li&gt;It is tiny, which makes it relatively easy to understand and modify;
the prototypes made me quite confident it's likely to stay that way.
Because it is only used internally,
I can leave out a lot of nice things that aren't actually needed
(including the extra features).&lt;/li&gt;
&lt;li&gt;It has 0 dependencies. That's even better than 1.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;reader&lt;/em&gt; already had great test coverage,
so little additional testing was required.&lt;/li&gt;
&lt;/ul&gt;
&lt;section class="admonition attention"&gt;
&lt;p class="admonition-title"&gt;Attention&lt;/p&gt;
&lt;p&gt;My query builder is not directly comparable with that of an ORM.
Instead, it is an alternative to building &lt;em&gt;plain SQL&lt;/em&gt; strings by hand.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The caveats that apply to plain SQL apply to it as well:&lt;/strong&gt;
Using user-supplied values directly in an SQL query
exposes you to &lt;a class="external" href="https://en.wikipedia.org/wiki/SQL_injection"&gt;SQL injection&lt;/a&gt; attacks.
Instead, use &lt;a class="external" href="https://en.wikipedia.org/wiki/SQL_injection#Parameterized_statements"&gt;parametrized queries&lt;/a&gt; whenever possible,
and &lt;a class="external" href="https://en.wikipedia.org/wiki/SQL_injection#Escaping"&gt;escaping&lt;/a&gt; only as a last resort.&lt;/p&gt;
&lt;p&gt;Since I was coming from plain SQL, I was already doing all of this.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="other-alternatives"&gt;Other alternatives&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#other-alternatives" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Here's a non-exhaustive list of other things I looked at.
I'm only covering the libraries I actually considered using,
or that are interesting in some way.
There are others out there;
some aren't actively maintained,
some I simply missed.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Update (July 2021)&lt;/p&gt;
&lt;p&gt;I added a few more interesting libraries.&lt;/p&gt;
&lt;/section&gt;
&lt;h3 id="do-nothing"&gt;Do nothing&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#do-nothing" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;It's worth keeping in mind that &amp;quot;do nothing&amp;quot;
&lt;a class="internal" href="/sentinels#is-this-worth-a-pep"&gt;is always an option&lt;/a&gt;
– probably the first option to consider, in many cases.&lt;/p&gt;
&lt;p&gt;There's two kinds of doing nothing:
&lt;em&gt;passive&lt;/em&gt;, where you wait for new requirements to come up
– for the problem to reveal itself –,
and &lt;em&gt;active&lt;/em&gt;, where you explore options,
but don't commit to anything just yet.&lt;/p&gt;
&lt;p&gt;I ended up doing both, to a point.&lt;/p&gt;
&lt;h3 id="disable-parts-of-the-query"&gt;Disable parts of the query&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#disable-parts-of-the-query" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;An interesting (but quickly abandoned) idea was to not build queries;
instead, have just one query,
and disable parts of it with boolean or optional parameters,
and hope the &lt;a class="external" href="https://www.sqlite.org/queryplanner.html"&gt;query planner&lt;/a&gt; optimizes it:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="SQL"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(:&lt;/span&gt;&lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;IS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;...&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;-- 7 more expressions like this&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;There are two huge issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I'm not sure &lt;em&gt;any&lt;/em&gt; optimizer is that smart
(also, the query might be optimized before the parameters are passed in).
Even if it were, I'm not smart enough to design indexes for a query like this.&lt;/li&gt;
&lt;li&gt;It doesn't seem possible to do it for JOIN, different ORDER BY terms,
or even an arbitrary number of WHERE conditions (e.g. for tags).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Even if they were all possible, the result would be almost impossible to understand.&lt;/p&gt;
&lt;h3 id="sqlalchemy-core-peewee-query-builder"&gt;SQLAlchemy Core, Peewee query builder&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#sqlalchemy-core-peewee-query-builder" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a class="external" href="https://www.sqlalchemy.org/"&gt;SQLAlchemy&lt;/a&gt; and &lt;a class="external" href="http://docs.peewee-orm.com/"&gt;Peewee&lt;/a&gt; are both SQL toolkits / object-relational mappers.&lt;/p&gt;
&lt;p&gt;SQLAlchemy has over 15 years of history,
and is &lt;em&gt;the&lt;/em&gt; database toolkit for Python.
Hell, there's even an Architecture of Open Source Applications
&lt;a class="external" href="http://aosabook.org/en/sqlalchemy.html"&gt;chapter&lt;/a&gt; about it.&lt;/p&gt;
&lt;p&gt;Peewee is a bit younger (~10 years), simple and small by design.&lt;/p&gt;
&lt;p&gt;Both have a lot of extensions&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;,
and can be used without the ORM part;
Peewee can even generate &lt;a class="external" href="http://docs.peewee-orm.com/en/latest/peewee/query_builder.html"&gt;plain SQL&lt;/a&gt; without defining models.&lt;/p&gt;
&lt;p&gt;In the end, both seemed too complicated,
and meant I had to switch to them eventually,
adding the burden of researching a use case I don't have yet.
However, if I ever need multi-database support,
it's likely I'll use one of them.&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;&lt;a id="sqlbuilder-pypika"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="sqlbuilder-pypika-python-sql"&gt;SQLBuilder, PyPika, python-sql&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#sqlbuilder-pypika-python-sql" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a class="external" href="https://sqlbuilder.readthedocs.io/en/latest/"&gt;SQLBuilder&lt;/a&gt; and &lt;a class="external" href="https://pypika.readthedocs.io/en/latest/"&gt;PyPika&lt;/a&gt; are standalone query builders
– no ORM, no connection management, just SQL generation;
they are similar to the Peewee query builder.&lt;/p&gt;
&lt;p&gt;SQLBuilder doesn't seem actively maintained.
Aside from that, I didn't use it because it would make
a potential migration to SQLAlchemy or Peewee more difficult.&lt;/p&gt;
&lt;p&gt;PyPika I discovered while writing this article;
it is actively maintained and has somewhat better documentation.&lt;/p&gt;
&lt;p&gt;&lt;small&gt;(update)&lt;/small&gt;
Another actively maintained query builder is &lt;a class="external" href="https://pypi.org/project/python-sql/"&gt;python-sql&lt;/a&gt;.
It is part of &lt;a class="external" href="https://en.wikipedia.org/wiki/Tryton"&gt;Tryton&lt;/a&gt;, an open-source ERP platform;
it's been around for a while, and will likely continue to be.
I missed this one during my research :)&lt;/p&gt;
&lt;p&gt;&lt;a id="sqlbuilder-mini"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="sqlbuilder-mini-psycopg2-sql"&gt;sqlbuilder.mini, psycopg2.sql&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#sqlbuilder-mini-psycopg2-sql" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;SQLBuilder comes with another, extremely lightweight SQL builder, &lt;a class="external" href="https://sqlbuilder.readthedocs.io/en/latest/#short-manual-for-sqlbuilder-mini"&gt;sqlbuilder.mini&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;As dicussed &lt;a class="anchor" href="#the-first-prototype"&gt;in the beginning&lt;/a&gt;,
I like the overall approach
(and at ~500 lines, it's small enough to vendor),
but it still seems verbose, and the generated SQL isn't very readable.&lt;/p&gt;
&lt;p&gt;&lt;small&gt;(update)&lt;/small&gt;
&lt;a class="external" href="https://www.psycopg.org/docs/sql.html"&gt;psycopg2.sql&lt;/a&gt; has a similar philosophy in how it treats SQL strings.
Unlike my builder, it's &amp;quot;inside-out&amp;quot;
(you append stuff to lists explicitly),
so it's more verbose.
It does support escaping identifiers and placeholders, though;
I didn't really deal with that in any way.&lt;/p&gt;
&lt;p&gt;&lt;a id="jinjasql-sqlpy"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;h3 id="jinjasql-sqlpy-pugsql"&gt;JinjaSQL, SQLpy, PugSQL&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#jinjasql-sqlpy-pugsql" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;These two are interesting because they use templating.&lt;/p&gt;
&lt;p&gt;&lt;a class="external" href="https://github.com/sripathikrishnan/jinjasql"&gt;JinjaSQL&lt;/a&gt; is exactly what you'd expect:
generate SQL from Jinja templates.
I didn't use it because composition
through macros would still be verbose,
and a bit tricky (careful with the comma after that last column).&lt;/p&gt;
&lt;p&gt;&lt;small&gt;(update)&lt;/small&gt;
Template engines like Jinja are useful
if you need to allow &lt;em&gt;end users&lt;/em&gt; to build queries,
since you can &lt;a class="external" href="https://jinja.palletsprojects.com/en/3.0.x/sandbox/"&gt;sandbox&lt;/a&gt; templates
(sandboxing Python is not easy, at least &lt;a class="external" href="https://mail.python.org/pipermail/python-dev/2013-November/130132.html"&gt;not with CPython&lt;/a&gt;).
&lt;a class="external" href="https://github.com/dbt-labs/dbt"&gt;dbt&lt;/a&gt;, brought to my attention by a reader,
seems to be using Jinja in this way.&lt;/p&gt;
&lt;p&gt;&lt;a class="external" href="https://github.com/9fin/sqlpy"&gt;SQLpy&lt;/a&gt; is similar, but different.
You put your named queries in a separate file,
and access them from Python as functions.
Query building happens via named parameters:
if you don't pass a parameter when executing the query,
the lines using that parameter aren't included in the query
(as you'd expect, this comes with a lot of &lt;a class="external" href="https://sqlpy.readthedocs.io/en/latest/readme.html#built-sql"&gt;caveats&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;&lt;small&gt;(update)&lt;/small&gt;
I also re-discovered &lt;a class="external" href="https://github.com/mcfunley/pugsql"&gt;PugSQL&lt;/a&gt;,
which is like SQLpy, but is actively maintained
and doesn't do the magic line disappearing stuff.
Turns out they're both inspired by a pair of Clojure libraries.&lt;/p&gt;
&lt;p&gt;If your SQL doesn't change, but is parametrized,
PugSQL looks like a good lightweight solution.
For me, adding WHERE conditions was a strong requirement.&lt;/p&gt;
&lt;h3 id="pony"&gt;Pony&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#pony" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;I don't think I considered &lt;a class="external" href="https://ponyorm.org/"&gt;Pony&lt;/a&gt; at the time,
but it's worth mentioning:
it has been around since 2012,
is actively maintained, and has commercial support.&lt;/p&gt;
&lt;p&gt;And it can translate &lt;a class="external" href="https://docs.ponyorm.org/queries.html"&gt;generator expressions&lt;/a&gt; like this one into SQL queries:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;select&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;Customer&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;orders&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;price&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For &lt;em&gt;reader&lt;/em&gt; it is definitely overkill.&lt;/p&gt;
&lt;p&gt;It does look really, really interesting, though
(&lt;a class="external" href="http://boringtechnology.club/"&gt;too interesting&lt;/a&gt;, maybe?).&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/own-query-builder&amp;t=Why%20I%20wrote%20my%20own%20SQL%20query%20builder%20%28in%20Python%29"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Why%20I%20wrote%20my%20own%20SQL%20query%20builder%20%28in%20Python%29%20https%3A//death.andgravity.com/own-query-builder"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/own-query-builder&amp;title=Why%20I%20wrote%20my%20own%20SQL%20query%20builder%20%28in%20Python%29"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/own-query-builder"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Why%20I%20wrote%20my%20own%20SQL%20query%20builder%20%28in%20Python%29&amp;url=https%3A//death.andgravity.com/own-query-builder&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;p&gt;&lt;strong&gt;Next: &lt;a class="internal" href="/query-builder-how"&gt;Write an SQL query builder in 150 lines Python!&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;That's one of the benefits of using libraries
that have been around for a while.
Some extensions relevant to my project:
SQLAlchemy has &lt;a class="external" href="https://alembic.sqlalchemy.org/"&gt;Alembic&lt;/a&gt; for migrations (from the same author) and
&lt;a class="external" href="https://github.com/djrobstep/sqlakeyset"&gt;sqlakeyset&lt;/a&gt; for scrolling window queries;
Peewee has &lt;em&gt;a lot&lt;/em&gt; of SQLite-specific functionality. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;They'd also be my first choice for a project with resources and deadlines. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/own-query-builder" rel="alternate"/>
    <summary>This is the third article in a series about writing an SQL query builder in 150 lines of Python. Here, I talk about why I decided to write my own, the alternatives I considered, why I didn't use an existing library, and how I knew it wouldn't become a maintenance burden.</summary>
    <published>2021-06-28T11:35:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/more-arguments">
    <id>https://death.andgravity.com/more-arguments</id>
    <title>When your functions take the same arguments, consider using a class: counter-examples</title>
    <updated>2021-06-18T15:02:00+00:00</updated>
    <content type="html">&lt;p&gt;In &lt;a class="internal" href="/same-arguments"&gt;a previous article&lt;/a&gt;,
I talk about this heuristic for using classes in Python:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;If you have functions that take the same set of arguments, consider using a class.&lt;/strong&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Thing is, &lt;a class="internal" href="/same-arguments#the-heuristic"&gt;heuristics don't always work&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;To make the most out of them, it helps to know what the exceptions are.&lt;/p&gt;
&lt;p&gt;So, let's look at a few real-world examples
where functions taking the same arguments
&lt;strong&gt;don't necessarily make a class&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="counter-example-two-sets-of-arguments"&gt;Counter-example: two sets of arguments&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#counter-example-two-sets-of-arguments" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Consider the following scenario:&lt;/p&gt;
&lt;p&gt;We have a feed reader web application.
It shows a list of feeds and a list of entries (articles),
filtered in various ways.&lt;/p&gt;
&lt;p&gt;Because we want to do the same thing from the command-line,
we pull database-specific logic into functions in a separate module.
The functions take a database connection and other arguments,
query the database, and return the results.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_entry_counts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;search_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_feeds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The main usage pattern is:
at the start of the program, connect to the database;
depending on user input,
repeatedly call the functions with the same connection, but different options.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Taking the heuristic to the extreme, we end up with this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_feed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_read&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_important&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_entry_counts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;search_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_feeds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is not very useful:
every time we change the options, we need to create a new &lt;code&gt;Storage&lt;/code&gt; object
(or worse, have a single one and
&lt;a class="internal" href="/same-arguments#caveat-attribute-changes-are-confusing"&gt;change its attributes&lt;/a&gt;).
Also, &lt;code&gt;get_feeds()&lt;/code&gt; doesn't even use them –
but somehow leaving it out seems just as bad.&lt;/p&gt;
&lt;p&gt;What's missing is a bit of nuance:
there isn't &lt;em&gt;one&lt;/em&gt; set of arguments, there are &lt;em&gt;two&lt;/em&gt;,
and one of them changes more often than the other.&lt;/p&gt;
&lt;p&gt;&lt;a id="data-access-object"&gt;&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Let's take care of the obvious one first.&lt;/p&gt;
&lt;p&gt;The database connection changes least often,
so it makes sense to keep it on the storage,
and pass a storage object around:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_db&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;db&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_entry_counts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;search_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_feeds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The most important benefit of this is that
it &lt;strong&gt;abstracts the database from the code using it&lt;/strong&gt;,
allowing you to have more than one kind of storage.&lt;/p&gt;
&lt;p&gt;Want to store entries as files on disk?
Write a FileStorage class that reads them from there.
Want to test your application with various combinations of made-up entries?
Write a MockStorage class that keeps the entries in a list, in memory.
Whoever calls &lt;code&gt;get_entries()&lt;/code&gt; or &lt;code&gt;search_entries()&lt;/code&gt;
doesn't have to know &lt;em&gt;or care&lt;/em&gt; where the entries are coming from
or how the search is implemented.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;This is the &lt;a class="external" href="https://en.wikipedia.org/wiki/Data_access_object"&gt;data access object&lt;/a&gt; design pattern.
In object-oriented programming terminology,
a DAO provides an abstract interface that
&lt;em&gt;encapsulates&lt;/em&gt; a persistence mechanism.&lt;/p&gt;
&lt;/section&gt;
&lt;hr /&gt;
&lt;p&gt;OK, the above looks just about right to me –
I wouldn't really change anything else.&lt;/p&gt;
&lt;p&gt;Some arguments are still repeating, but it's &lt;em&gt;useful repetition:&lt;/em&gt;
once a user learns to filter entries with one method,
they can do it with any of them.
Also, people use different arguments at different times;
from their perspective, it's not really repetition.&lt;/p&gt;
&lt;p&gt;And anyway, we're already using a class...&lt;/p&gt;
&lt;h2 id="counter-example-data-classes"&gt;Counter-example: data classes&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#counter-example-data-classes" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Let's add more requirements.&lt;/p&gt;
&lt;p&gt;There's more functionality beyond storing things,
and we have multiple users for that as well
(web app, CLI, someone using our code as a library).
So we leave &lt;code&gt;Storage&lt;/code&gt; to do &lt;em&gt;only&lt;/em&gt; storage,
and wrap it in a &lt;code&gt;Reader&lt;/code&gt; object that &lt;em&gt;has&lt;/em&gt; a storage:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_storage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;storage&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_storage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;update_feeds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# calls various storage methods multiple times:&lt;/span&gt;
        &lt;span class="c1"&gt;# get feeds to be retrieved from storage,&lt;/span&gt;
        &lt;span class="c1"&gt;# store new/modified entries&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, the main caller of &lt;code&gt;Storage.get_entries()&lt;/code&gt; is &lt;code&gt;Reader.get_entries()&lt;/code&gt;.
Furthermore, the filter arguments are rarely used directly by storage methods,
most of the time they're passed to helper functions:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_get_entries_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;p&gt;Problem:
When we add a new entry filter option,
we have to change the Reader methods,
the Storage methods, &lt;em&gt;and&lt;/em&gt; the helpers.
And it's likely we'll do so in the future.&lt;/p&gt;
&lt;p&gt;Solution: Group the arguments in &lt;strong&gt;a class that contains only data&lt;/strong&gt;.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;typing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;EntryFilterOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NamedTuple&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Storage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

    &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filter_options&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_get_entries_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;filter_options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;...&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_entry_counts&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filter_options&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;search_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;filter_options&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_feeds&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, regardless of how much they're passed around,
there are only two places where it matters what the options are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;in a Reader method, which builds the EntryFilterOptions object&lt;/li&gt;
&lt;li&gt;where they get used, either a helper or a Storage method&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Note that while we're using the Python class &lt;em&gt;syntax&lt;/em&gt;,
EntryFilterOptions is &lt;em&gt;not a class&lt;/em&gt;
in the traditional object-oriented programming sense,
since it has no behavior.&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;
Sometimes, these are known as &amp;quot;&lt;a class="external" href="https://en.wikipedia.org/wiki/Passive_data_structure"&gt;passive data structures&lt;/a&gt;&amp;quot; or &amp;quot;plain old data&amp;quot;.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;A plain class or a &lt;a class="external" href="https://docs.python.org/3/library/dataclasses.html"&gt;dataclass&lt;/a&gt; would have been a decent choice as well;
why I chose a &lt;a class="external" href="https://docs.python.org/3/library/typing.html#typing.NamedTuple"&gt;named tuple&lt;/a&gt; is a discussion for
&lt;a class="internal" href="/namedtuples#you-want-consumers-that-do-unpacking-to-fail"&gt;another article&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;I used type hints because it's a cheap way of documenting the options,
but you don't have to, &lt;a class="internal" href="/dataclasses"&gt;not even for dataclasses&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;The example above is a simplified version of the code in my feed reader library.
In the real world, &lt;a class="external" href="https://github.com/lemon24/reader/blob/1.18/src/reader/_types.py#L290"&gt;EntryFilterOptions&lt;/a&gt; has more options (with more on the way),
and the &lt;a class="external" href="https://github.com/lemon24/reader/blob/1.18/src/reader/core.py#L880"&gt;Reader&lt;/a&gt; and &lt;a class="external" href="https://github.com/lemon24/reader/blob/1.18/src/reader/_storage.py#L949"&gt;Storage&lt;/a&gt; get_entries() are a bit more complicated.&lt;/p&gt;
&lt;p&gt;Another real-world example of this pattern is &lt;a class="external" href="https://docs.python-requests.org/"&gt;Requests&lt;/a&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;get(), post() and so on end up calling &lt;a class="external" href="https://github.com/psf/requests/blob/v2.25.1/requests/sessions.py#L470"&gt;Session.request()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;which packages the arguments into a &lt;a class="external" href="https://github.com/psf/requests/blob/v2.25.1/requests/models.py#L198"&gt;Request&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;and turns it into a &lt;a class="external" href="https://github.com/psf/requests/blob/v2.25.1/requests/models.py#L272"&gt;PreparedRequest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;which is finally sent by an &lt;a class="external" href="https://github.com/psf/requests/blob/v2.25.1/requests/adapters.py#L394"&gt;HTTPAdapter&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;That's pretty much it for now – hang around for some extra stuff, though ;)&lt;/p&gt;
&lt;p&gt;I hope I managed to add more nuance to the original article,
and that you're now at least a &lt;em&gt;little&lt;/em&gt; bit better equipped to use classes.
Keep in mind that this is more an art than a science,
and &lt;a class="internal" href="/same-arguments#try-it-out"&gt;that you can always change your mind later&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/more-arguments&amp;t=When%20your%20functions%20take%20the%20same%20arguments%2C%20consider%20using%20a%20class%3A%20counter-examples"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=When%20your%20functions%20take%20the%20same%20arguments%2C%20consider%20using%20a%20class%3A%20counter-examples%20https%3A//death.andgravity.com/more-arguments"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/more-arguments&amp;title=When%20your%20functions%20take%20the%20same%20arguments%2C%20consider%20using%20a%20class%3A%20counter-examples"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/more-arguments"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=When%20your%20functions%20take%20the%20same%20arguments%2C%20consider%20using%20a%20class%3A%20counter-examples&amp;url=https%3A//death.andgravity.com/more-arguments&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;





&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        If you&amp;#39;ve made it this far, you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/caching-methods"&gt;
            Caching a lot of methods in Python
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="bonus-other-alternatives"&gt;Bonus: other alternatives&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bonus-other-alternatives" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Still here? Cool!&lt;/p&gt;
&lt;p&gt;Let's look at some of the other options I considered,
and why I didn't go that way.&lt;/p&gt;
&lt;h3 id="why-not-a-dict"&gt;Why not a dict?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-not-a-dict" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Instead of defining a whole new class,
we could've used a dict:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;feed&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;read&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;important&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;But this has a number of drawbacks:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Dicts are not type-checked.
&lt;a class="external" href="https://docs.python.org/3/library/typing.html#typing.TypedDict"&gt;TypedDict&lt;/a&gt; helps, but doesn't prevent using the wrong keys &lt;em&gt;at runtime&lt;/em&gt;.&lt;/li&gt;
&lt;li&gt;Dicts break code completion.
TypedDict may help with smarter tools like PyCharm,
but doesn't in interactive mode or IPython.&lt;/li&gt;
&lt;li&gt;Dicts are &lt;em&gt;mutable&lt;/em&gt;.
For our use case, immutability is a plus:
the options don't have much reason to change,
so it's useful to disallow it.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="why-not-kwargs"&gt;Why not **kwargs?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-not-kwargs" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Why not pass &lt;code&gt;**kwargs&lt;/code&gt; directly to EntryFilterOptions?&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Reader&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_storage&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;kwargs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Because:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;It also breaks code completion.&lt;/li&gt;
&lt;li&gt;It makes the code less self-documenting:
you don't know what arguments &lt;code&gt;get_entries()&lt;/code&gt; takes,
even if you read the source.
Presumably, they're in the docstring,
but not everybody writes one all the time.&lt;/li&gt;
&lt;li&gt;If we introduce another options object (say, for pagination),
we still have to write code to split the kwargs between the two.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="why-not-entryfilteroptions"&gt;Why not EntryFilterOptions?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-not-entryfilteroptions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Why not take an EntryFilterOptions directly, then?&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;reader&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;make_reader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;EntryFilterOptions&lt;/span&gt;
&lt;span class="n"&gt;reader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_reader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;options&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;EntryFilterOptions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;entries&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;reader&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;options&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Because it makes things verbose for the user:
they have to import EntryFilterOptions,
and build and pass one to get_entries() &lt;em&gt;for every call&lt;/em&gt;.
That's not very friendly.&lt;/p&gt;
&lt;p&gt;The Reader and Storage method signatures differ
because they're used differently:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reader methods are mostly called by external users in many ways&lt;/li&gt;
&lt;li&gt;Storage methods are mostly called by internal users (Reader) in a few ways&lt;/li&gt;
&lt;/ul&gt;
&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;Ted Kaminski discusses this distinction in more detail in
&lt;a class="external" href="https://www.tedinski.com/2018/01/23/data-objects-and-being-railroaded-into-misdesign.html"&gt;Data, objects, and how we're railroaded into poor design&lt;/a&gt;. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/more-arguments" rel="alternate"/>
    <summary>In this article, we look at a few real-world examples where functions taking the same arguments don't necessarily make a class, as counter-examples to a heuristic for using classes in Python.</summary>
    <published>2021-06-18T12:05:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/sentinels">
    <id>https://death.andgravity.com/sentinels</id>
    <title>Python sentinel objects, type hints, and PEP 661</title>
    <updated>2021-06-10T11:42:00+00:00</updated>
    <content type="html">&lt;p&gt;&lt;a class="external" href="https://www.python.org/dev/peps/pep-0661/"&gt;PEP 661&lt;/a&gt; &amp;quot;Sentinel Values&amp;quot;
recently brought to attention the &lt;strong&gt;sentinel object&lt;/strong&gt; pattern.&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;While by no means new&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt;,
this time the pattern appears in the context of &lt;strong&gt;&lt;a class="external" href="https://www.python.org/dev/peps/pep-0483/"&gt;typing&lt;/a&gt;&lt;/strong&gt;,
so it's worth taking a look at how the two interact.&lt;/p&gt;
&lt;details class="toc" open&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#what-s-a-sentinel-and-why-do-i-need-one"&gt;What's a sentinel, and why do I need one?&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#real-world-examples"&gt;Real world examples&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#non-private-sentinels"&gt;Non-private sentinels&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#what-s-this-got-to-do-with-typing"&gt;What's this got to do with typing?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#what-s-with-pep-661"&gt;What's with PEP 661?&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#how-does-this-affect-me"&gt;How does this affect me?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#is-this-worth-a-pep"&gt;Is this worth a PEP?&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="what-s-a-sentinel-and-why-do-i-need-one"&gt;What's a sentinel, and why do I need one?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-a-sentinel-and-why-do-i-need-one" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The PEP 661 abstract summarizes it best:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Unique placeholder values, widely known as &amp;quot;sentinel values&amp;quot;, are useful in Python programs for several things, such as default values for function arguments where None is a valid input value.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The simplest use case I can think of is
a function that returns a default value only if &lt;em&gt;explicitly&lt;/em&gt; provided,
otherwise raises an exception.&lt;/p&gt;
&lt;p&gt;The &lt;a class="external" href="https://docs.python.org/3/library/functions.html#next"&gt;next()&lt;/a&gt; built-in function is a good example:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;next(iterator[, default])&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Retrieve the next item from the &lt;em&gt;iterator&lt;/em&gt; by calling its &lt;code&gt;__next__()&lt;/code&gt; method.
If &lt;em&gt;default&lt;/em&gt; is given, it is returned if the iterator is exhausted,
otherwise StopIteration is raised.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Given this definition, let's try to re-implement it.&lt;/p&gt;
&lt;p&gt;next() essentially has two signatures&lt;sup class="footnote-ref" id="fnref-3"&gt;&lt;a href="#fn-3"&gt;3&lt;/a&gt;&lt;/sup&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;next(iterator)&lt;/code&gt; -&amp;gt; item or raise exception&lt;/li&gt;
&lt;li&gt;&lt;code&gt;next(iterator, default)&lt;/code&gt; -&amp;gt; item or default&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are two main ways to write a function that supports both:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;next(*args, **kwargs)&lt;/code&gt;;
you have to extract &lt;em&gt;iterator&lt;/em&gt; and &lt;em&gt;default&lt;/em&gt; from &lt;em&gt;args&lt;/em&gt; and &lt;em&gt;kwargs&lt;/em&gt;,
and raise TypeError if there are too many / too few / unexpected arguments&lt;/li&gt;
&lt;li&gt;&lt;code&gt;next(iterator, default=None)&lt;/code&gt;;
Python checks the arguments, you just need to check if &lt;code&gt;default is None&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To me, the second seems easier to implement than the first.&lt;/p&gt;
&lt;p&gt;But the second version has a problem:
for some users, &lt;code&gt;None&lt;/code&gt; is a valid default –
how can &lt;code&gt;next()&lt;/code&gt; distinguish between
&lt;em&gt;raise-exception-&lt;code&gt;None&lt;/code&gt;&lt;/em&gt; and &lt;em&gt;default-value-&lt;code&gt;None&lt;/code&gt;&lt;/em&gt;?&lt;/p&gt;
&lt;p&gt;In your own code,
you may be able to guarantee &lt;code&gt;None&lt;/code&gt; is never a valid value,
making this a non-issue.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;In a library&lt;/em&gt;, however,
you don't want to restrict users in this way,
since you usually can't foresee all their use cases.
Even if you did choose to restrict valid values like this,
you'd have to document it,
and the users would have to learn about it,
and always remember the exception.&lt;sup class="footnote-ref" id="fnref-4"&gt;&lt;a href="#fn-4"&gt;4&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;Here's where a private, &lt;em&gt;internal-use only&lt;/em&gt; sentinel object helps:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;1&lt;/span&gt;
&lt;span class="normal"&gt;2&lt;/span&gt;
&lt;span class="normal"&gt;3&lt;/span&gt;
&lt;span class="normal"&gt;4&lt;/span&gt;
&lt;span class="normal"&gt;5&lt;/span&gt;
&lt;span class="normal"&gt;6&lt;/span&gt;
&lt;span class="normal"&gt;7&lt;/span&gt;
&lt;span class="normal"&gt;8&lt;/span&gt;
&lt;span class="normal"&gt;9&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;_missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_missing&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__next__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;StopIteration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;_missing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;details&gt;
&lt;summary&gt;Example output:&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;1&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;None&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;StopIteration&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;Now, next() knows that &lt;code&gt;default=_missing&lt;/code&gt; means &lt;em&gt;raise exception&lt;/em&gt;,
and &lt;code&gt;default=None&lt;/code&gt; is just a regular default value to be returned.&lt;/p&gt;
&lt;p&gt;You can think of &lt;em&gt;_missing&lt;/em&gt; as of &lt;em&gt;another None&lt;/em&gt;,
for when the actual None is already taken
– a &amp;quot;higher-order&amp;quot; None.
Because it's private to the module,
users can never (accidentally) use it as a default value,
and never have know about it.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;For a more in-depth explanation of sentinel objects and related patterns,
see &lt;a class="external" href="https://python-patterns.guide/python/sentinel-object/"&gt;The Sentinel Object Pattern&lt;/a&gt;
by Brandon Rhodes.&lt;/p&gt;
&lt;/section&gt;
&lt;h3 id="real-world-examples"&gt;Real world examples&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#real-world-examples" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The real next() doesn't actually use sentinel values,
because it's &lt;a class="external" href="https://github.com/python/cpython/blob/5571cabf1b3385087aba2c7c10289bba77494e08/Python/bltinmodule.c#L1446-L1480"&gt;implemented in C&lt;/a&gt;, and things are sometimes different there.&lt;/p&gt;
&lt;p&gt;But there are plenty of examples in pure-Python code:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;The dataclasses module &lt;a class="external" href="https://github.com/python/cpython/blob/c8353239eda0d05f7facd1a19acc2b836a057807/Lib/dataclasses.py#L158-L170"&gt;has two&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The docs even explain what a sentinel is:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[...] the &lt;em&gt;MISSING&lt;/em&gt; value is a sentinel object used to detect if the &lt;em&gt;default&lt;/em&gt; and &lt;em&gt;default_factory&lt;/em&gt; parameters are provided. This sentinel is used because &lt;em&gt;None&lt;/em&gt; is a valid value for &lt;em&gt;default&lt;/em&gt;. No code should directly use the &lt;em&gt;MISSING&lt;/em&gt; value.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;(The other one is used in the &lt;em&gt;__init__&lt;/em&gt; of the generated classes
to show a default value comes from a factory.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;attrs &lt;a class="external" href="https://github.com/python-attrs/attrs/blob/9709dd82e1cc0f5b783f4127e87498dfdd6a224a/src/attr/_make.py#L59-L92"&gt;also has two&lt;/a&gt;.
One of them (analogous to dataclasses.MISSING)
is even included in the
&lt;a class="external" href="https://www.attrs.org/en/stable/api.html#attr.NOTHING"&gt;API documentation&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Werkzeug &lt;a class="external" href="https://github.com/pallets/werkzeug/blob/eff04478a83619b4d7f15e6eee16a99bd80ed879/src/werkzeug/_internal.py#L51-L59"&gt;has one&lt;/a&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I &lt;a class="external" href="https://github.com/lemon24/reader/blob/1.18/src/reader/types.py#L551-L557"&gt;have one&lt;/a&gt;
in my feed reader library (originally stolen from Werkzeug).
I use it for methods like &lt;code&gt;get_feed(feed[, default])&lt;/code&gt;,
which either raises FeedNotFoundError or returns &lt;em&gt;default&lt;/em&gt;.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="non-private-sentinels"&gt;Non-private sentinels&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#non-private-sentinels" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;I mentioned before sentinels are private;
that's not always the case.&lt;/p&gt;
&lt;p&gt;If the sentinel is the default argument of a public method or function,
it may be a good idea to expose / document it,
to facilitate inheritance and function wrappers.&lt;sup class="footnote-ref" id="fnref-5"&gt;&lt;a href="#fn-5"&gt;5&lt;/a&gt;&lt;/sup&gt;
&lt;a class="external" href="https://www.attrs.org/en/stable/api.html#attr.NOTHING"&gt;attrs&lt;/a&gt; is a good example of this.&lt;/p&gt;
&lt;p&gt;(If you don't expose it,
people can still extend your code
by using &lt;em&gt;their own&lt;/em&gt; sentinel,
and then calling either form of your function.)&lt;/p&gt;
&lt;h2 id="what-s-this-got-to-do-with-typing"&gt;What's this got to do with typing?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-this-got-to-do-with-typing" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Let's try to add type hints to our
&lt;a class="anchor" href="#what-s-a-sentinel-and-why-do-i-need-one"&gt;hand-rolled next()&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;
&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;
&lt;span class="normal"&gt;52&lt;/span&gt;
&lt;span class="normal"&gt;53&lt;/span&gt;
&lt;span class="normal"&gt;54&lt;/span&gt;
&lt;span class="normal"&gt;55&lt;/span&gt;
&lt;span class="normal"&gt;56&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;typing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;overload&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TypeVar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;

&lt;span class="n"&gt;T&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TypeVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;T&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;U&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TypeVar&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;U&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="c1"&gt;# We define MissingType in one of two ways:&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;MissingType&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;pass&lt;/span&gt;
&lt;span class="c1"&gt;# MissingType = object&lt;/span&gt;

&lt;span class="c1"&gt;# The second one is equivalent to the original&lt;/span&gt;
&lt;span class="c1"&gt;# `_missing = object()`, but the alias allows us&lt;/span&gt;
&lt;span class="c1"&gt;# to keep the same type annotations.&lt;/span&gt;

&lt;span class="n"&gt;_missing&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;MissingType&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;


&lt;span class="c1"&gt;# As mentioned before, next() is actually two functions;&lt;/span&gt;
&lt;span class="c1"&gt;# typing.overload allows us to express this.&lt;/span&gt;
&lt;span class="c1"&gt;#&lt;/span&gt;
&lt;span class="c1"&gt;# One that returns an item or raises an exception:&lt;/span&gt;

&lt;span class="nd"&gt;@overload&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# ... and one that takes a default value (of some type U),&lt;/span&gt;
&lt;span class="c1"&gt;# and returns either an item, or that default value&lt;/span&gt;
&lt;span class="c1"&gt;# (of the *same* type U):&lt;/span&gt;

&lt;span class="nd"&gt;@overload&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;

&lt;span class="c1"&gt;# The implementation takes all the arguments,&lt;/span&gt;
&lt;span class="c1"&gt;# and returns a union of all the types:&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Iterator&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;MissingType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;_missing&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Union&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;T&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;U&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;iterator&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__next__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;StopIteration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

        &lt;span class="c1"&gt;# &amp;quot;if default is _missing&amp;quot; is idiomatic here,&lt;/span&gt;
        &lt;span class="c1"&gt;# but Mypy doesn&amp;#39;t understand it&lt;/span&gt;
        &lt;span class="c1"&gt;# (&amp;quot;var is None&amp;quot; is a special case).&lt;/span&gt;
        &lt;span class="c1"&gt;# It does understand isinstance(), though:&lt;/span&gt;
        &lt;span class="c1"&gt;# https://mypy.readthedocs.io/en/stable/casts.html#casts&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;MissingType&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="c1"&gt;# If MissingType is `object`, this is always true,&lt;/span&gt;
            &lt;span class="c1"&gt;# since all types are a subclass of `object`.&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;isinstance()&lt;/code&gt; thing at the end is why a plain &lt;code&gt;object()&lt;/code&gt; sentinel
doesn't work – you can't (easily) get Mypy to treat your own
&amp;quot;constants&amp;quot; the way it does a &lt;a class="external" href="https://docs.python.org/3/library/constants.html"&gt;built-in constant&lt;/a&gt; like None,
and the sentinel doesn't have a &lt;em&gt;distinct&lt;/em&gt; type.&lt;/p&gt;
&lt;p&gt;Also, if you use the &lt;code&gt;MissingType = object&lt;/code&gt; version, Mypy complains:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;next.py:37: error: Overloaded function implementation cannot produce return type of signature 2
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;If you're wondering if the good version actually worked,
here's what Mypy says:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;59&lt;/span&gt;
&lt;span class="normal"&gt;60&lt;/span&gt;
&lt;span class="normal"&gt;61&lt;/span&gt;
&lt;span class="normal"&gt;62&lt;/span&gt;
&lt;span class="normal"&gt;63&lt;/span&gt;
&lt;span class="normal"&gt;64&lt;/span&gt;
&lt;span class="normal"&gt;65&lt;/span&gt;
&lt;span class="normal"&gt;66&lt;/span&gt;
&lt;span class="normal"&gt;67&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;it&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;iter&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;reveal_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# next.py:62: note: Revealed type is &amp;#39;builtins.int*&amp;#39;&lt;/span&gt;

&lt;span class="n"&gt;two&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;next&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;a string&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;reveal_type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;two&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# next.py:66: note: Revealed type is &amp;#39;Union[builtins.int*, builtins.str*]&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;h2 id="what-s-with-pep-661"&gt;What's with PEP 661?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-s-with-pep-661" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;There are many sentinel implementations out there;
there are &lt;em&gt;15 different ones&lt;/em&gt; &lt;a class="external" href="https://mail.python.org/archives/list/python-dev@python.org/message/JBYXQH3NV3YBF7P2HLHB5CD6V3GVTY55/"&gt;in the standard library alone&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Many of them have at least one of these issues:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;non-descriptive / too long repr() (e.g. &lt;code&gt;&amp;lt;object object at 0x7f99a355fc20&amp;gt;&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;don't pickle correctly (e.g. after unpickling you get a different, new object)&lt;/li&gt;
&lt;li&gt;&lt;a class="anchor" href="#what-s-this-got-to-do-with-typing"&gt;don't work well with typing&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Thus, &lt;a class="external" href="https://www.python.org/dev/peps/pep-0661/"&gt;PEP 661&lt;/a&gt;
&amp;quot;suggests adding a utility for defining sentinel values,
to be used in the stdlib and made publicly available as part of the stdlib&amp;quot;.
It looks like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;NotGiven&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sentinel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;NotGiven&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;NotGiven&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;NotGiven&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;MISSING&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sentinel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;MISSING&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;repr&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;mymodule.MISSING&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;MISSING&lt;/span&gt;
&lt;span class="go"&gt;mymodule.MISSING&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This utility would address all the known issues,
saving developers
(mostly, stdlib and third party library authors)
from reinventing the wheel (again).&lt;/p&gt;
&lt;h3 id="how-does-this-affect-me"&gt;How does this affect me?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#how-does-this-affect-me" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Not at all.&lt;/p&gt;
&lt;p&gt;If the PEP gets accepted and implemented,
you'll be able to create an issue-free sentinel with one line of code.&lt;/p&gt;
&lt;p&gt;Of course, you can keep using your own sentinel objects if you want to;
the PEP doesn't even propose to change
the &lt;em&gt;existing&lt;/em&gt; sentinels in the standard library.&lt;/p&gt;
&lt;h3 id="is-this-worth-a-pep"&gt;Is this worth a PEP?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#is-this-worth-a-pep" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;PEPs exist to support discussions in cases where
the &amp;quot;correct&amp;quot; way to go isn't obvious,
consensus or coordination are required,
or the changes have a big blast radius.
A lot of PEPs get &lt;a class="external" href="https://www.python.org/dev/peps/#abandoned-withdrawn-and-rejected-peps"&gt;abandoned or rejected&lt;/a&gt;
(that's fine, it's how the process is supposed to work).&lt;/p&gt;
&lt;p&gt;PEP 661 seems to fall under the &amp;quot;requires consensus&amp;quot; category;
it follows a &lt;a class="external" href="https://discuss.python.org/t/sentinel-values-in-the-stdlib/8810"&gt;community poll&lt;/a&gt;
where although the top pick was &amp;quot;do nothing&amp;quot;,
most voters went for &amp;quot;do &lt;em&gt;something&lt;/em&gt;&amp;quot;
(but with no clear agreement on what that should be).&lt;/p&gt;
&lt;p&gt;The poll introduction states:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;This is a minor detail, so ISTM most important that we reach a reasonable decision quickly, even if that decision is that nothing should be done.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It's worth remembering that &lt;em&gt;doing nothing&lt;/em&gt; is always an option. :)&lt;/p&gt;
&lt;p&gt;If you're into this kind of thing,
I highly recommend going through the &lt;a class="external" href="https://discuss.python.org/t/sentinel-values-in-the-stdlib/8810"&gt;poll thread&lt;/a&gt;
and the (ongoing) &lt;a class="external" href="https://discuss.python.org/t/pep-661-sentinel-values/9126"&gt;PEP discussion thread&lt;/a&gt; –
usually, these discussions are API design master classes.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's all I have for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/sentinels&amp;t=Python%20sentinel%20objects%2C%20type%20hints%2C%20and%20PEP%20661"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Python%20sentinel%20objects%2C%20type%20hints%2C%20and%20PEP%20661%20https%3A//death.andgravity.com/sentinels"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/sentinels&amp;title=Python%20sentinel%20objects%2C%20type%20hints%2C%20and%20PEP%20661"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/sentinels"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Python%20sentinel%20objects%2C%20type%20hints%2C%20and%20PEP%20661&amp;url=https%3A//death.andgravity.com/sentinels&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;The PEP is still in draft status as of 2021-06-10. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;Here's a &lt;a class="external" href="https://web.archive.org/web/20201112004749/http://effbot.org/zone/default-values.htm"&gt;2008 article&lt;/a&gt; about it. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-3"&gt;&lt;p&gt;While Python &lt;a class="external" href="https://www.reddit.com/r/Python/comments/nvt59p/why_doesnt_python_support_function_overloading/h15ayin"&gt;doesn't support overloading&lt;/a&gt;,
sometimes it's useful to think about functions in this way. &lt;a href="#fnref-3" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-4"&gt;&lt;p&gt;The same applies to using some other &amp;quot;common&amp;quot; value,
for example, a &lt;code&gt;&amp;quot;&amp;lt;NotGiven&amp;gt;&amp;quot;&lt;/code&gt; string sentinel.&lt;/p&gt;
&lt;p&gt;For immutable values like strings, it's probably worse.
Because of optimizations like &lt;a class="external" href="https://docs.python.org/3/library/sys.html?highlight=intern#sys.intern"&gt;interning&lt;/a&gt;,
strings constructed at different times
may actually result in the same object.
The &lt;a class="external" href="https://docs.python.org/3/reference/datamodel.html#objects-values-and-types"&gt;data model&lt;/a&gt; &lt;em&gt;specifically&lt;/em&gt; allows for this to happen (emphasis mine):&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Types affect almost all aspects of object behavior.
Even the importance of object identity is affected in some sense:
&lt;strong&gt;for immutable types, operations that compute new values may actually
return a reference to any existing object with the same type and value&lt;/strong&gt;,
while for mutable objects this is not allowed.&lt;/p&gt;
&lt;/blockquote&gt; &lt;a href="#fnref-4" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/li&gt;
&lt;li id="fn-5"&gt;&lt;p&gt;Thanks to &lt;a class="external" href="https://www.reddit.com/r/Python/comments/ntipjq/x/h0u7k35"&gt;u/energybased&lt;/a&gt; for reminding me of this! &lt;a href="#fnref-5" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/sentinels" rel="alternate"/>
    <summary>PEP 661 proposes adding a utility for defining sentinel values in the Python standard library. In this article, you'll get a PEP 661 summary, learn what sentinel objects are (with real-world examples), how to use them with type hints, and a bit about why PEPs exist in the first place.</summary>
    <published>2021-06-10T07:50:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/same-arguments">
    <id>https://death.andgravity.com/same-arguments</id>
    <title>When to use classes in Python? When your functions take the same arguments</title>
    <updated>2021-06-04T12:35:00+00:00</updated>
    <content type="html">&lt;p&gt;Are you having trouble figuring out when to use classes or how to organize them?&lt;/p&gt;
&lt;p&gt;Have you repeatedly searched for &amp;quot;when to use classes in Python&amp;quot;,
read all the articles and watched all the talks,
and &lt;em&gt;still&lt;/em&gt;  don't know whether you should be using classes in any given situation?&lt;/p&gt;
&lt;p&gt;Have you read discussions about it that for all you know &lt;em&gt;may be right&lt;/em&gt;,
but they're &lt;em&gt;so academic&lt;/em&gt; you can't parse the jargon?&lt;/p&gt;
&lt;p&gt;Have you read articles that all treat the &amp;quot;obvious&amp;quot; cases,
leaving you with no clear answer when you try to apply them to your own code?&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;My experience is that, &lt;strong&gt;unfortunately&lt;/strong&gt;,
the best way to learn this &lt;em&gt;is&lt;/em&gt; to &lt;a class="internal" href="/stdlib"&gt;look at lots of examples&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Most guidelines tend to either be too vague &lt;em&gt;if you don't already know enough&lt;/em&gt; about the subject,
or too specific and saying things you already know.&lt;/p&gt;
&lt;p&gt;This is one of those things that once you get it seems obvious and intuitive,
&lt;em&gt;but it's not&lt;/em&gt;, and is quite difficult to explain properly.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;So, instead of prescribing a general approach,
let's look at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;one specific case&lt;/strong&gt; where you may want to use classes&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;examples from real-world code&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;some considerations you should keep in mind&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="the-heuristic"&gt;The heuristic&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-heuristic" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;If you have functions that take the same set of arguments, consider using a class.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;That's it.&lt;/p&gt;
&lt;p&gt;In its most basic form,
a class is when you group data with functions that operate on that data;
it doesn't have to represent a real (&amp;quot;business&amp;quot;) object,
it can be an &lt;em&gt;abstract object&lt;/em&gt; that exists only
to make things easier to use / understand.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;As Wikipedia &lt;a class="external" href="https://simple.wikipedia.org/wiki/Heuristic"&gt;puts it&lt;/a&gt;,
&amp;quot;A &lt;strong&gt;heuristic&lt;/strong&gt; is a practical way to solve a problem.
It is &lt;em&gt;better than chance&lt;/em&gt;, but &lt;em&gt;does not always work&lt;/em&gt;.
A person develops a heuristic by using
intelligence, experience, and common sense.&amp;quot;&lt;/p&gt;
&lt;p&gt;So, this is &lt;strong&gt;not&lt;/strong&gt; the correct thing to do &lt;strong&gt;all the time&lt;/strong&gt;,
or even &lt;em&gt;most&lt;/em&gt; of the time.&lt;/p&gt;
&lt;p&gt;Instead, I hope that this and &lt;em&gt;other&lt;/em&gt; heuristics
can help &lt;strong&gt;build the right intuition&lt;/strong&gt;
for people on their way from
&amp;quot;I know the class syntax, now what?&amp;quot; to
&amp;quot;proper&amp;quot; object-oriented design.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="example-highlightedstring"&gt;Example: HighlightedString&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#example-highlightedstring" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;My feed reader library supports &lt;a class="external" href="https://reader.readthedocs.io/en/latest/guide.html#full-text-search"&gt;searching&lt;/a&gt; articles.
The results include article snippets,
and which parts of the snippet actually matched.&lt;/p&gt;
&lt;p&gt;To highlight the matches (say, on a web page),
we write a function that takes a string and a list of slices&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;,
and adds before/after markers to the parts inside the slices:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;water on mars&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;highlights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;apply_highlights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;highlights&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;lt;b&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;lt;/b&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;water on &amp;lt;b&amp;gt;mars&amp;lt;/b&amp;gt;&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;While writing it,
we pull part of the logic into a helper
that splits the string such that highlights always have odd indices.
We don't &lt;em&gt;have&lt;/em&gt; to, but it's easier to reason about problems one at a time.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;split_highlights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;highlights&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;water on &amp;#39;, &amp;#39;mars&amp;#39;, &amp;#39;&amp;#39;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To make things easier,
we only allow non-overlapping slices
with positive start/stop and no step.
We pull this logic into another function
that raises an exception for bad slices.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;validate_highlights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;highlights&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# no exception&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;validate_highlights&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="nb"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;ValueError&lt;/span&gt;: &lt;span class="n"&gt;highlights must not overlap: slice(6, 10, None), slice(9, 13, None)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Quiz: Which function should call &lt;code&gt;validate_highlights()&lt;/code&gt;? Both? The user?&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Instead of separate functions, we can write a &lt;a class="external" href="https://github.com/lemon24/reader/blob/8e46f5ddd9b8bc4c8c7c346c68f8abcd2d6ab441/src/reader/types.py#L280-L434"&gt;HighlightedString&lt;/a&gt; class with:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;value&lt;/code&gt; and &lt;code&gt;highlights&lt;/code&gt; as attributes&lt;/li&gt;
&lt;li&gt;&lt;code&gt;apply()&lt;/code&gt; and &lt;code&gt;split()&lt;/code&gt; as methods&lt;/li&gt;
&lt;li&gt;the validation happening in &lt;code&gt;__init__&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;HighlightedString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;water on mars&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;water on mars&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;highlights&lt;/span&gt;
&lt;span class="go"&gt;(slice(9, 13, None),)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;b&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;lt;/b&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;water on &amp;lt;b&amp;gt;mars&amp;lt;/b&amp;gt;&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;split&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="go"&gt;[&amp;#39;water on &amp;#39;, &amp;#39;mars&amp;#39;, &amp;#39;&amp;#39;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;HighlightedString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;water on mars&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;ValueError&lt;/span&gt;: &lt;span class="n"&gt;invalid highlight: start must be not be greater than stop: slice(13, 9, None)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This essentially bundles &lt;em&gt;data&lt;/em&gt; and &lt;em&gt;behavior&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;You may ask:
I can do any number of things with a string and some slices,
&lt;strong&gt;why this behavior&lt;/strong&gt; specifically?
Because, in this context,
&lt;strong&gt;this behavior is generally useful&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;Besides being shorter to use, a class:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;shows intent&lt;/strong&gt;:
this isn't just a string and some slices,
it's a &lt;em&gt;highlighted string&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;makes it easier to discover&lt;/strong&gt; what actions are possible
(&lt;a class="external" href="https://docs.python.org/3/library/functions.html#help"&gt;help()&lt;/a&gt;, code completion)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;makes code cleaner&lt;/strong&gt;;
&lt;code&gt;__init__&lt;/code&gt; validation ensures invalid objects &lt;em&gt;cannot&lt;/em&gt; exist;
thus, the methods don't have to validate anything themselves&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="caveat-attribute-changes-are-confusing"&gt;Caveat: attribute changes are confusing&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#caveat-attribute-changes-are-confusing" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Let's say we pass a highlighted string to a function
that writes the results in a text file,
and after that we do some other stuff with it.&lt;/p&gt;
&lt;p&gt;What would you think if this happened?&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;b&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;lt;/b&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;water on &amp;lt;b&amp;gt;mars&amp;lt;/b&amp;gt;&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;render_results_page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;output.txt&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;titles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;apply&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;b&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;lt;/b&amp;gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;&amp;lt;b&amp;gt;water&amp;lt;/b&amp;gt; on mars&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You may think it's quite unexpected; I know I would.
Either intentionally or by mistake,
&lt;code&gt;render_results_page()&lt;/code&gt; seems to have changed our highlights,
when it was supposed to just render the results.&lt;/p&gt;
&lt;p&gt;That's OK, mistakes happen.
But how can we prevent it from happening in the future?&lt;/p&gt;
&lt;h3 id="solution-make-the-class-immutable"&gt;Solution: make the class immutable&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#solution-make-the-class-immutable" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Well, in the real implementation, &lt;em&gt;this mistake can't happen&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;HighlightedString is a &lt;a class="external" href="https://docs.python.org/3/library/dataclasses.html#frozen-instances"&gt;frozen dataclass&lt;/a&gt;,
so its attributes are read-only;
also, &lt;code&gt;highlights&lt;/code&gt; is stored as a &lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#tuple"&gt;tuple&lt;/a&gt;,
which is immutable as well:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;highlights&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;dataclasses.FrozenInstanceError&lt;/span&gt;: &lt;span class="n"&gt;cannot assign to field &amp;#39;highlights&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;string&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;highlights&lt;/span&gt;&lt;span class="p"&gt;[:]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;&amp;#39;tuple&amp;#39; object does not support item assignment&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can find this pattern in &lt;a class="external" href="https://werkzeug.palletsprojects.com/en/2.0.x/datastructures/"&gt;werkzeug.datastructures&lt;/a&gt;,
which contains HTTP-flavored subclasses of common Python objects.
For example, &lt;a class="external" href="https://werkzeug.palletsprojects.com/en/2.0.x/datastructures/#werkzeug.datastructures.Accept"&gt;Accept&lt;/a&gt;&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt; is an immutable &lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#list"&gt;list&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Accept&lt;/span&gt;&lt;span class="p"&gt;([(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;image/png&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)])&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="go"&gt;(&amp;#39;image/png&amp;#39;, 1)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;accept&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;image/gif&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c"&gt;...&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;&amp;#39;Accept&amp;#39; objects are immutable&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="try-it-out"&gt;Try it out&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#try-it-out" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;If you're doing something and you think you need a class,
do it and see how it looks.
If you think it's better, keep it,
otherwise, revert the change.
You can always switch in either direction later.&lt;/p&gt;
&lt;p&gt;If you got it right the first time, great!
If not, &lt;strong&gt;by having to fix it you'll learn something&lt;/strong&gt;,
and next time you'll know better.&lt;/p&gt;
&lt;p&gt;Also, don't beat yourself up.&lt;/p&gt;
&lt;p&gt;Sure, there are nice libraries out there
that use classes in &lt;em&gt;just the right way&lt;/em&gt;,
after spending lots of time to find the right abstraction.
But &lt;strong&gt;abstraction is difficult and time consuming&lt;/strong&gt;,
and in everyday code good enough is just that – good enough –
you don't need to go to the extreme.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Update&lt;/strong&gt;:
I wrote an &lt;a class="internal" href="/more-arguments"&gt;article about exceptions to this heuristic&lt;/a&gt;
(that is, when functions with the same arguments
don't necessarily make a class).&lt;/p&gt;
&lt;/section&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/same-arguments&amp;t=When%20to%20use%20classes%20in%20Python%3F%20When%20your%20functions%20take%20the%20same%20arguments"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=When%20to%20use%20classes%20in%20Python%3F%20When%20your%20functions%20take%20the%20same%20arguments%20https%3A//death.andgravity.com/same-arguments"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/same-arguments&amp;title=When%20to%20use%20classes%20in%20Python%3F%20When%20your%20functions%20take%20the%20same%20arguments"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/same-arguments"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=When%20to%20use%20classes%20in%20Python%3F%20When%20your%20functions%20take%20the%20same%20arguments&amp;url=https%3A//death.andgravity.com/same-arguments&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;





&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        If you&amp;#39;ve made it this far, you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/same-functions"&gt;
            When to use classes in Python? When you repeat similar sets of functions
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;A &lt;a class="external" href="https://docs.python.org/3/library/functions.html#slice"&gt;slice&lt;/a&gt; is an object Python uses internally
for the extended indexing syntax;
&lt;code&gt;thing[9:13]&lt;/code&gt; and &lt;code&gt;thing[slice(9, 13)]&lt;/code&gt; are equivalent. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;You may have used Accept yourself:
the &lt;a class="external" href="https://flask.palletsprojects.com/en/2.0.x/api/#flask.Request.accept_encodings"&gt;&lt;code&gt;request.accept_*&lt;/code&gt;&lt;/a&gt; attributes
on Flask's &lt;a class="external" href="https://flask.palletsprojects.com/en/2.0.x/api/#flask.request"&gt;request&lt;/a&gt; global are all Accept instances. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/same-arguments" rel="alternate"/>
    <summary>In this article, we look at a heuristic for using classes in Python, with examples from real-world code, and some things to keep in mind.</summary>
    <published>2021-05-27T09:55:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/query-builder-why">
    <id>https://death.andgravity.com/query-builder-why</id>
    <title>Why use an SQL query builder in the first place?</title>
    <updated>2021-05-20T12:57:00+00:00</updated>
    <content type="html">&lt;p&gt;&lt;strong&gt;&lt;a class="internal" href="/query-builder"&gt;Previously&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;This is the second article &lt;a class="internal" href="/query-builder"&gt;in a series&lt;/a&gt; about
writing an SQL query builder in 150 lines of Python,
&lt;strong&gt;why I wrote it&lt;/strong&gt;, &lt;strong&gt;how I thought about it&lt;/strong&gt;, and the decisions I had to make.&lt;/p&gt;
&lt;p&gt;Today, we'll talk about &lt;strong&gt;why I needed a query builder&lt;/strong&gt; in the first place,
and how it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="anchor" href="#preventing-combinatorial-explosion"&gt;keeps down the number of queries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="anchor" href="#separation-of-concerns"&gt;leads to cleaner code and cleaner SQL&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="anchor" href="#composition-and-reuse"&gt;enables composition and reuse&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="anchor" href="#introspection"&gt;prevents needless repetition&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="anchor" href="#abstraction"&gt;provides a base for higher level behavior&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The examples below are &lt;em&gt;actual use cases&lt;/em&gt; I had for my feed &lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt; library.
In practice, they overlap a bit,
since they're different aspects of the same problem.&lt;/p&gt;
&lt;section class="admonition attention"&gt;
&lt;p class="admonition-title"&gt;Attention&lt;/p&gt;
&lt;p&gt;The query builder shown below is not directly comparable with that of an ORM.
Instead, it is an alternative to building &lt;em&gt;plain SQL&lt;/em&gt; strings by hand.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The caveats that apply to plain SQL apply to it as well:&lt;/strong&gt;
Using user-supplied values directly in an SQL query
exposes you to &lt;a class="external" href="https://en.wikipedia.org/wiki/SQL_injection"&gt;SQL injection&lt;/a&gt; attacks.
Instead, use &lt;a class="external" href="https://en.wikipedia.org/wiki/SQL_injection#Parameterized_statements"&gt;parametrized queries&lt;/a&gt; whenever possible,
and &lt;a class="external" href="https://en.wikipedia.org/wiki/SQL_injection#Escaping"&gt;escaping&lt;/a&gt; only as a last resort.&lt;/p&gt;
&lt;/section&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;Some of the problems I talk about in this article
can be solved with &lt;a class="external" href="https://en.wikipedia.org/wiki/Stored_procedure"&gt;stored procedures&lt;/a&gt;.
Regardless of &lt;a class="external" href="https://wiki.c2.com/?StoredProceduresAreEvil"&gt;what people think of them&lt;/a&gt;,
they are a non-choice for me,
since SQLite (&lt;a class="internal" href="/own-query-builder#requirements-and-existing-libraries"&gt;my target database&lt;/a&gt;) does not have stored procedures.&lt;/p&gt;
&lt;/section&gt;
&lt;details class="toc"&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#the-problem"&gt;The problem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#preventing-combinatorial-explosion"&gt;Preventing combinatorial explosion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#separation-of-concerns"&gt;Separation of concerns&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#composition-and-reuse"&gt;Composition and reuse&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#intermission-scrolling-window-queries"&gt;Intermission: scrolling window queries&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#introspection"&gt;Introspection&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#abstraction"&gt;Abstraction&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="the-problem"&gt;The problem&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-problem" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;You have a method that retrieves all or some of the entries from the database:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;feed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Tuple&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]],&lt;/span&gt;
    &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;has_enclosures&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Literal&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;recent&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;random&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Iterable&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;In the end, the method queries the &lt;code&gt;entries&lt;/code&gt; table.
The query changes depending on the arguments:
different columns are selected,
different WHERE expressions and ordering terms are used.&lt;/p&gt;
&lt;p&gt;The simplest solution in terms of code is to have one query per variation;
if the method started with no arguments and they were added over time,
this may be a natural thing to do (initially).&lt;/p&gt;
&lt;p&gt;Let's count the queries you'd need &lt;code&gt;get_entries()&lt;/code&gt; above:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;all queries are &lt;a class="external" href="https://docs.python.org/3/library/sqlite3.html#sqlite3-placeholders"&gt;parametrized&lt;/a&gt;,
so the query doesn't change for different values
(e.g. if &lt;code&gt;feed&lt;/code&gt; is not None, we add &lt;code&gt;WHERE feed = :feed&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;optional arguments are either &lt;em&gt;present&lt;/em&gt; or &lt;em&gt;not present&lt;/em&gt; (2 queries)&lt;ul&gt;
&lt;li&gt;to make queries more readable,
for optional boolean arguments I emit 3 queries:
one for True (&lt;code&gt;condition&lt;/code&gt;), one for False (&lt;code&gt;NOT condition&lt;/code&gt;),
and one for None (nothing added);
however, since it can be done with only 2 queries, we'll count 2&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;code&gt;sort&lt;/code&gt; changes the query in more complicated ways,
but it currently has 2 values, so we'll count it as 2 queries
(additional sorts &lt;em&gt;can&lt;/em&gt; be added in the future, though)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;So, there's 6 parameters with 2 variations each: 2&lt;sup&gt;6&lt;/sup&gt; = &lt;strong&gt;64 variations&lt;/strong&gt;!&lt;/p&gt;
&lt;p&gt;I'm not sure having 64 different queries is such a good idea... 😕&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;The &lt;a class="external" href="https://reader.readthedocs.io/en/latest/api.html#reader.Reader.get_entries"&gt;real method&lt;/a&gt; has one parameter that can take an
&lt;em&gt;arbitrary number of tags&lt;/em&gt; in a bunch of different formats
(&lt;code&gt;True&lt;/code&gt;, &lt;code&gt;['one']&lt;/code&gt;, &lt;code&gt;[['one'], ['two']]&lt;/code&gt;, &lt;code&gt;[['one', 'two']]&lt;/code&gt;, ...).
I don't know how to count that, so I left it out;
even without it, the conclusion is the same.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="preventing-combinatorial-explosion"&gt;Preventing combinatorial explosion&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#preventing-combinatorial-explosion" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;OK, what now?&lt;/p&gt;
&lt;p&gt;The next easiest thing is to build the query by concatenating strings.
To keep things short, we'll only do &lt;code&gt;read&lt;/code&gt; and &lt;code&gt;important&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;make_entries_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;where_snippets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;where_snippets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;NOT&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; entries.read&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;where_snippets&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;NOT&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; entries.important&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;where_snippets&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;where_keyword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;WHERE&amp;#39;&lt;/span&gt;
        &lt;span class="n"&gt;where_snippet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39; AND&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;            &amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;where_snippets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;where_keyword&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;
        &lt;span class="n"&gt;where_snippet&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;        SELECT&lt;/span&gt;
&lt;span class="s2"&gt;            entries.id,&lt;/span&gt;
&lt;span class="s2"&gt;            entries.title&lt;/span&gt;
&lt;span class="s2"&gt;        FROM entries&lt;/span&gt;
&lt;span class="s2"&gt;        &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;where_keyword&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;            &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;where_snippet&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;make_entries_query(False, True)&lt;/code&gt; outputs:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="SQL"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;
&lt;span class="w"&gt;             &lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;important&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Not bad...&lt;/p&gt;
&lt;h2 id="separation-of-concerns"&gt;Separation of concerns&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#separation-of-concerns" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;OK, it is a bit verbose,
especially as we add more snippets in other places.
Don't think so? Here's the full &lt;a class="external" href="https://github.com/lemon24/reader/blob/7b47c88bb4e1388e7c5af1c269fb4a78e227120a/src/reader/_storage.py#L605-L746"&gt;original code&lt;/a&gt;;
that's a lot of work just to &lt;code&gt;append()&lt;/code&gt; some strings.&lt;/p&gt;
&lt;p&gt;Also, while the SQL above is acceptable, it's not the best:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it's needlessly indented&lt;/li&gt;
&lt;li&gt;there's an extra space in front of &lt;code&gt;entries.read&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;the &lt;code&gt;where_snippets&lt;/code&gt; joiner needs more spaces if we indent the function&lt;/li&gt;
&lt;li&gt;(not shown above) multi-line snippets are tricky to indent properly&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We &lt;em&gt;could&lt;/em&gt; wrap everything in &lt;code&gt;dedent&lt;/code&gt;/&lt;code&gt;indent&lt;/code&gt; calls,
but that would make things even more verbose.&lt;/p&gt;
&lt;p&gt;What if there was an object with lots of &lt;code&gt;append()&lt;/code&gt;-like methods?
...something like:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;make_entries_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;entries.id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;entries.title&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;entries&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;NOT&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; entries.read&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;NOT&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; entries.important&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The important bits are still there, but most of the noise went away.
Formatting is a &lt;a class="external" href="https://en.wikipedia.org/wiki/Separation_of_concerns"&gt;separate concern&lt;/a&gt;, so it happens somewhere else;
things are magically cleaned up, dedented, and stitched back together
into clean, regular SQL:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="SQL"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;entries&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;important&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="composition-and-reuse"&gt;Composition and reuse&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#composition-and-reuse" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Let's say we later add a &lt;a class="external" href="https://reader.readthedocs.io/en/latest/api.html#reader.Reader.get_entry_counts"&gt;method to count entries&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Surely, we'd want it to have the same filtering capabilities as &lt;code&gt;get_entries()&lt;/code&gt;.
So we move that part into a function:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;apply_filter_options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;NOT&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; entries.read&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;NOT&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; entries.important&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;make_entry_counts_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="s1"&gt;&amp;#39;count(*)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;&amp;#39;coalesce(sum(read == 1), 0)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;&amp;#39;coalesce(sum(important == 1), 0)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;entries&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;apply_filter_options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;important&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Why pass the query in, instead of just returning the WHERE snippets?
Because it allows doing &lt;em&gt;other kinds&lt;/em&gt; of changes transparently:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;apply_filter_options&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;updates_enabled&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;updates_enabled&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;JOIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;feeds ON feeds.url = entries.feed&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;updates_enabled&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;NOT&amp;#39;&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; feeds.updates_enabled&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The query builder offers a &lt;strong&gt;standard interface for making changes to a query&lt;/strong&gt;,
which makes it easier to compose operations,
which in turn makes it easier to reuse logic.&lt;/p&gt;
&lt;p&gt;If you find this a bit contrived,
check out &lt;a class="external" href="https://github.com/lemon24/reader/blob/1.17/src/reader/_storage.py#L1409-L1479"&gt;this function&lt;/a&gt; for a real-world example.&lt;/p&gt;
&lt;h2 id="intermission-scrolling-window-queries"&gt;Intermission: scrolling window queries&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#intermission-scrolling-window-queries" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;If you're familiar with
scrolling window queries / cursor pagination / keyset pagination,
feel free to skip to the
&lt;a class="anchor" href="#introspection"&gt;next section&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Before we go forward, we need to learn how to implement pagination.&lt;/p&gt;
&lt;p&gt;Assume we have a table like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="sqlite3con"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;sqlite&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="gp"&gt;sqlite&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;a&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;c&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Let's say we need to get the &lt;code&gt;things&lt;/code&gt; sorted by &lt;code&gt;name&lt;/code&gt;,
but split over multiple queries, each returning a 2-row page.
There are two main ways of doing it.&lt;/p&gt;
&lt;p&gt;The obvious one is LIMIT+OFFSET:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="sqlite3con"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;sqlite&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;OFFSET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;a&lt;/span&gt;
&lt;span class="go"&gt;b&lt;/span&gt;
&lt;span class="gp"&gt;sqlite&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;OFFSET&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This returns the correct answer, but has one big performance issue:
each query fetches &lt;strong&gt;and discards&lt;/strong&gt; the rows up to OFFSET,
and then returns (at most) LIMIT rows;
the more data you have, and the nearer the end of the result set you are,
the slower this gets.&lt;/p&gt;
&lt;p&gt;The other one is &lt;a class="external" href="https://www.sqlite.org/rowvalue.html#scrolling_window_queries"&gt;scrolling window queries&lt;/a&gt;
(also known as &lt;em&gt;cursor based pagination&lt;/em&gt;):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="sqlite3con"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;sqlite&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;a&lt;/span&gt;
&lt;span class="go"&gt;b&lt;/span&gt;
&lt;span class="gp"&gt;sqlite&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;things&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;b&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LIMIT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="go"&gt;c&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Instead of using OFFSET to skip &lt;em&gt;n&lt;/em&gt; rows,
it uses WHERE to skip to &lt;em&gt;after the last row&lt;/em&gt; from the previous query,
using the sort key (the &lt;em&gt;cursor&lt;/em&gt;).
While superficially the same,
now &lt;strong&gt;the query can use indices&lt;/strong&gt;
to avoid fetching rows it doesn't need.&lt;/p&gt;
&lt;p&gt;An additional benefit, unrelated to performance,
is that you won't get duplicate/missing results
if rows are inserted/deleted between queries.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;See &lt;a class="external" href="https://medium.com/swlh/why-you-shouldnt-use-offset-and-limit-for-your-pagination-4440e421ba87"&gt;this article&lt;/a&gt; for a more detailed explanation,
complete with a benchmark you can run yourself.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="introspection"&gt;Introspection&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#introspection" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Now that we're familiar with scrolling window queries,
let's do it for a real query:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;make_feeds_query&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;url&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;coalesce(user_title, title)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;feeds&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# if sort == &amp;#39;title&amp;#39;:&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;title_sort&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;lower(coalesce(user_title, title))&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ORDER_BY&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;title_sort&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;url&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This retrieves feeds sorted by title, preferring custom titles;
for feeds with the same title, it falls back to the primary key,
so the result stays consistent between queries.
I omitted other supported orderings for brevity.&lt;/p&gt;
&lt;p&gt;To get the first page, we add a limit:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_feeds_query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LIMIT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:limit&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="SQL"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;title_sort&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;feeds&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;title_sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;limit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;After we run the query, we save the last sort key:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;first_page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;last_result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first_page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;last_result&lt;/span&gt;
&lt;span class="go"&gt;(&amp;#39;file:feed.xml&amp;#39;, &amp;#39;Title&amp;#39;, &amp;#39;title&amp;#39;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_title_sort&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;last_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;last_url&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;last_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To get the second page, we add a limit and a WHERE condition:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_feeds_query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LIMIT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:limit&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;(title_sort, url) &amp;gt; (:last_title_sort, :last_url)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;...and use the last sort key from the previous page as query parameters:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;second_page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;params&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;But, every time we do this,
we have to remember the indices of the sort key values in the result
and the parameter names for the current ordering.&lt;/p&gt;
&lt;p&gt;There must be a better way.&lt;/p&gt;
&lt;p&gt;The WHERE parameters don't actually &lt;em&gt;need&lt;/em&gt; a descriptive name;
also, the computer &lt;em&gt;already knows&lt;/em&gt; both what we're sorting by,
and where those things are in the result tuple –
it's right there in  &lt;code&gt;make_feeds_query()&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Let's make the computer do the work, then.
To add the WHERE condition:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_feeds_query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LIMIT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:limit&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;order_by_things&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ORDER BY&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;:last_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_by_things&lt;/span&gt;&lt;span class="p"&gt;))]&lt;/span&gt;
&lt;span class="c1"&gt;# use the query builder do the formatting (sneaky, but it works)&lt;/span&gt;
&lt;span class="n"&gt;comparison&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;(&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;order_by_things&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;) &amp;gt; (&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]})&lt;/span&gt;

&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;comparison&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="SQL"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;coalesce&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;user_title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AS&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;title_sort&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;feeds&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;title_sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;last_1&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;BY&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;title_sort&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;
&lt;span class="k"&gt;LIMIT&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="k"&gt;limit&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To extract the sort key parameters from the last result:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;order_by_things&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;ORDER BY&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;alias&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SELECT&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;last_&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;last_result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;names&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;index&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;order_by_things&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;
&lt;span class="go"&gt;{&amp;#39;last_0&amp;#39;: &amp;#39;title&amp;#39;, &amp;#39;last_1&amp;#39;: &amp;#39;file:feed.xml&amp;#39;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Now, we only need to specify what we're sorting by once, in &lt;code&gt;make_feeds_query()&lt;/code&gt;.
Because the query builder provides a &lt;strong&gt;standard representation of a query&lt;/strong&gt;,
we can inspect that programmatically,
and let the computer do the work itself.&lt;/p&gt;
&lt;p&gt;Can we do better?&lt;/p&gt;
&lt;h2 id="abstraction"&gt;Abstraction&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#abstraction" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The pattern above seems quite useful,
but it's complicated enough I probably wouldn't get it right all the time;
I bet I missed some corner cases, too.&lt;/p&gt;
&lt;p&gt;If only there was a way to tell the computer directly what I want:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;make_feeds_query&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;url&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;coalesce(user_title, title)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;feeds&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# if sort == &amp;#39;title&amp;#39;:&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;title_sort&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;lower(coalesce(user_title, title))&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scrolling_window_order_by&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;title_sort&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;url&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;...and then have it execute the queries for me:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_feeds_query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;first_page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paginated_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;first_page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="go"&gt;((&amp;#39;file:feed.xml&amp;#39;, &amp;#39;Title&amp;#39;, &amp;#39;title&amp;#39;), (&amp;#39;title&amp;#39;, &amp;#39;file:feed.xml&amp;#39;))&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;first_page&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_feeds_query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;second_page&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;paginated_query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;last&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Pretty neat, huh?&lt;/p&gt;
&lt;p&gt;This is provided by a mixin that's not counted in the 150 lines –
but hey, &lt;a class="external" href="https://github.com/djrobstep/sqlakeyset"&gt;not even SQLAlchemy has it built-in&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/query-builder-why&amp;t=Why%20use%20an%20SQL%20query%20builder%20in%20the%20first%20place%3F"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Why%20use%20an%20SQL%20query%20builder%20in%20the%20first%20place%3F%20https%3A//death.andgravity.com/query-builder-why"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/query-builder-why&amp;title=Why%20use%20an%20SQL%20query%20builder%20in%20the%20first%20place%3F"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/query-builder-why"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Why%20use%20an%20SQL%20query%20builder%20in%20the%20first%20place%3F&amp;url=https%3A//death.andgravity.com/query-builder-why&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;p&gt;&lt;strong&gt;Next: &lt;a class="internal" href="/own-query-builder"&gt;Why I wrote my own SQL query builder&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
</content>
    <link href="https://death.andgravity.com/query-builder-why" rel="alternate"/>
    <summary>This is the second article in a series about writing an SQL query builder in 150 lines of Python, why I wrote it, how I thought about it, and the decisions I had to make. In this article, I talk about why I needed a query builder in the first place, with examples derived from real use cases I had for my feed reader library.</summary>
    <published>2021-05-18T15:20:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/query-builder">
    <id>https://death.andgravity.com/query-builder</id>
    <title>SQL query builder in 150 lines of Python</title>
    <updated>2021-05-18T15:20:00+00:00</updated>
    <content type="html">&lt;p&gt;In this series,
we'll look at an SQL query builder
I wrote for my feed &lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt; library.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Yup, you read that right, the whole thing fits in 150 lines!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;While the code is interesting in its own right
(if for no other reason other than the size),
in the first few articles we'll discuss
&lt;strong&gt;why I wrote it&lt;/strong&gt;, &lt;strong&gt;how I thought about it&lt;/strong&gt;, and &lt;strong&gt;what other options I considered&lt;/strong&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="internal" href="/query-builder-why"&gt;why I needed a query builder in the first place&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a class="internal" href="/own-query-builder"&gt;why I decided to write my own&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;what alternatives I considered, and why I didn't use an existing library&lt;/li&gt;
&lt;li&gt;how I knew it wouldn't become too big and/or a maintenance burden&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;how I modeled the problem, and how I got the idea for it&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;After that, we'll &lt;a class="internal" href="/query-builder-how"&gt;&lt;strong&gt;rewrite it from scratch&lt;/strong&gt;&lt;/a&gt;,
iteratively, and talk about:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;API design&lt;/li&gt;
&lt;li&gt;metaprogramming&lt;/li&gt;
&lt;li&gt;worse ways of doing things&lt;/li&gt;
&lt;li&gt;why I removed a bunch of features&lt;/li&gt;
&lt;li&gt;trade-offs, and knowing when to be lazy&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="so-what-does-it-look-like"&gt;So, what does it look like?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#so-what-does-it-look-like" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;You call &lt;code&gt;KEYWORD&lt;/code&gt; methods on a &lt;code&gt;Query&lt;/code&gt; object to append text:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;url&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;builder.Query object at 0x7fc953e60640&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;SELECT&lt;/span&gt;
&lt;span class="go"&gt;    url&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can chain the calls for convenience (order doesn't matter):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;feeds&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;title&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;updated&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;lt;builder.Query object at 0x7fc953e60640&amp;gt;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;SELECT&lt;/span&gt;
&lt;span class="go"&gt;    url,&lt;/span&gt;
&lt;span class="go"&gt;    title,&lt;/span&gt;
&lt;span class="go"&gt;    updated&lt;/span&gt;
&lt;span class="go"&gt;FROM&lt;/span&gt;
&lt;span class="go"&gt;    feeds&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To get the SQL, you convert the query to a string:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;SELECT\n    url,\n    title,\n    updated\nFROM\n    feeds\n&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Other common things work as well:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;SELECT&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alias&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;long(expression)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
&lt;span class="go"&gt;SELECT&lt;/span&gt;
&lt;span class="go"&gt;    long(expression) AS alias&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;WHERE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;condition&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;another condition&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;WHERE&lt;/span&gt;
&lt;span class="go"&gt;    condition AND&lt;/span&gt;
&lt;span class="go"&gt;    another condition&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Query&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FROM&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;first&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LEFT_JOIN&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;second USING (column)&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="go"&gt;FROM&lt;/span&gt;
&lt;span class="go"&gt;    first&lt;/span&gt;
&lt;span class="go"&gt;LEFT JOIN&lt;/span&gt;
&lt;span class="go"&gt;    second USING (column)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If you want to take a peek at the code right now,
you can find the final version &lt;a class="attachment" href="/_file/query-builder-how/07-more-init/builder.py"&gt;here&lt;/a&gt;
and the tests &lt;a class="attachment" href="/_file/query-builder-how/07-more-init/test_builder.py"&gt;here&lt;/a&gt;.
The version used by &lt;a class="external" href="https://github.com/lemon24/reader"&gt;reader&lt;/a&gt; is &lt;a class="external" href="https://github.com/lemon24/reader/blob/15121f667a6f2e388f0072a3fcd715f533883899/src/reader/_sql_utils.py"&gt;here&lt;/a&gt;
(type-annotated, and with extra features).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Next: &lt;a class="internal" href="/query-builder-why"&gt;Why use a query builder in the first place?&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
</content>
    <link href="https://death.andgravity.com/query-builder" rel="alternate"/>
    <summary>In this series, we examine an SQL query builder I wrote in 150 lines of Python, why I wrote it, how I thought about it, and the decisions I had to make. This article is a sneak peek of the series and the code.</summary>
    <published>2021-05-11T07:50:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/run-sh">
    <id>https://death.andgravity.com/run-sh</id>
    <title>Using a Makefile with .PHONY-only targets? Use a run.sh script instead</title>
    <updated>2021-04-28T15:40:00+00:00</updated>
    <content type="html">&lt;p&gt;I recently discovered a neat pattern:&lt;/p&gt;
&lt;p&gt;When you have a Makefile that only has &lt;code&gt;.PHONY&lt;/code&gt; targets,
you can &lt;em&gt;turn it into a shell script with functions&lt;/em&gt;,
and dispatch to them by adding &lt;code&gt;&amp;quot;$@&amp;quot;&lt;/code&gt; at the end.&lt;/p&gt;
&lt;p&gt;It makes things easier to read and write,
allows passing arguments to the &amp;quot;targets&amp;quot;,
and enables reuse both inside and outside the script.&lt;/p&gt;
&lt;p&gt;This is not my idea, but I think it's quite cool,
and thought others might too.
Here's &lt;a class="external" href="http://www.oilshell.org/blog/2020/02/good-parts-sketch.html#semi-automation-with-runsh-scripts"&gt;the article that sold me on it&lt;/a&gt;;
it discusses the benefits in more detail
and links to other projects that use it.&lt;/p&gt;
&lt;h2 id="why-have-a-makefile-in-the-first-place"&gt;Why have a Makefile in the first place?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-have-a-makefile-in-the-first-place" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;I've been using a Makefile in my Python &lt;a class="external" href="https://github.com/lemon24/reader"&gt;feed reader library&lt;/a&gt;
to get convenient shortcuts for common development stuff:
install dependencies, run tests, etc.
In time, I ended up using some of the targets in CI,
and mentioning them in the developer docs.&lt;/p&gt;
&lt;p&gt;(I originally took this pattern from Flask,
although they stopped using it after 1.0.)&lt;/p&gt;
&lt;p&gt;Here's an abridged version to give you a taste (full Makefile &lt;a class="external" href="https://github.com/lemon24/reader/blob/1.16/Makefile"&gt;here&lt;/a&gt;):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Makefile"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nf"&gt;.PHONY&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;test&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;

&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;pytest&lt;span class="w"&gt; &lt;/span&gt;--runslow

&lt;span class="c"&gt;# mypy does not work on pypy as of January 2020&lt;/span&gt;
&lt;span class="nf"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$$&lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;import sys; print(sys.implementation.name)&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pypy&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mypy does not work on pypy, doing nothing&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="o"&gt;||&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;mypy&lt;span class="w"&gt; &lt;/span&gt;--strict&lt;span class="w"&gt; &lt;/span&gt;src
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For me, this has two main downsides:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;There's no way to pass arguments to the targets,
for example to call &lt;code&gt;pytest -v&lt;/code&gt; while also getting
the &amp;quot;default&amp;quot; &lt;code&gt;--runslow&lt;/code&gt; option.
(In this case, I could have used the &lt;a class="external" href="https://docs.pytest.org/en/stable/reference.html#confval-addopts"&gt;&lt;code&gt;addopts&lt;/code&gt;&lt;/a&gt; config key –
but I don't want to &lt;em&gt;force&lt;/em&gt; everyone to use &lt;code&gt;--runslow&lt;/code&gt;,
I just want to show it's the &lt;em&gt;recommended&lt;/em&gt; way.)&lt;/li&gt;
&lt;li&gt;It makes it harder to write fully-featured scripts;
it is &lt;em&gt;possible&lt;/em&gt;, but the result tends to be &lt;a class="external" href="https://unix.stackexchange.com/a/270799"&gt;less readable&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="enter-run-sh"&gt;Enter run.sh&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#enter-run-sh" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;We could re-write that as a shell script; let's call it &lt;code&gt;run.sh&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="ch"&gt;#!/bin/bash&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;pytest&lt;span class="w"&gt; &lt;/span&gt;--runslow&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;typing&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;local&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;impl&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;python&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;import sys; print(sys.implementation.name)&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;# mypy does not work on pypy as of January 2020&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$impl&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pypy&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;]]&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;then&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;mypy does not work on pypy, doing nothing&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;return&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;fi&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;mypy&lt;span class="w"&gt; &lt;/span&gt;--strict&lt;span class="w"&gt; &lt;/span&gt;src&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;code&gt;$@&lt;/code&gt; at the end dispatches the script arguments to a function
(so &lt;code&gt;./run.sh test&lt;/code&gt; calls &lt;code&gt;test&lt;/code&gt;);
the &lt;code&gt;$@&lt;/code&gt; in &lt;code&gt;test&lt;/code&gt; passes the remaining arguments along
(so &lt;code&gt;./run.sh test -v&lt;/code&gt; ends up running &lt;code&gt;pytest --runslow -v&lt;/code&gt;).&lt;/p&gt;
&lt;h2 id="why-i-think-it-s-cool"&gt;Why I think it's cool&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-i-think-it-s-cool" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id="executable-documentation"&gt;Executable documentation&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#executable-documentation" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;A script is a simple way of documenting project-specific development tools
– with a bit of care, it becomes &lt;em&gt;executable documentation;&lt;/em&gt;
this is a huge benefit that's highlighted in the original article as well.&lt;/p&gt;
&lt;p&gt;I'm strongly considering adding more comments to my &lt;code&gt;run.sh&lt;/code&gt;,
and including it directly in the developer docs,
&lt;em&gt;instead&lt;/em&gt; of the written documentation.&lt;/p&gt;
&lt;p&gt;Most commands are self-evident,
and if you want to run something in a different way,
you can copy-paste it directly into a terminal
(not straightforward with a Makefile).
Hell, you can even source it if you're using a compatible shell,
and have a sort of &amp;quot;project shell&amp;quot;.&lt;/p&gt;
&lt;h3 id="reusability"&gt;Reusability&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#reusability" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Let's look at an example.
I run coverage in three ways:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;for development, with HTML reports and &lt;a class="external" href="https://coverage.readthedocs.io/en/coverage-5.5/contexts.html"&gt;contexts&lt;/a&gt; (&amp;quot;who tests what&amp;quot;)&lt;/li&gt;
&lt;li&gt;for testing across Python versions/interpreters, with &lt;a class="external" href="https://tox.readthedocs.io/en/latest/"&gt;tox&lt;/a&gt;;
contexts could be useful, but they increase run time&lt;/li&gt;
&lt;li&gt;for continuous integration&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;; contexts are not needed&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;All cases should fail if coverage for specific modules is below 100%.&lt;/p&gt;
&lt;p&gt;run.sh makes it possible to skip contexts when running under tox/CI,
which reduced CI run time by 10-30%.
Also, it avoids duplicating some &lt;a class="external" href="https://github.com/lemon24/reader/blob/1.16/Makefile#L14-L17"&gt;pretty hairy commands&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Now, the developer-facing &lt;code&gt;coverage-all&lt;/code&gt; command looks like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;coverage-all&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;coverage-run&lt;span class="w"&gt; &lt;/span&gt;--cov-context&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;coverage-report&lt;span class="w"&gt; &lt;/span&gt;--show-contexts
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... the tox.ini commands look like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="INI"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;[testenv]&lt;/span&gt;
&lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;./run.sh coverage-run --cov-append&lt;/span&gt;

&lt;span class="k"&gt;[testenv:coverage-report]&lt;/span&gt;
&lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;./run.sh coverage-report&lt;/span&gt;

&lt;span class="k"&gt;[testenv:typing]&lt;/span&gt;
&lt;span class="na"&gt;commands&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;./run.sh typing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... and CI calls a function that looks like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ci-run&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;coverage-run&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;coverage-report&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;typing
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;p&gt;This reusability extends to using the functions &lt;em&gt;anywhere&lt;/em&gt; commands are expected:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;timeout&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;./run.sh&lt;span class="w"&gt; &lt;/span&gt;myfunction
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... including inside the script itself
(the original article calls this &lt;a class="external" href="http://www.oilshell.org/blog/2020/02/good-parts-sketch.html#the-0-dispatch-pattern-solves-three-important-problems"&gt;&lt;code&gt;$0&lt;/code&gt;-dispatch&lt;/a&gt;):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;typing-dev&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;find&lt;span class="w"&gt; &lt;/span&gt;src&lt;span class="w"&gt; &lt;/span&gt;-name&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;*.py&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;entr&lt;span class="w"&gt; &lt;/span&gt;-cdr&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$0&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;typing&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$@&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here, &lt;code&gt;entr&lt;/code&gt; takes a command (and its arguments),
and runs it every time a Python file in &lt;code&gt;src&lt;/code&gt; changes.
Note that we use &lt;code&gt;$0&lt;/code&gt; to dispatch to the script's &lt;code&gt;typing&lt;/code&gt; &amp;quot;target&amp;quot;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;You can find reader's full run.sh &lt;a class="external" href="https://github.com/lemon24/reader/blob/master/run.sh"&gt;here&lt;/a&gt;;
in addition to the things above, it has:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;more complex examples&lt;/li&gt;
&lt;li&gt;a workaround to make &lt;code&gt;$0&lt;/code&gt;-dispatch work
when called with &lt;code&gt;bash run.sh&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;a wrapper for using &lt;a class="external" href="http://eradman.com/entrproject/"&gt;entr&lt;/a&gt; with &lt;code&gt;git ls-files&lt;/code&gt;,
based on &lt;a class="external" href="https://jvns.ca/blog/2020/06/28/entr/#restart-every-time-a-new-file-is-added-entr-d"&gt;this pattern&lt;/a&gt; from Julia Evans&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/run-sh&amp;t=Using%20a%20Makefile%20with%20.PHONY-only%20targets%3F%20Use%20a%20run.sh%20script%20instead"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Using%20a%20Makefile%20with%20.PHONY-only%20targets%3F%20Use%20a%20run.sh%20script%20instead%20https%3A//death.andgravity.com/run-sh"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/run-sh&amp;title=Using%20a%20Makefile%20with%20.PHONY-only%20targets%3F%20Use%20a%20run.sh%20script%20instead"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/run-sh"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Using%20a%20Makefile%20with%20.PHONY-only%20targets%3F%20Use%20a%20run.sh%20script%20instead&amp;url=https%3A//death.andgravity.com/run-sh&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;I could probably use tox for CI as well, like &lt;a class="external" href="https://github.com/pallets/flask/blob/2.0.0rc1/.github/workflows/tests.yaml#L53"&gt;Flask does&lt;/a&gt; lately. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/run-sh" rel="alternate"/>
    <summary>... in which we look at an interesting alternative to Makefiles with .PHONY-only targets, and why I think it's cool.</summary>
    <published>2021-04-28T15:40:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/hashlib-buffer-required">
    <id>https://death.andgravity.com/hashlib-buffer-required</id>
    <title>hashlib: object supporting the buffer API required</title>
    <updated>2021-04-20T14:17:00+00:00</updated>
    <content type="html">&lt;p&gt;So you're trying to compute a hash using &lt;a class="external" href="https://docs.python.org/3/library/hashlib.html"&gt;&lt;code&gt;hashlib&lt;/code&gt;&lt;/a&gt;,
and get an exception like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;&amp;lt;stdin&amp;gt;&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;1&lt;/span&gt;, in &lt;span class="n"&gt;&amp;lt;module&amp;gt;&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;object supporting the buffer API required&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... or like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;&amp;lt;stdin&amp;gt;&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;1&lt;/span&gt;, in &lt;span class="n"&gt;&amp;lt;module&amp;gt;&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;Unicode-objects must be encoded before hashing&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="what-does-it-mean"&gt;What does it mean?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-does-it-mean" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The first clue are these two bits from &lt;a class="external" href="https://docs.python.org/3/library/hashlib.html#hash-algorithms"&gt;the docs&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;You can now feed this object with bytes-like objects (normally bytes) using the &lt;code&gt;update()&lt;/code&gt; method.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Feeding string objects into &lt;code&gt;update()&lt;/code&gt; is not supported, as hashes work on bytes, not on characters.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Now, &amp;quot;object supporting the buffer API required&amp;quot;
is a more precise way of saying &amp;quot;the object is not &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-bytes-like-object"&gt;bytes-like&lt;/a&gt;&amp;quot;.
That is, it cannot export a series of bytes through the &lt;em&gt;buffer interface&lt;/em&gt;,
a way for Python objects to provide access to their underlying binary data.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;In the code above, the constructor passes
the initial data to &lt;code&gt;update()&lt;/code&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="why-does-this-happen"&gt;Why does this happen?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-does-this-happen" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/library/hashlib.html#hashlib.hash.update"&gt;&lt;code&gt;update()&lt;/code&gt;&lt;/a&gt; refuses to take anything other than bytes because
&lt;em&gt;there are many different ways&lt;/em&gt; of converting arbitrary objects to bytes
(and some can't even be meaningfully converted
– for example, file objects or sockets).&lt;/p&gt;
&lt;p&gt;Let's look at the initial example,
where we're trying to get the hash of an int.&lt;/p&gt;
&lt;p&gt;One way of converting an int to bytes is
to get its string representation,
and convert that into bytes;
&lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#str.encode"&gt;&lt;code&gt;encode()&lt;/code&gt;&lt;/a&gt;'s default &lt;code&gt;utf-8&lt;/code&gt; encoding should be acceptable:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;2&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;b&amp;#39;2&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Alternatively, we can use &lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#int.to_bytes"&gt;&lt;code&gt;to_bytes()&lt;/code&gt;&lt;/a&gt; to convert it directly;
to do it, we must specify an explicit byte length and order:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;big&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;b&amp;#39;\x00\x02&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;little&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;b&amp;#39;\x02\x00&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;big&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;b&amp;#39;\x00\x00\x00\x02&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;little&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;b&amp;#39;\x02\x00\x00\x00&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;a class="external" href="https://docs.python.org/3/library/struct.html"&gt;&lt;code&gt;struct&lt;/code&gt;&lt;/a&gt; module allows doing the same thing for C structs
composed of bools, bytes, integers and floats, with varied representations:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;gt;i&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;b&amp;#39;\x00\x00\x00\x02&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;i&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;b&amp;#39;\x02\x00\x00\x00&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;gt;q&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;b&amp;#39;\x00\x00\x00\x00\x00\x00\x00\x02&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;q&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;b&amp;#39;\x02\x00\x00\x00\x00\x00\x00\x00&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As you can see, we get different bytes depending on the method used.
Obviously, the hash also differs:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="nb"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;utf-8&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;big&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;little&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;big&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;little&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;gt;i&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;struct&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;lt;i&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="go"&gt;c81e728d9d4c2f636f067f89cc14862c&lt;/span&gt;
&lt;span class="go"&gt;7209a1ce16f85bd1cbd287134ff5cbb6&lt;/span&gt;
&lt;span class="go"&gt;11870cb56df12527e588f2ef967232e8&lt;/span&gt;
&lt;span class="go"&gt;f11177d2ec63d995fb4ac628e0d782df&lt;/span&gt;
&lt;span class="go"&gt;f2dd0dedb2c260419ece4a9e03b2e828&lt;/span&gt;
&lt;span class="go"&gt;f11177d2ec63d995fb4ac628e0d782df&lt;/span&gt;
&lt;span class="go"&gt;f2dd0dedb2c260419ece4a9e03b2e828&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="what-now"&gt;What now?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#what-now" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;In general, you have to pick a standard way of converting things to bytes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;If you only want to hash integers&lt;/strong&gt;, you can pick one of the methods above.
If you go with &lt;code&gt;to_bytes()&lt;/code&gt; or &lt;code&gt;struct&lt;/code&gt;,
the byte size has to fit the biggest number you expect;
for example, 255 is the biggest number you can express with 1 byte;
you need 2 bytes for 256:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;255&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;big&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;b&amp;#39;\xff&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;big&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;&amp;lt;stdin&amp;gt;&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;1&lt;/span&gt;, in &lt;span class="n"&gt;&amp;lt;module&amp;gt;&lt;/span&gt;
&lt;span class="gr"&gt;OverflowError&lt;/span&gt;: &lt;span class="n"&gt;int too big to convert&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to_bytes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;big&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;b&amp;#39;\x01\x00&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;If you want to hash arbitrary objects&lt;/strong&gt;,
you have to find a standard way of converting them to bytes
for each type you need to support, recursively.
I've written &lt;a class="internal" href="/stable-hashing"&gt;an article&lt;/a&gt; about doing this
for (almost) arbitrary objects.&lt;/p&gt;
&lt;p&gt;Particularly, note that &lt;code&gt;repr(...).encode()&lt;/code&gt; will only work
if the result of the object's &lt;code&gt;__repr__&lt;/code&gt; method has all the data you need,
in a predictable order, and nothing that changes between equal objects
(including across processes etc.).&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;C&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__eq__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;other&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;C&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;b&lt;/span&gt;
&lt;span class="go"&gt;True&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="nb"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;False&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;&amp;lt;__main__.C object at 0x7f8890132df0&amp;gt;&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nb"&gt;repr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;&amp;lt;__main__.C object at 0x7f88901be580&amp;gt;&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Here, &lt;code&gt;a&lt;/code&gt; and &lt;code&gt;b&lt;/code&gt; are equal,
but because &lt;code&gt;C&lt;/code&gt; doesn't define &lt;code&gt;__repr__&lt;/code&gt;,
it inherits the default one from &lt;code&gt;object&lt;/code&gt;,
which just returns the type name and memory address of the object.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/hashlib-buffer-required&amp;t=hashlib%3A%20object%20supporting%20the%20buffer%20API%20required"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=hashlib%3A%20object%20supporting%20the%20buffer%20API%20required%20https%3A//death.andgravity.com/hashlib-buffer-required"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/hashlib-buffer-required&amp;title=hashlib%3A%20object%20supporting%20the%20buffer%20API%20required"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/hashlib-buffer-required"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=hashlib%3A%20object%20supporting%20the%20buffer%20API%20required&amp;url=https%3A//death.andgravity.com/hashlib-buffer-required&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


</content>
    <link href="https://death.andgravity.com/hashlib-buffer-required" rel="alternate"/>
    <summary>In this article, you'll find out what Python hashlib "object supporting the buffer API required" TypeErrors mean, why do they happen, and what you can do about it.</summary>
    <published>2021-04-20T12:44:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/stdlib">
    <id>https://death.andgravity.com/stdlib</id>
    <title>Learn by reading code: Python standard library design decisions explained</title>
    <updated>2021-04-19T16:42:00+00:00</updated>
    <content type="html">&lt;p&gt;So, you're an advanced beginner
– you've learned your way past Python basics and can solve real problems.&lt;/p&gt;
&lt;p&gt;You've now moving past tutorials and blog posts;
maybe you feel they offer one-dimensional solutions
to &lt;em&gt;simple, made-up problems;&lt;/em&gt;
maybe instead of solving &lt;em&gt;this specific problem&lt;/em&gt;,
you want to get better at solving problems &lt;em&gt;in general&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Maybe you've heard you should
&lt;em&gt;develop an eye&lt;/em&gt; by reading and writing a lot of code.&lt;/p&gt;
&lt;p&gt;It's true.&lt;/p&gt;
&lt;p&gt;So, &lt;strong&gt;what code should you read?&lt;/strong&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;quot;Just read what you like.&amp;quot;&lt;/p&gt;
&lt;p&gt;What if you don't know what you like?&lt;/p&gt;
&lt;p&gt;What if you don't like the right thing?
Or worse, what if you like the &lt;em&gt;wrong&lt;/em&gt; thing,
and get stuck with bad habits because of it?&lt;/p&gt;
&lt;p&gt;After all, you have to have an eye for that...&lt;/p&gt;
&lt;p&gt;...but that's what you're trying to develop in the first place.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&amp;quot;There are so many projects on GitHub – pick one you like and see how they did it.&amp;quot;&lt;/p&gt;
&lt;p&gt;But most successful projects are quite large; where do you start from?&lt;/p&gt;
&lt;p&gt;And even if you knew where to start,
&lt;em&gt;how they did it&lt;/em&gt; isn't always obvious.
Yes, the code is right there, but it doesn't really tell you
&lt;em&gt;why&lt;/em&gt; they did it,
what they &lt;em&gt;didn't&lt;/em&gt; do,
nor &lt;em&gt;how they thought&lt;/em&gt; about the whole thing.&lt;/p&gt;
&lt;p&gt;In other words, it is not obvious from the code itself
what the design philosophy was,
and what choices were considered before settling on an implementation.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;In this article, we'll look at &lt;strong&gt;some Python standard library modules where it is&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="a-note-about-the-standard-library"&gt;A note about the standard library&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#a-note-about-the-standard-library" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;As a whole, the Python standard library isn't &lt;em&gt;great&lt;/em&gt; for learning &amp;quot;good&amp;quot; style.&lt;/p&gt;
&lt;p&gt;While all the modules are &lt;em&gt;useful&lt;/em&gt;, they're not very uniform:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;they have different authors;&lt;/li&gt;
&lt;li&gt;some of them are old
(pythonic was different 10-20 years ago); and&lt;/li&gt;
&lt;li&gt;they have to preserve backwards compatibility
(refactoring risks introducing bugs,
and major API changes are out of the question).&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;On the other hand, at least part of them
have &lt;strong&gt;detailed proposals&lt;/strong&gt;
explaining the design goals and tradeoffs,
and the newer ones are actually quite consistent.&lt;/p&gt;
&lt;p&gt;It's a few of the latter we'll look at.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;Style aside, there's a lot to learn from the standard library,
since it solves real problems for a diverse population of developers.&lt;/p&gt;
&lt;p&gt;It's interesting to look at the differences
between stdlib stuff and newer external alternatives
– the gap shows a perceived deficiency
(otherwise they wouldn't have bothered with the new thing).
A decent example of this is urllib vs. requests.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="how-to-read-these"&gt;How to read these&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#how-to-read-these" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Roughly in this order:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Get familiar with the library as a user:
read the documentation, play with the examples a bit.&lt;/li&gt;
&lt;li&gt;Read the corresponding Python Enhancement Proposal (PEP).
The interesting sections usually are
the abstract, rationale, design decisions, discussion, and rejected ideas.&lt;/li&gt;
&lt;li&gt;Read the code; it's conveniently linked at the top of each documentation page.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="statistics"&gt;statistics&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#statistics" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;a class="external" href="https://docs.python.org/3/library/statistics.html"&gt;statistics&lt;/a&gt; module adds statistical functions to the standard library;
it's not intended to be a competitor to libraries like NumPy,
but is rather &amp;quot;aimed at the level of graphing and scientific calculators&amp;quot;.&lt;/p&gt;
&lt;p&gt;It was introduced in &lt;a class="external" href="https://www.python.org/dev/peps/pep-0450/"&gt;PEP 450&lt;/a&gt;.
Even if you are not familiar with the subject matter,
it is a very interesting read:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The Rationale section compares the proposal with NumPy
and do-it-yourself solutions;
it's particularly good at showing &lt;em&gt;what&lt;/em&gt; and &lt;em&gt;why&lt;/em&gt;
something is added to the standard library.&lt;/li&gt;
&lt;li&gt;There's also a Design Decisions section
that makes explicit what the general design philosophy was;
Discussion and FAQ have some interesting details as well.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The documentation is also very nice.
This is by design; as the proposal says:
&amp;quot;Plenty of documentation,
aimed at readers who understand the basic concepts but may not know
(for example) which variance they should use [...]
But avoid going into tedious mathematical detail.&amp;quot;&lt;/p&gt;
&lt;p&gt;The code is relatively simple, and when it's not,
there are comments and links to detailed explanations or papers.
This &lt;em&gt;may&lt;/em&gt; be useful if you're just learning about this stuff and
find it easier to read code than maths notation.&lt;/p&gt;



&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        Liking this so far? Here&amp;#39;s another article you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/aosa"&gt;
            Struggling to structure code in larger programs? Great resources a beginner might not find so easily
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h2 id="pathlib"&gt;pathlib&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#pathlib" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;a class="external" href="https://docs.python.org/3/library/pathlib.html"&gt;pathlib&lt;/a&gt; module provides a simple hierarchy of classes
to handle filesystem paths;
it is a higher level alternative to &lt;a class="external" href="https://docs.python.org/3/library/os.path.html"&gt;os.path&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It was introduced in &lt;a class="external" href="https://www.python.org/dev/peps/pep-0428/"&gt;PEP 428&lt;/a&gt;.
Most of the examples serve to illustrate the underlying philosophy,
with the code left as specification.&lt;/p&gt;
&lt;p&gt;The code is a good read for a few reasons:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;You're likely &lt;em&gt;already familiar&lt;/em&gt; with the subject matter;
even if you didn't use pathlib before,
you may have used os.path,
or a similar library in some other language.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It is a good &lt;em&gt;object-oriented&lt;/em&gt; solution.
It uses object oriented programming with abstract (read: invented) concepts
to achieve better code structure and reuse.
It's probably a much better example than the old
Animal​–​Dog​–​Cat​–​Duck​–​speak().&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;It is a good &lt;em&gt;comparative study&lt;/em&gt; subject:
both pathlib and os.path solve the same problem
with &lt;em&gt;vastly&lt;/em&gt; different programming styles.
Also, there was &lt;a class="external" href="https://www.python.org/dev/peps/pep-0355/"&gt;another proposal&lt;/a&gt; that was rejected,
and there are at least five similar libraries out there;
pathlib learns from all of them.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="dataclasses"&gt;dataclasses&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#dataclasses" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The &lt;a class="external" href="https://docs.python.org/3/library/dataclasses.html"&gt;dataclasses&lt;/a&gt; module reduces the boilerplate of writing classes
by generating special methods like &lt;code&gt;__init__&lt;/code&gt; and &lt;code&gt;__repr__&lt;/code&gt;.
(See &lt;a class="external" href="https://realpython.com/python-data-classes/"&gt;this tutorial&lt;/a&gt; for an introduction that has
more concrete examples than the official documentation.)&lt;/p&gt;
&lt;p&gt;It was introduced in &lt;a class="external" href="https://www.python.org/dev/peps/pep-0557/"&gt;PEP 557&lt;/a&gt; as a simpler version of &lt;a class="external" href="https://www.attrs.org/en/stable/why.html#data-classes"&gt;attrs&lt;/a&gt;.
The Specification section is similar to the documentation;
the good stuff is in Rationale, Discussion, and Rejected Ideas.&lt;/p&gt;
&lt;p&gt;The code is extremely well commented;
particularly interesting is &lt;a class="external" href="https://github.com/python/cpython/blob/3.9/Lib/dataclasses.py#L779"&gt;this use&lt;/a&gt; of &lt;a class="external" href="https://www.hillelwayne.com/decision-tables/"&gt;decision tables&lt;/a&gt;
(&lt;a class="external" href="https://github.com/python/cpython/blob/3.9/Lib/dataclasses.py#L119"&gt;ASCII version&lt;/a&gt;, &lt;a class="external" href="https://bugs.python.org/issue32929#msg312829"&gt;nested if version&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;It is also a good example of metaprogramming;
Raymond Hettinger's
&lt;a class="external" href="https://www.youtube.com/watch?v=T-TwcmT6Rcw"&gt;Dataclasses: The code generator to end all code generators&lt;/a&gt;
talk&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt; covers this aspect in detail.
If you're having trouble understanding the code, watch the talk first;
I found its examination of the &lt;em&gt;generated&lt;/em&gt; code quite helpful.&lt;/p&gt;
&lt;h2 id="bonus-graphlib"&gt;Bonus: graphlib&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bonus-graphlib" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/library/graphlib.html"&gt;graphlib&lt;/a&gt; was added in Python 3.9,
and at the moment contains just one thing:
an implementation of a topological sort algorithm
(here's a &lt;a class="external" href="https://runestone.academy/runestone/books/published/pythonds3/Graphs/TopologicalSorting.html"&gt;refresher&lt;/a&gt; on what that is and how it's useful).&lt;/p&gt;
&lt;p&gt;It doesn't come with a PEP;
it does however have an &lt;a class="external" href="https://bugs.python.org/issue17005"&gt;issue&lt;/a&gt;
with lots of comments from various core developers,
including Raymond Hettinger and Tim Peters
(of Zen of Python fame).&lt;/p&gt;
&lt;p&gt;Since this is essentially a solved problem,
&lt;em&gt;the discussion focuses on the API:&lt;/em&gt;
where to put it, what to call it,
how to represent the input and output,
how to make it easy to use and flexible at the same time.&lt;/p&gt;
&lt;p&gt;One thing they're trying to do is reconcile two diferent use cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;em&gt;Here's a graph, give me all the nodes in topological order.&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;Here's a graph, give me the nodes that can be processed right now&lt;/em&gt;
(either because they don't have dependencies,
or because their dependencies have already been processed).
This is useful to parallelize work,
for example downloading and installing packages
that depend on other packages.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Unlike with PEPs, you can see the solution evolving as you read.
Most proposals summarize the other choices as well,
but if you don't follow the mailing list
it's easy to get the impression they just &lt;em&gt;appear&lt;/em&gt;, fully formed.&lt;/p&gt;
&lt;p&gt;Compared to the discussion in the issue, the code itself is tiny
– just under 250 lines, mostly comments and documentation.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/stdlib&amp;t=Learn%20by%20reading%20code%3A%20Python%20standard%20library%20design%20decisions%20explained"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Learn%20by%20reading%20code%3A%20Python%20standard%20library%20design%20decisions%20explained%20https%3A//death.andgravity.com/stdlib"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/stdlib&amp;title=Learn%20by%20reading%20code%3A%20Python%20standard%20library%20design%20decisions%20explained"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/stdlib"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Learn%20by%20reading%20code%3A%20Python%20standard%20library%20design%20decisions%20explained&amp;url=https%3A//death.andgravity.com/stdlib&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;&lt;a class="external" href="https://www.youtube.com/watch?v=T-TwcmT6Rcw"&gt;Recording&lt;/a&gt;, &lt;a class="external" href="https://www.dropbox.com/s/te4q0xf46zkuu21/hettinger_dataclasses_pycon_2018.zip"&gt;HTML slides&lt;/a&gt;, &lt;a class="external" href="https://www.dropbox.com/s/m8pwkkz43qz5pgt/HettingerPycon2018.pdf"&gt;PDF slides&lt;/a&gt;. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/stdlib" rel="alternate"/>
    <summary>On your Python learning journey, you may have heard that a great way to get better is to read code written by other people. That's true, but finding good code to study is not easy, mostly because the design philosophy and the reasoning behind the code are rarely documented. The Python standard library is special in this regard: not only is the code open source, but the discussions around the design decisions are public, too.</summary>
    <published>2021-04-12T14:55:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/dataclasses">
    <id>https://death.andgravity.com/dataclasses</id>
    <title>Dataclasses without type annotations</title>
    <updated>2023-01-13T16:08:32+00:00</updated>
    <content type="html">&lt;p&gt;The &lt;a class="external" href="https://docs.python.org/3/library/dataclasses.html"&gt;dataclasses&lt;/a&gt; standard library module
reduces the boilerplate of writing classes
by generating special methods like &lt;code&gt;__init__&lt;/code&gt; and &lt;code&gt;__repr__&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I've noticed a small (but vocal) minority of people that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;would like to use dataclasses, but feel they are forced to use type annotations to do so;
and more generally, that choosing to opt out of type hints
means they are restricted from using specific orthogonal language features&lt;/li&gt;
&lt;li&gt;perceive dataclasses' use of type annotations
as a sign of type annotations becoming compulsory in the future&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Now, I know most of these people are probably just looking
for something to be angry about – this is the internet, after all.&lt;/p&gt;
&lt;p&gt;But if you &lt;em&gt;really&lt;/em&gt; want to use dataclasses, you can, static typing or not. Here:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;two&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;Data(one=1, two=2)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;I'll say it again:
&lt;strong&gt;dataclasses do not require type annotations&lt;/strong&gt;.
Despite what most examples show, they only require variable annotations.&lt;/p&gt;
&lt;p&gt;If you'd like to know why, how to make the best of it,
and what this means about Python in general, read on!&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2021-04-01 update&lt;/strong&gt;:
The decorator from the
&lt;a class="anchor" href="#if-you-really-don-t-like-variable-annotations"&gt;If you really don't like variable annotations&lt;/a&gt;
section below is now available on PyPI: &lt;a class="external" href="https://pypi.org/project/typeless-dataclasses/"&gt;typeless-dataclasses&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;Dataclasses were added in Python 3.7,
with a &lt;a class="external" href="https://pypi.org/project/dataclasses/"&gt;backport&lt;/a&gt; available for 3.6.
If you need to support earlier versions,
or want more powerful features like validators,
converters, and &lt;code&gt;__slots__&lt;/code&gt;, check out &lt;a class="external" href="https://www.attrs.org/en/stable/why.html#data-classes"&gt;attrs&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;!--

.. tip::

    If you really don't like variable annotations,
    and would be willing to use a custom decorator
    to make them truly optional,
    [check this out](#if-you-really-don-t-like-variable-annotations).

--&gt;

&lt;details class="toc"&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#a-bit-of-language-lawyering"&gt;A bit of language lawyering&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#if-not-type-hints-then-what"&gt;If not type hints, then what?&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#partial-types"&gt;Partial types&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#documentation"&gt;Documentation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#ellipsis"&gt;Ellipsis&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#being-type-checker-friendly"&gt;Being type checker friendly&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#aside-named-tuples"&gt;Aside: named tuples&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#will-this-not-break-stuff"&gt;Will this not break stuff?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#if-you-really-don-t-like-variable-annotations"&gt;If you really don't like variable annotations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#we-are-all-consenting-adults"&gt;We are all consenting adults&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="a-bit-of-language-lawyering"&gt;A bit of language lawyering&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#a-bit-of-language-lawyering" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;First, let's define some terms, straight from the Python glossary:&lt;/p&gt;
&lt;dl&gt;
&lt;dt&gt;&lt;a class="external" href="https://docs.python.org/3/glossary.html#term-annotation"&gt;annotation&lt;/a&gt;&lt;/dt&gt;
&lt;dd&gt;A label associated with a variable, a class attribute or a function parameter or return value, used by convention as a &lt;em&gt;type hint&lt;/em&gt;.&lt;/dd&gt;
&lt;dt&gt;&lt;a class="external" href="https://docs.python.org/3/glossary.html#term-variable-annotation"&gt;variable annotation&lt;/a&gt;&lt;/dt&gt;
&lt;dd&gt;An &lt;em&gt;annotation&lt;/em&gt; of a variable or a class attribute.&lt;/dd&gt;
&lt;dt&gt;&lt;a class="external" href="https://docs.python.org/3/glossary.html#term-type-hint"&gt;type hint&lt;/a&gt;&lt;/dt&gt;
&lt;dd&gt;An &lt;em&gt;annotation&lt;/em&gt; that specifies the expected type for a variable, a class attribute, or a function parameter or return value. Type hints are optional and are not enforced by Python [...].&lt;/dd&gt;
&lt;dt&gt;&lt;a class="external" href="https://www.python.org/dev/peps/pep-0526/"&gt;PEP 526&lt;/a&gt;&lt;/dt&gt;
&lt;dd&gt;(my definition) Titled &amp;quot;Syntax for Variable Annotations&amp;quot;, Python enhancement proposal that specifies two different things: syntax to add annotations to variables &lt;em&gt;and&lt;/em&gt; how to use said syntax with &lt;a class="external" href="https://www.python.org/dev/peps/pep-0484/"&gt;PEP 484&lt;/a&gt;, &amp;quot;Type Hints&amp;quot;.&lt;/dd&gt;
&lt;/dl&gt;
&lt;hr /&gt;
&lt;p&gt;In practice, &lt;em&gt;annotation&lt;/em&gt; is used somewhat interchangably with &lt;em&gt;type [hint] annotation&lt;/em&gt;.
There's this example from the beginning of the &lt;a class="external" href="https://docs.python.org/3/library/dataclasses.html"&gt;dataclasses&lt;/a&gt; module:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The member variables to use in these generated methods are defined using PEP 526 type annotations.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;It's also true that most of the examples are using PEP 484 annotations.&lt;/p&gt;
&lt;p&gt;However, the &lt;a class="external" href="https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass"&gt;dataclass()&lt;/a&gt; decorator documentation clearly says that the types specified in the annotation are ignored:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The dataclass() decorator examines the class to find fields. A field is defined as class variable that has a type annotation. With two exceptions described below, &lt;strong&gt;nothing in dataclass() examines the type specified in the variable annotation&lt;/strong&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Which has some very interesting implications: you can use &lt;em&gt;anything&lt;/em&gt; as the annotation.&lt;/p&gt;
&lt;p&gt;Don't believe me? Here's &lt;a class="external" href="https://www.reddit.com/r/Python/comments/7hpmp8/-/dqtjftk/?context=3"&gt;from the author himself&lt;/a&gt;, again:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;That is, use any type you want. If you're not using a static type checker, no one is going to care what type you use.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Really,&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="IPython"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Literally&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;anything&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;can go&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;in here&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;as_long_as&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;it can be evaluated&amp;quot;&lt;/span&gt;
    &lt;span class="c1"&gt;# Now, I&amp;#39;ve noticed a tendency for this program to get rather silly.&lt;/span&gt;
    &lt;span class="n"&gt;hell&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;with_&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;from __future__ import annotations&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;it_s&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;even&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;evaluated&lt;/span&gt;
    &lt;span class="n"&gt;it&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;just&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;be&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;syntactically&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;valid&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="c1"&gt;# Right! Stop that! It&amp;#39;s SILLY!&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="if-not-type-hints-then-what"&gt;If not type hints, then what?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#if-not-type-hints-then-what" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Now that we've seen that type hints are not required,
let's look at some decent alternatives
of using dataclasses without them.&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h3 id="partial-types"&gt;Partial types&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#partial-types" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;My favorite approach is to use a built-in, string or literal
that roughly matches type of the attribute,
to make the intent more obvious to human readers.
I've found myself doing this naturally,
and it's what prompted this article in the first place.&lt;/p&gt;
&lt;p&gt;It's quite convenient when you come back to the code after a few months :)&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;set&lt;/span&gt;
    &lt;span class="n"&gt;two&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;dict(int -&amp;gt; str)&amp;#39;&lt;/span&gt;
    &lt;span class="n"&gt;three&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="documentation"&gt;Documentation&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#documentation" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Speaking of showing intent:
if you're not using &lt;a class="external" href="https://www.sphinx-doc.org/en/master/usage/extensions/autodoc.html#directive-autoattribute"&gt;some other convention&lt;/a&gt; for attribute documentation,
annotations seem like a good place for short docstrings.
Although I doubt any documentation generators support this;
still fine for scripts, though.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;the first thing&amp;quot;&lt;/span&gt;
    &lt;span class="n"&gt;two&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;the second thing; an integer&amp;quot;&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="ellipsis"&gt;Ellipsis&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#ellipsis" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;The &lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#the-ellipsis-object"&gt;Ellipsis&lt;/a&gt; literal is a nice way of saying
&amp;quot;I don't care about this value&amp;quot;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;
    &lt;span class="n"&gt;two&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="being-type-checker-friendly"&gt;Being type checker friendly&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#being-type-checker-friendly" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;If you still want the dataclass to work with type checking,
while not bothering with types yourself, you can use &lt;a class="external" href="https://docs.python.org/3/library/typing.html#typing.Any"&gt;Any&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;typing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt;
    &lt;span class="n"&gt;two&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Any&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Or, if you don't like the extra import, use &lt;a class="external" href="https://docs.python.org/3/library/functions.html#object"&gt;object&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;one&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt;
    &lt;span class="n"&gt;two&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;object&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This works because everything in Python is an object (figuratively &lt;em&gt;and literally&lt;/em&gt;).&lt;/p&gt;
&lt;h3 id="aside-named-tuples"&gt;Aside: named tuples&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#aside-named-tuples" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;Not directly related to dataclasses,
but some of the above work with the &lt;a class="external" href="https://docs.python.org/3/library/typing.html#typing.NamedTuple"&gt;typed version of namedtuple&lt;/a&gt; as well.&lt;/p&gt;
&lt;p&gt;They don't all&lt;sup class="footnote-ref" id="fnref-2"&gt;&lt;a href="#fn-2"&gt;2&lt;/a&gt;&lt;/sup&gt; work because
NamedTuple checks the annotation is a type
(as defined by &lt;a class="external" href="https://www.python.org/dev/peps/pep-0484/#non-goals"&gt;PEP 484&lt;/a&gt;) at runtime.
So, &lt;a class="anchor" href="#partial-types"&gt;built-in types&lt;/a&gt;,
including &lt;a class="anchor" href="#being-type-checker-friendly"&gt;object&lt;/a&gt; and None, are OK;
&lt;a class="anchor" href="#ellipsis"&gt;ellipsis&lt;/a&gt; and &lt;a class="anchor" href="#documentation"&gt;string literals&lt;/a&gt; aren't.&lt;/p&gt;
&lt;h2 id="will-this-not-break-stuff"&gt;Will this not break stuff?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#will-this-not-break-stuff" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;No.&lt;/p&gt;
&lt;p&gt;If the documentation states that &lt;a class="external" href="https://docs.python.org/3/library/dataclasses.html#dataclasses.dataclass"&gt;dataclass()&lt;/a&gt; ignores annotation values,
it will stay like that for the foreseeable future;
standard library deprecations aren't taken lightly.&lt;/p&gt;
&lt;p&gt;Also, all of the major typing PEPs (&lt;a class="external" href="https://www.python.org/dev/peps/pep-0484/#non-goals"&gt;484&lt;/a&gt;, &lt;a class="external" href="https://www.python.org/dev/peps/pep-0526/#non-goals"&gt;526&lt;/a&gt;, &lt;a class="external" href="https://www.python.org/dev/peps/pep-0563/#non-goals"&gt;563&lt;/a&gt;)
clearly state that:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Python will remain a dynamically typed language,
and the authors have no desire to ever make type hints mandatory,
even by convention.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;563 does imply that the type hinting use of annotations will become
standard in the future, but that's only relevant if you care about typing.&lt;/p&gt;
&lt;h2 id="if-you-really-don-t-like-variable-annotations"&gt;If you really don't like variable annotations&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#if-you-really-don-t-like-variable-annotations" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;... I made a decorator that makes them optional:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dataclasses&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dataclass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="nd"&gt;@typeless&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;two&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Compare with attrs:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;attr&lt;/span&gt;

&lt;span class="nd"&gt;@attr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;s&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;one&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ib&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;two&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;attr&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ib&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;details&gt;
&lt;summary&gt;It is less than 30 lines of code,
and works by adding annotations programmatically:&lt;/summary&gt;

&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;typing&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;inspect&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;dataclasses&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;typeless&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;hasattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;__annotations__&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__annotations__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__dict__&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;startswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;__&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;endswith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;__&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;isattribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Field&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;annotation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Any&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;annotation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ClassVar&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;typing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Any&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="bp"&gt;cls&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__annotations__&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;setdefault&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;annotation&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;cls&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;isattribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="nb"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isroutine&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ismethoddescriptor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;inspect&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isdatadescriptor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/details&gt;

&lt;p&gt;It's silly, but it works!&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;2021-04-01 update&lt;/strong&gt;: This is now available on PyPI: &lt;a class="external" href="https://pypi.org/project/typeless-dataclasses/"&gt;typeless-dataclasses&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="we-are-all-consenting-adults"&gt;We are all consenting adults&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#we-are-all-consenting-adults" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;There's a saying in the Python world,
probably as pervasive as &lt;a class="external" href="https://www.python.org/dev/peps/pep-0020/"&gt;The Zen of Python&lt;/a&gt; itself,
that you may be unaware of if you haven't read older articles
or discussions on python-dev: &lt;em&gt;we are all consenting adults&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;It was initially used to refer to Python's attitude towards
&lt;a class="external" href="https://mail.python.org/pipermail/tutor/2003-October/025932.html"&gt;private class attributes&lt;/a&gt; (that is, nothing's really private),
but it also applies to things like &lt;a class="external" href="https://code.activestate.com/lists/python-list/185411"&gt;monkey patching&lt;/a&gt;, &lt;a class="external" href="https://mail.python.org/pipermail/tutor/2012-July/090243.html"&gt;code generation&lt;/a&gt;,
and more:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;[...] No class or class instance can
keep you away from all what's inside (this makes introspection
possible and powerful). Python trusts you. It says &amp;quot;hey, if you want
to go poking around in dark places, I'm gonna trust that you've got
a good reason and you're not making trouble.&amp;quot;&lt;/p&gt;
&lt;p&gt;After all, we're all consenting adults here.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;As long as you're OK with the consequences,
you can do whatever you please;
no one's stopping you.
Of course, it is the responsible, adult thing to &lt;em&gt;learn&lt;/em&gt; what those are
– &amp;quot;know the rules so you can break them effectively&amp;quot; kind of thing.&lt;/p&gt;
&lt;p&gt;Yes, if you're working on a team,
you might have to gather consensus and persuade people
(or if you can't, go with the current one),
but isn't that how a healthy team works anyway?&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Type annotations are (and will continue to be) a thing,
and dataclasses exist in the context of that;
it would be silly to not converge on something,
and not have clear guidance for beginners.&lt;/p&gt;
&lt;p&gt;But if you go and read the documentation,
there is a clear alternative &lt;em&gt;right there&lt;/em&gt;.
If you are experienced enough have opinions about things,
you are probably experienced enough to understand the alternatives
and make your own choices.&lt;/p&gt;
&lt;p&gt;Python trusts you :)&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/dataclasses&amp;t=Dataclasses%20without%20type%20annotations"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Dataclasses%20without%20type%20annotations%20https%3A//death.andgravity.com/dataclasses"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/dataclasses&amp;title=Dataclasses%20without%20type%20annotations"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/dataclasses"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Dataclasses%20without%20type%20annotations&amp;url=https%3A//death.andgravity.com/dataclasses&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;You may have seen some of the examples below
in very nice Reddit comments like &lt;a class="external" href="https://www.reddit.com/r/Python/comments/8d7ddz/-/dxlm6ul/?context=8&amp;amp;depth=9"&gt;this one&lt;/a&gt;
(it appears in other threads as well,
where the author's patience wasn't really deserved;
I'm deliberately not linking to those). &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id="fn-2"&gt;&lt;p&gt;In a previous version of this article, I stated they &lt;em&gt;all&lt;/em&gt; work. &lt;a href="#fnref-2" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/dataclasses" rel="alternate"/>
    <summary>... in which we talk about the many ways of using Python dataclasses without type annotations (or even variable annotations!), and what this says about Python.</summary>
    <published>2021-03-23T15:18:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/stable-hashing">
    <id>https://death.andgravity.com/stable-hashing</id>
    <title>Deterministic hashing of Python data objects</title>
    <updated>2021-03-19T19:51:00+00:00</updated>
    <content type="html">&lt;p&gt;... in which we calculate deterministic hashes for Python data objects,
stable across interpreter versions and implementations.&lt;/p&gt;
&lt;p&gt;If you're in a hurry,
you can find the final version of the code &lt;a class="anchor" href="#conclusion"&gt;at the end&lt;/a&gt;.&lt;/p&gt;
&lt;details class="toc"&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#why-is-this-useful"&gt;Why is this useful?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#requirements"&gt;Requirements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#problem-we-need-a-stable-hash-function"&gt;Problem: we need a stable hash function&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#hash-not-stable-too-restrictive"&gt;hash(): not stable, too restrictive&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#hashlib-still-restrictive"&gt;hashlib: still restrictive&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#problem-we-need-a-deterministic-way-of-serializing-objects"&gt;Problem: we need a deterministic way of serializing objects&lt;/a&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#pickle-not-stable"&gt;pickle: not stable&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#str-and-repr-not-stable-not-safe"&gt;str() and repr(): not stable, not safe&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#json"&gt;json 👍&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#problem-we-need-to-ignore-empty-values"&gt;Problem: we need to ignore empty values&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#problem-we-need-to-skip-some-fields"&gt;Problem: we need to skip some fields&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#conclusion"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="why-is-this-useful"&gt;Why is this useful?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-is-this-useful" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Let's say you have a &lt;a class="external" href="https://github.com/lemon24/reader"&gt;feed reader library&lt;/a&gt;;
one of its main features is retrieving and storing &lt;a class="external" href="https://en.wikipedia.org/wiki/Web_feed"&gt;web feeds&lt;/a&gt;
(Atom, RSS, and so on).&lt;/p&gt;
&lt;p&gt;Entries (articles) usually have an &lt;code&gt;updated&lt;/code&gt; date,
indicating the last time the entry was modified in a significant way.
An entry is updated only if its &lt;code&gt;updated&lt;/code&gt; in the feed
is newer than the one we have stored for it.&lt;/p&gt;
&lt;p&gt;However, you notice the content of some entries changes
without &lt;code&gt;updated&lt;/code&gt; changing,
so you decide to update entries whenever they change,
regardless of &lt;code&gt;updated&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;After the feed is retrieved,
entries are converted to data objects like these
(the real ones have &lt;a class="external" href="https://github.com/lemon24/reader/blob/d2aaf418f8c4d45a97cabaa7eb76498239d0503b/src/reader/_types.py#L97-L114"&gt;more attributes&lt;/a&gt;, though):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;language&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;

&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;A naive approach is to get the stored entry data
and compare it with the feed version,
but that's pretty inefficient.&lt;/p&gt;
&lt;p&gt;A better solution is to use a hash function
– a way to map data of arbitrary size (the message)
to a fixed-size chunk of data (the hash value),
such that:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;it is quick to compute the hash value for any given message&lt;/li&gt;
&lt;li&gt;the same message always results in the same hash&lt;/li&gt;
&lt;li&gt;it is extremely unlikely two slightly different messages have the same hash&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Then, instead of getting the full entry data from storage,
we just get its (previously computed) hash,
and compare it with the hash of the one we just retrieved.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;&lt;a class="external" href="https://en.wikipedia.org/wiki/Cryptographic_hash_function"&gt;Cryptographic hash functions&lt;/a&gt; have more properties,
but the three listed above are everything we need.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="requirements"&gt;Requirements&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#requirements" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Looking at our use case, we need a hash function that:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;supports (almost) arbitrary data objects;
in our case, the various built-in types, datetimes,
and dataclass instances should be enough&lt;/li&gt;
&lt;li&gt;is safe; passing an unsupported object should be an error&lt;/li&gt;
&lt;li&gt;is stable across interpreter versions and implementations,
operating systems, and host machines&lt;/li&gt;
&lt;li&gt;ignores &amp;quot;empty&amp;quot; values, to allow adding new fields without the hash changing&lt;/li&gt;
&lt;li&gt;can skip some of the fields (I actually realized this is needed much later)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Because I'm using it in an existing library, I have some additional requirements:&lt;/p&gt;
&lt;ol start="6"&gt;
&lt;li&gt;it should not have other dependencies outside the standard library,
since any extra dependency gets passed down to the users&lt;/li&gt;
&lt;li&gt;it should be minimally invasive to existing code&lt;/li&gt;
&lt;li&gt;it should work with static typing&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id="problem-we-need-a-stable-hash-function"&gt;Problem: we need a stable hash function&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-we-need-a-stable-hash-function" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id="hash-not-stable-too-restrictive"&gt;hash(): not stable, too restrictive&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#hash-not-stable-too-restrictive" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;An easy solution seems to be the built-in &lt;a class="external" href="https://docs.python.org/3/library/functions.html#hash"&gt;hash()&lt;/a&gt; function,
which returns the integer hash of an object.
However, it has a couple of issues.&lt;/p&gt;
&lt;p&gt;By default, the hashes of str and bytes objects
are randomized for security reasons (&lt;a class="external" href="https://docs.python.org/3/reference/datamodel.html#object.__hash__"&gt;details&lt;/a&gt;, second note),
so they're not predictable between Python processes:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;print(hash(&amp;quot;abc&amp;quot;))&amp;#39;&lt;/span&gt;
&lt;span class="go"&gt;-4743820898567001518&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;-c&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;print(hash(&amp;quot;abc&amp;quot;))&amp;#39;&lt;/span&gt;
&lt;span class="go"&gt;-6699381079787346150&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Also, hash() only supports &lt;a class="external" href="https://docs.python.org/3/glossary.html#term-hashable"&gt;hashable&lt;/a&gt; objects;
this means no lists, dicts, or non-&lt;a class="external" href="https://docs.python.org/3/library/dataclasses.html#frozen-instances"&gt;frozen&lt;/a&gt; dataclasses.
For my specific use case, this wasn't actually a problem, but
it already puts huge constraints on how arbitrary the input can be.&lt;/p&gt;
&lt;h3 id="hashlib-still-restrictive"&gt;hashlib: still restrictive&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#hashlib-still-restrictive" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/library/hashlib.html"&gt;hashlib&lt;/a&gt; contains many different secure hash algorithms,
which are by definition deterministic.&lt;/p&gt;
&lt;p&gt;But it has one big problem – it only takes bytes:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;abc&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;900150983cd24fb0d6963f7d28e17f72&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;abc&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;&amp;lt;stdin&amp;gt;&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;1&lt;/span&gt;, in &lt;span class="n"&gt;&amp;lt;module&amp;gt;&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;Unicode-objects must be encoded before hashing&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hexdigest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;&amp;lt;stdin&amp;gt;&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;1&lt;/span&gt;, in &lt;span class="n"&gt;&amp;lt;module&amp;gt;&lt;/span&gt;
&lt;span class="gr"&gt;TypeError&lt;/span&gt;: &lt;span class="n"&gt;object supporting the buffer API required&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is something we can work with, though: it changes our problem
from &lt;em&gt;we need a stable hash function&lt;/em&gt;
to &lt;em&gt;we need a deterministic way of serializing objects to bytes&lt;/em&gt;.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;If you're curious &lt;em&gt;why&lt;/em&gt; it only takes bytes,
check out &lt;a class="internal" href="/hashlib-buffer-required"&gt;this article&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="problem-we-need-a-deterministic-way-of-serializing-objects"&gt;Problem: we need a deterministic way of serializing objects&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-we-need-a-deterministic-way-of-serializing-objects" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;h3 id="pickle-not-stable"&gt;pickle: not stable&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#pickle-not-stable" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/library/pickle.html"&gt;pickle&lt;/a&gt; can turn most Python objects into bytes.&lt;/p&gt;
&lt;p&gt;It does have multiple protocols,
and the default one can change with the Python version;
but we can select one and stick with it –
we'll use version 4, added in Python 3.4.&lt;/p&gt;
&lt;p&gt;Again, the easy solution is deceiving, since it seems to work:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;&lt;span class="k"&gt;function&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pickle&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;{&lt;/span&gt;
&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;lt;&amp;lt;EOD
&lt;span class="go"&gt;import pickle&lt;/span&gt;
&lt;span class="go"&gt;from datetime import datetime&lt;/span&gt;
&lt;span class="go"&gt;print(pickle.dumps($3, protocol=$2))&lt;/span&gt;
&lt;span class="go"&gt;EOD&lt;/span&gt;
&lt;span class="go"&gt;}&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;pickle&lt;span class="w"&gt; &lt;/span&gt;python3.6&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;[1, &amp;quot;abc&amp;quot;]&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;md5sum
&lt;span class="go"&gt;02fa88b9fea0912efe731ed56906b251  -&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;pickle&lt;span class="w"&gt; &lt;/span&gt;python3.7&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;[1, &amp;quot;abc&amp;quot;]&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;md5sum
&lt;span class="go"&gt;02fa88b9fea0912efe731ed56906b251  -&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;pickle&lt;span class="w"&gt; &lt;/span&gt;python3.8&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;[1, &amp;quot;abc&amp;quot;]&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;md5sum
&lt;span class="go"&gt;02fa88b9fea0912efe731ed56906b251  -&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;pickle&lt;span class="w"&gt; &lt;/span&gt;pypy3.6&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;[1, &amp;quot;abc&amp;quot;]&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;md5sum
&lt;span class="go"&gt;02fa88b9fea0912efe731ed56906b251  -&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;... until it doesn't:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;pickle&lt;span class="w"&gt; &lt;/span&gt;python3.6&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;datetime(1, 1, 1)&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;md5sum
&lt;span class="go"&gt;9c4423b791578d865d8fbeb070a1b934  -&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;pickle&lt;span class="w"&gt; &lt;/span&gt;pypy3.6&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;datetime(1, 1, 1)&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;md5sum
&lt;span class="go"&gt;3c7c834cb2f1cf4aba8be5c326bb9ddd  -&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Version 0 isn't stable either, but in a different way:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;pickle&lt;span class="w"&gt; &lt;/span&gt;python3.6&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;datetime(1, 1, 1)&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;md5sum
&lt;span class="go"&gt;01acd91b95556a09f5ff9b7495e120da  -&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;pickle&lt;span class="w"&gt; &lt;/span&gt;pypy3.6&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;datetime(1, 1, 1)&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;md5sum
&lt;span class="go"&gt;01acd91b95556a09f5ff9b7495e120da  -&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;pickle&lt;span class="w"&gt; &lt;/span&gt;python3.7&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;datetime(1, 1, 1)&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;md5sum
&lt;span class="go"&gt;a6c815eca494dbf716cd4a7e5556d779  -&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Version 3 does &lt;em&gt;seem&lt;/em&gt; to work fine across all of the above,
on both macOS and Linux (I also tested it with more complicated data).
But what's to say it'll remain that way?&lt;/p&gt;
&lt;p&gt;In fairness, &lt;em&gt;this is not an issue with pickle&lt;/em&gt; –
it guarantees you'll get the same object back after unpickling,
not that you'll get the same binary stream after pickling.&lt;/p&gt;
&lt;p&gt;And it's easy to explain why: pickles are actually &lt;em&gt;programs&lt;/em&gt;.
Some relevant and quite interesting comments from &lt;a class="external" href="https://github.com/python/cpython/blob/3.9/Lib/pickletools.py"&gt;pickletools&lt;/a&gt;:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&amp;quot;A pickle&amp;quot; is a program for a virtual pickle machine (PM, but more accurately
called an unpickling machine). It's a sequence of opcodes, interpreted by the
PM, building an arbitrarily complex Python object.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;Historically, many enhancements
have been made to the pickle protocol in order to do a better (faster,
and/or more compact) job on
[builtin scalar and container types, like ints and tuples].&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;As explained below [for compatibility],
pickle opcodes never go away, not even when better ways to do a thing
get invented. The repertoire of the PM just keeps growing over time.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;This means there can be multiple pickles
that unpickle to the same object&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;,
even within the bounds of a specific protocol version.&lt;/p&gt;
&lt;h3 id="str-and-repr-not-stable-not-safe"&gt;str() and repr(): not stable, not safe&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#str-and-repr-not-stable-not-safe" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/library/functions.html#func-str"&gt;str()&lt;/a&gt; and &lt;a class="external" href="https://docs.python.org/3/library/functions.html#repr"&gt;repr()&lt;/a&gt; might seem like valid solutions,
but neither of them are.&lt;/p&gt;
&lt;p&gt;First, they're not stable, and not guaranteed
to have all the information we may care about:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;str(object)&lt;/code&gt; returns &lt;code&gt;object.__str__()&lt;/code&gt;, which is the &amp;quot;informal&amp;quot; or nicely printable string representation of object. [...] If object does not have a &lt;code&gt;__str__()&lt;/code&gt; method, then &lt;code&gt;str()&lt;/code&gt; falls back to returning &lt;code&gt;repr(object)&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;blockquote&gt;
&lt;p&gt;For many types, [&lt;code&gt;repr()&lt;/code&gt;] makes an attempt to return a string that would yield an object with the same value when passed to &lt;code&gt;eval()&lt;/code&gt;, otherwise the representation is a string enclosed in angle brackets that contains the name of the type of the object together with additional information often including the name and address of the object.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;More importantly, they're not safe – &lt;em&gt;all&lt;/em&gt; Python objects have them,
even some that we would not want to serialize at all:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;object&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;span class="go"&gt;Content(value=&amp;lt;object object at 0x7f993cd5ff40&amp;gt;, language=None)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;lambda&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;Content(value=&amp;lt;function &amp;lt;lambda&amp;gt; at 0x7f993e96ec10&amp;gt;, language=None)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id="json"&gt;json 👍&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#json" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/library/json.html"&gt;json&lt;/a&gt; might be just what we need;
even the pickle documentation recommends it,
although for &lt;a class="external" href="https://docs.python.org/3/library/pickle.html#comparison-with-json"&gt;different reasons&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Compared to the previous solutions, json has the opposite problem:
it &lt;a class="external" href="https://docs.python.org/3/library/json.html#json.JSONEncoder"&gt;only supports&lt;/a&gt; dicts (with string/number keys only),
lists, strings, and a few other basic types.&lt;/p&gt;
&lt;p&gt;But that's not that big of an issue as you may think,
since the json module makes it really easy to support other types:
we just have to convert them to something it already understands.&lt;/p&gt;
&lt;p&gt;Let's start with datetimes:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;json_default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timespec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;microseconds&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;object of type &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; not serializable&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json_default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;&amp;quot;0001-01-01T00:00:00.000000&amp;quot;&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Dataclasses aren't much harder to add either, thanks to &lt;a class="external" href="https://docs.python.org/3/library/dataclasses.html#dataclasses.asdict"&gt;asdict()&lt;/a&gt;,
which converts them to dicts and recurses into any nested dataclasses,
dicts, lists and tuples along the way:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;json_default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;asdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;pass&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timespec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;microseconds&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;object of type &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; not serializable&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;value&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)]),&lt;/span&gt; &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json_default&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;{&amp;quot;id&amp;quot;: &amp;quot;id&amp;quot;, &amp;quot;updated&amp;quot;: &amp;quot;0001-01-01T00:00:00.000000&amp;quot;, &amp;quot;content&amp;quot;: [{&amp;quot;value&amp;quot;: &amp;quot;value&amp;quot;, &amp;quot;language&amp;quot;: null}]}&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You may notice the dataclass type does not appear in the result,
which for our use case is actually fine:
dataclasses &lt;code&gt;One(value=1)&lt;/code&gt; and &lt;code&gt;Two(value=1)&lt;/code&gt; will both get converted
to the dict &lt;code&gt;{'value': 1}&lt;/code&gt;, resulting in the same hash.
To make them different, we can include the type name:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;__type&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="vm"&gt;__name__&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="n"&gt;asdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;
&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;__type&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;Content&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;value&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;language&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To ensure the output remains stable across Python versions,
we'll force all of the &lt;a class="external" href="https://docs.python.org/3/library/json.html#json.dumps"&gt;dumps()&lt;/a&gt; default arguments to known values,
and require it to sort dict keys:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;json_dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;default&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;json_default&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;ensure_ascii&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;sort_keys&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;indent&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;separators&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;,&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;:&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;One more wrapper to hash the serialized value, and we're done:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;hashlib&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;md5&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;json_dumps&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;utf-8&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;get_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;value&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)]))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;78b4b8120af5f832b7ecfc34db1fe02b&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="problem-we-need-to-ignore-empty-values"&gt;Problem: we need to ignore empty values&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-we-need-to-ignore-empty-values" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Say we have a dataclass like the following:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;get_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;5d872de403edb944a7b10450eda2f46a&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Which in time evolves to get another attribute:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;
&lt;span class="gp"&gt;... &lt;/span&gt;    &lt;span class="n"&gt;another&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
&lt;span class="gp"&gt;...&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;get_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;e54ad2c961239bd70da9a603cd078e18&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The old and new versions result in different dicts,
so they have different hashes.&lt;/p&gt;
&lt;p&gt;But should they?
The only &amp;quot;real&amp;quot; data &lt;code&gt;Data(2)&lt;/code&gt; contains is &lt;code&gt;value=2&lt;/code&gt;.
Likewise, there's not much of a difference
in actual information between &lt;code&gt;None&lt;/code&gt; and &lt;code&gt;[]&lt;/code&gt;
(for our use case, at least).&lt;/p&gt;
&lt;p&gt;We can ignore &amp;quot;empty&amp;quot; values quite easily
by using the asdict() &lt;code&gt;dict_factory&lt;/code&gt; argument.
I overlooked it initially, thinking it has to be a mapping class;
it turns out any function that takes a key-value pair iterable works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;collections.abc&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;Collection&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;dict_drop_empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pairs&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;pairs&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
            &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;json_default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;asdict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dict_factory&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dict_drop_empty&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We now get the same hash as for the first version of the object:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;Data(value=2, another=None)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;get_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;5d872de403edb944a7b10450eda2f46a&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Note that we are specific about the &lt;em&gt;falsy&lt;/em&gt; values we ignore:
an empty string, list, or dict are empty, but &lt;code&gt;0&lt;/code&gt; or &lt;code&gt;False&lt;/code&gt; are not.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/library/collections.abc.html"&gt;collections.abc&lt;/a&gt; provides abstract base classes
that can be used to test whether a class provides a particular interface,
without requiring it to subclass anything.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;isinstance(v, (str, tuple, list, dict))&lt;/code&gt; works in our example,
but &lt;code&gt;isinstance(v, Collection)&lt;/code&gt; checks for other collections
we may have failed to think about that don't inherit from them;
for example: &lt;a class="external" href="https://docs.python.org/3/library/stdtypes.html#set-types-set-frozenset"&gt;sets&lt;/a&gt;, &lt;a class="external" href="https://docs.python.org/3/library/collections.html#collections.deque"&gt;deques&lt;/a&gt;, &lt;a class="external" href="https://numpy.org/doc/stable/reference/generated/numpy.ndarray.html"&gt;numpy arrays&lt;/a&gt;,
and even ones that don't exist yet.&lt;/p&gt;
&lt;/section&gt;
&lt;h2 id="problem-we-need-to-skip-some-fields"&gt;Problem: we need to skip some fields&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#problem-we-need-to-skip-some-fields" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Let's look at a more advanced version of our Entry class:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;feed_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;It the &lt;a class="external" href="https://reader.readthedocs.io/en/latest/guide.html#changing-feed-urls"&gt;feed URL changes&lt;/a&gt;, does the data of the entry change?
That is, would we want to update the entry next time we retrieve it?&lt;/p&gt;
&lt;p&gt;No, in our context, both &lt;code&gt;feed_url&lt;/code&gt; and &lt;code&gt;id&lt;/code&gt; are &lt;em&gt;metadata;&lt;/em&gt;
changing them should not change the hash,
since the actual data does not change.&lt;/p&gt;
&lt;p&gt;We could deal with it by changing json_default()
to remove specific keys from the asdict() result.&lt;/p&gt;
&lt;p&gt;However, this moves class-specific logic away from the class,
and into json_default(),
which forces us to change it whenever we add a new class, or
the list of metadata attributes changes.
Passing the list as an argument to get_hash() just moves the problem elsewhere.&lt;/p&gt;
&lt;p&gt;A better way is to allow classes to tell json_default()
what attributes to remove via a well-known class attribute.&lt;/p&gt;
&lt;p&gt;Neither of these approaches work with asdict() and nested dataclasses,
since asdict() is recursive, and we only get to look at the top-level class.
We cannot do anything in &lt;code&gt;dict_factory&lt;/code&gt; either,
since we don't know which class the attribute pairs belong to.&lt;/p&gt;
&lt;p&gt;To work around it, we'll implement a non-recursive version of asdict(),
and rely on json.dumps() for recursion (it's cool like that):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;dataclass_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;fields&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dataclasses&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;got dataclass type, expected instance&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;exclude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;_hash_exclude_&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;())&lt;/span&gt;

    &lt;span class="n"&gt;rv&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;fields&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;exclude&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;getattr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;Collection&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;
        &lt;span class="n"&gt;rv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;field&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;rv&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;json_default&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dataclass_dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;thing&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;We're using &lt;code&gt;_hash_exclude_&lt;/code&gt; (with an underscore at the end) to mimic
the &lt;code&gt;__double_underscore__&lt;/code&gt; convention Python uses for magic methods;
we are &lt;em&gt;not&lt;/em&gt; using double underscores because &lt;a class="external" href="https://docs.python.org/3/reference/lexical_analysis.html#reserved-classes-of-identifiers"&gt;they are reserved&lt;/a&gt; by Python.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;To exclude some fields from the hash,
we just need to set &lt;code&gt;_hash_exclude_&lt;/code&gt; to a tuple containing their names:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@dataclass&lt;/span&gt;
&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;feed_url&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="nb"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;
    &lt;span class="n"&gt;updated&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Optional&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;Content&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;_hash_exclude_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;feed_url&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;get_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;feed one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;4f97b1e8e99d3304f50cd3c4428f224e&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;get_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;feed two&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;4f97b1e8e99d3304f50cd3c4428f224e&amp;#39;&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;get_hash&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Entry&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;feed one&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;id&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hex&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="go"&gt;&amp;#39;0c739e158792c5a91ec1632804d905c1&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#conclusion" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;By leveraging &lt;a class="external" href="https://docs.python.org/3/library/dataclasses.html"&gt;dataclasses&lt;/a&gt; and the &lt;a class="external" href="https://docs.python.org/3/library/json.html"&gt;json&lt;/a&gt; module,
we've managed to get stable, deterministic hashes
for (almost) arbitrary Python data objects,
with a decent trade-off between generality and safety,
and two additional features,
&lt;em&gt;all in under 50 lines of code!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;It's true that the solution is pretty specific to my use case,
but with this little code, it should be trivial to adapt to something else.&lt;/p&gt;
&lt;p&gt;If you're interested in using it, have a look at:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;the &lt;a class="external" href="https://github.com/lemon24/reader/blob/1efcd38c78f70dcc4e0d279e0fa2a0276749111e/src/reader/_hash_utils.py"&gt;commented, type-annotated code&lt;/a&gt;, and&lt;/li&gt;
&lt;li&gt;the &lt;a class="external" href="https://github.com/lemon24/reader/blob/1efcd38c78f70dcc4e0d279e0fa2a0276749111e/tests/test_hash_utils.py"&gt;test suite&lt;/a&gt;.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/stable-hashing&amp;t=Deterministic%20hashing%20of%20Python%20data%20objects"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Deterministic%20hashing%20of%20Python%20data%20objects%20https%3A//death.andgravity.com/stable-hashing"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/stable-hashing&amp;title=Deterministic%20hashing%20of%20Python%20data%20objects"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/stable-hashing"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Deterministic%20hashing%20of%20Python%20data%20objects&amp;url=https%3A//death.andgravity.com/stable-hashing&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;I found this idea somewhere on Stack Overflow,
but I can't seem to find that specific post again. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/stable-hashing" rel="alternate"/>
    <summary>In this article, you'll learn how to calculate deterministic hashes for arbitrary Python data objects, stable across interpreter versions and implementations.</summary>
    <published>2021-03-19T17:25:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/aosa">
    <id>https://death.andgravity.com/aosa</id>
    <title>Struggling to structure code in larger programs? Great resources a beginner might not find so easily</title>
    <updated>2021-03-10T12:00:00+00:00</updated>
    <content type="html">&lt;p&gt;So, you're an advanced beginner –
you've learned your way past Python basics
and can solve real problems.&lt;/p&gt;
&lt;p&gt;Maybe you're about to embark on your first larger project,
but feel at a loss about where to start from,
and how to structure it,
and don't want to make any mistakes.&lt;/p&gt;
&lt;p&gt;Or you're midway through a large project already (not even your first),
but don't know how to make the modules work together;
what started as a pythonic script ended up as
not-so-beautiful modules and packages,
and &lt;em&gt;it all gets messy, so quickly&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Maybe you feel most tutorials and blog posts
go straight to &lt;em&gt;one solution to a simple, made-up problem&lt;/em&gt;,
without an in-depth look at alternatives.&lt;/p&gt;
&lt;p&gt;You've tried reading about architecture and design patterns,
but they seem &lt;em&gt;too abstract&lt;/em&gt;,
and you can't see how they apply to &lt;em&gt;your&lt;/em&gt; code.&lt;/p&gt;
&lt;p&gt;You may have heard that you have to develop an eye
by reading and writing a lot of code.
That there are so many Python projects on GitHub
– &amp;quot;just pick one you like and see how they did it&amp;quot;.&lt;/p&gt;
&lt;p&gt;But they're too large and you don't know &lt;em&gt;where to start&lt;/em&gt;, and even if you did,
from the code itself &lt;em&gt;it's not obvious&lt;/em&gt; what the design philosophy was
and what choices they considered before settling on an implementation.&lt;/p&gt;
&lt;p&gt;If any of this sounds familiar,
&lt;strong&gt;here's a few resources that might help&lt;/strong&gt;.&lt;/p&gt;
&lt;h2 id="the-architecture-of-open-source-applications"&gt;The Architecture of Open Source Applications&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-architecture-of-open-source-applications" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Remember reading code and the design decisions not being obvious?&lt;/p&gt;
&lt;p&gt;&lt;a class="external" href="http://aosabook.org/en/index.html#aosa2"&gt;The Architecture of Open Source Applications&lt;/a&gt;
is a whole book about exactly that
(two actually, since there are two volumes) – and it's free!&lt;/p&gt;
&lt;p&gt;From their website:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Architects look at thousands of buildings during their training, and study critiques of those buildings written by masters. In contrast, most software developers only ever get to know a handful of large programs well—usually programs they wrote themselves—and never study the great programs of history. As a result, they repeat one another's mistakes rather than building on one another's successes.&lt;/p&gt;
&lt;p&gt;Our goal is to change that. In these two books, the authors of four dozen open source applications explain how their software is structured, and why. What are each program's major components? How do they interact? And what did their builders learn during their development? In answering these questions, the contributors to these books provide unique insights into how they think.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;About &lt;strong&gt;a quarter of the chapters are about Python projects&lt;/strong&gt;;
you will encounter familiar names like
SQLAlchemy, PyPy, Twisted, matplotlib, and Mercurial.&lt;/p&gt;
&lt;p&gt;The discussions can be a bit high level,
since they're all mature and relatively large projects
(and there's only so much you can fit in a book chapter),
although some of them are more in-depth than others.&lt;/p&gt;
&lt;p&gt;You should have a look at the non-Python chapters as well
– there's a &lt;em&gt;lot&lt;/em&gt; of wisdom in there.
Also, at least part of the success of my last job interview
was due to having read the nginx chapter :)&lt;/p&gt;



&lt;div class="panel inline-panel" &gt;
    &lt;div class="panel-header text-large"&gt;
        Liking this so far? Here&amp;#39;s another article you might like:
    &lt;/div&gt;
    &lt;div class="panel-body"&gt;
        &lt;p&gt;&lt;a href="/stdlib"&gt;
            Learn by reading code: Python standard library design decisions explained
        &lt;/a&gt;
    &lt;/div&gt;
&lt;/div&gt;
&lt;h3 id="500-lines-or-less"&gt;500 Lines or Less&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#500-lines-or-less" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a class="external" href="http://aosabook.org/en/index.html#500lines"&gt;500 Lines or Less&lt;/a&gt; is another book from the
&lt;abbr title="Architecture of Open Source Applications"&gt;AOSA&lt;/abbr&gt; series.&lt;/p&gt;
&lt;p&gt;It aims to address the initial books being too high-level
by looking at purpose-made small projects
(again, focusing on design decisions).
From the introduction:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The first three volumes in the series were about big problems that big programs have to solve. For an engineer who is early in their career, it may be a challenge to understand and build upon programs that are much bigger than a few thousand lines of code, so, while big problems can be interesting to read about, they can also be challenging to learn from.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;And there's more better news: &lt;strong&gt;over half of the chapters are in Python&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;If you ever wondered how templating engines work,
give the &lt;a class="external" href="http://aosabook.org/en/500L/a-template-engine.html"&gt;A Template Engine&lt;/a&gt; chapter a read.
It will provide a gentle introduction to the problem
and examine a relatively simple solution
(but not that simple that it's not useful anymore –
that's actually the template engine &lt;a class="external" href="https://coverage.readthedocs.io/"&gt;coverage.py&lt;/a&gt; uses
to produce its HTML reports).&lt;/p&gt;
&lt;h2 id="jinja"&gt;Jinja&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#jinja" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;If the &lt;em&gt;500 Lines or Less&lt;/em&gt; &lt;a class="external" href="http://aosabook.org/en/500L/a-template-engine.html"&gt;template engine&lt;/a&gt; chapter got your interest,
you may want to take a look at &lt;a class="external" href="https://jinja.palletsprojects.com/"&gt;Jinja&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Jinja is one of the most used template engines for Python,
and if you used Flask, you've probably already used it.
It's been refined for over 14 years,
and like all the &lt;a class="external" href="https://palletsprojects.com/"&gt;Pallets projects&lt;/a&gt;, has a great API.&lt;/p&gt;
&lt;p&gt;I recommend you dive into the source starting from the &lt;a class="external" href="https://github.com/pallets/jinja/blob/2.11.3/src/jinja2/environment.py#L141"&gt;Environment&lt;/a&gt; class,
after reading the &lt;a class="external" href="https://jinja.palletsprojects.com/api/#basics"&gt;Basics&lt;/a&gt; and &lt;a class="external" href="https://jinja.palletsprojects.com/api/#high-level-api"&gt;High Level API&lt;/a&gt;
sections of the API documentation first.&lt;/p&gt;
&lt;p&gt;There are two talks given by Jinja's main author, Armin Ronacher,
to lead you on your journey:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a class="external" href="https://speakerdeck.com/mitsuhiko/code-generation-in-python-dismantling-jinja"&gt;Code Generation in Python: Dismantling Jinja&lt;/a&gt;
(&lt;a class="external" href="https://www.youtube.com/watch?v=jXlR0Icvvh8"&gt;recording&lt;/a&gt;)
walks through the design of Jinja's compiler infrastructure,
why it works the way it works,
and how it ended up where it is after many different iterations.&lt;/li&gt;
&lt;li&gt;&lt;a class="external" href="https://speakerdeck.com/mitsuhiko/lets-talk-about-templates"&gt;Let's Talk About Templates&lt;/a&gt;
(&lt;a class="external" href="https://www.youtube.com/watch?v=rHmljD-oZrY"&gt;recording&lt;/a&gt;)
compares Jinja and Django's templates,
looking at how their different histories and constraints
led to vastly different internal designs.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;You can find all of Armin's talks
&lt;a class="external" href="https://lucumr.pocoo.org/talks/"&gt;here&lt;/a&gt;.
&lt;em&gt;Letters from the Battlefield&lt;/em&gt; and &lt;em&gt;Good API Design&lt;/em&gt;
are especially nice higher-level design lessons,
with examples from the Flask and Jinja world.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;That's it for now.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/aosa&amp;t=Struggling%20to%20structure%20code%20in%20larger%20programs%3F%20Great%20resources%20a%20beginner%20might%20not%20find%20so%20easily"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Struggling%20to%20structure%20code%20in%20larger%20programs%3F%20Great%20resources%20a%20beginner%20might%20not%20find%20so%20easily%20https%3A//death.andgravity.com/aosa"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/aosa&amp;title=Struggling%20to%20structure%20code%20in%20larger%20programs%3F%20Great%20resources%20a%20beginner%20might%20not%20find%20so%20easily"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/aosa"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Struggling%20to%20structure%20code%20in%20larger%20programs%3F%20Great%20resources%20a%20beginner%20might%20not%20find%20so%20easily&amp;url=https%3A//death.andgravity.com/aosa&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


</content>
    <link href="https://death.andgravity.com/aosa" rel="alternate"/>
    <summary>Are you having trouble making the modules work together in a larger project? Have you tried looking at popular projects as models, but were put off by their size and scope, or found it hard to see why they did the things they did? Resources about this do exist, but they're scattered all over, and might be hard to find for someone early in their programming journey.</summary>
    <published>2021-03-10T12:00:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/fast-conway-cubes">
    <id>https://death.andgravity.com/fast-conway-cubes</id>
    <title>Optimizing Advent of Code 2020 day 17</title>
    <updated>2021-02-08T21:30:00+00:00</updated>
    <content type="html">&lt;p&gt;... in which we optimize
&lt;a class="internal" href="/conway-cubes"&gt;our Advent of Code 2020 day 17 solution&lt;/a&gt;,
a Python implementation of multidimensional &lt;a class="external" href="https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life"&gt;Game of Life&lt;/a&gt;,
to end up with a &lt;strong&gt;65x improvement&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;We will focus on profiling and optimizing the existing code, in a way
that helps you translate those skills to your regular, non-puzzle coding.&lt;/p&gt;
&lt;p&gt;We'll start from &lt;a class="attachment" href="/_file/fast-conway-cubes/00-begin/conway_cubes.py"&gt;the script&lt;/a&gt; as we left it in the initial article,
and check we didn't break anything using &lt;a class="attachment" href="/_file/fast-conway-cubes/00-begin/test_conway_cubes.py"&gt;the tests&lt;/a&gt; we already wrote.&lt;/p&gt;
&lt;details class="toc"&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#why-is-it-slow"&gt;Why is it slow?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#intro-to-profiling"&gt;Intro to profiling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#worst-offenders"&gt;Worst offenders&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#make-directions"&gt;make_directions()&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#any-coord-0"&gt;any(coord &amp;lt; 0 ...)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#neighbor-coords"&gt;neighbor_coords&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#going-multidimensional-again"&gt;Going multidimensional, again&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-real-world"&gt;The real world&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#bonus-pypy"&gt;Bonus: PyPy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#conclusion"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="why-is-it-slow"&gt;Why is it slow?&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#why-is-it-slow" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Our solution is pretty &lt;a class="internal" href="/conway-cubes#simulation"&gt;naive&lt;/a&gt;:
for each cell, count how many of the cell's neighbors are active,
and change the cell state based on that;
see &lt;a class="internal" href="/conway-cubes#the-problem"&gt;this&lt;/a&gt;
for a detailed explanation of the rules.&lt;/p&gt;
&lt;p&gt;As we add more dimensions, the run time increases by orders of magnitude;
for a world of size 16, I get:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;2D: .02 seconds&lt;/li&gt;
&lt;li&gt;3D: 1 second&lt;/li&gt;
&lt;li&gt;4D: 1 minute&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The same happens when we increase the world size:
in 4D,
a world of size 20 doesn't take only 1.25 times longer than a size 16 world,
but 2.4 times!&lt;/p&gt;
&lt;p&gt;To get a better picture of why this is happening,
let's count how many cells and and neighbors we need to look at every cycle
(as a reminder, neighbors are all the cells in a size 3 &amp;quot;square&amp;quot;
centered on the cell, except the cell itself):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;dims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;(256, 2048)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;dims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;(4096, 106496)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;dims&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;(65536, 5242880)&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;dims&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="go"&gt;(160000, 12800000)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That is indeed &lt;a class="external" href="https://en.wikipedia.org/wiki/Exponential_growth"&gt;exponential growth&lt;/a&gt; (the number of dimensions being the exponent).&lt;/p&gt;
&lt;p&gt;As I mentioned before, there are &lt;a class="external" href="https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life#Algorithms"&gt;many optimizations&lt;/a&gt; to simulating Life.
They usually involve one or more of:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reducing the number of cells to look at&lt;/li&gt;
&lt;li&gt;making neighbors faster to count&lt;/li&gt;
&lt;li&gt;detecting parts of the board that repeat either in space or time,
and reusing the previous results&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We won't change our naive algorithm;
instead, we'll try to make our existing Python code faster,
since it is both easier to do (at least initially),
and easier to translate to other Python problems.&lt;/p&gt;
&lt;p&gt;(If we were after speed at any cost,
we'd probably both use better algorithms,
and switch to a faster language.)&lt;/p&gt;
&lt;h2 id="intro-to-profiling"&gt;Intro to profiling&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#intro-to-profiling" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The Python standard library provides a &lt;a class="external" href="https://docs.python.org/3/library/profile.html"&gt;profiler&lt;/a&gt;
which allows getting statistics for how often and how long
various functions get executed.&lt;/p&gt;
&lt;p&gt;For us, the easiest way to use it is to pass a whole script, like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;python3&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;cProfile&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;scriptfile&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[&lt;/span&gt;arg&lt;span class="o"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;...
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;You can also profile specific bits of code;
see &lt;a class="external" href="https://docs.python.org/3/library/profile.html#profile.Profile"&gt;this&lt;/a&gt;
for details.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;Before profiling, we get a baseline run time for the &amp;quot;real&amp;quot; workload:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;conway_cubes.py&lt;span class="w"&gt; &lt;/span&gt;real&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;
&lt;span class="go"&gt;after cycle #0 (0.01s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #1 (24.15s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #2 (23.45s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #3 (23.79s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #4 (24.31s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #5 (24.15s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #6 (24.07s): ...&lt;/span&gt;
&lt;span class="go"&gt;the result is 2276 (143.94s)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;While working on the script,
we'll simulate a smaller world for just one cycle,
so we can iterate quickly.
We get a baseline for that as well:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;conway_cubes.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="go"&gt;after cycle #0 (0.00s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #1 (0.65s): ...&lt;/span&gt;
&lt;span class="go"&gt;the result is 29 (0.65s)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Let's run it through the profiler:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;cProfile&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;cumulative&lt;span class="w"&gt; &lt;/span&gt;conway_cubes.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;test&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;8&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;span class="go"&gt;after cycle #0 (0.00s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #1 (1.01s): ...&lt;/span&gt;
&lt;span class="go"&gt;the result is 29 (1.01s)&lt;/span&gt;

&lt;span class="go"&gt;         2268874 function calls (2241818 primitive calls) in 1.020 seconds&lt;/span&gt;

&lt;span class="go"&gt;   Ordered by: cumulative time&lt;/span&gt;

&lt;span class="go"&gt;   ncalls  tottime  percall  cumtime  percall filename:lineno(function)&lt;/span&gt;
&lt;span class="go"&gt;        1    0.000    0.000    1.020    1.020 {built-in method builtins.exec}&lt;/span&gt;
&lt;span class="go"&gt;        1    0.000    0.000    1.020    1.020 conway_cubes.py:1(&amp;lt;module&amp;gt;)&lt;/span&gt;
&lt;span class="go"&gt;        1    0.000    0.000    1.020    1.020 conway_cubes.py:162(main)&lt;/span&gt;
&lt;span class="go"&gt;        1    0.000    0.000    1.020    1.020 conway_cubes.py:120(run)&lt;/span&gt;
&lt;span class="go"&gt;        3    0.000    0.000    1.013    0.338 conway_cubes.py:104(simulate_forever)&lt;/span&gt;
&lt;span class="go"&gt;        1    0.005    0.005    1.013    1.013 conway_cubes.py:74(simulate)&lt;/span&gt;
&lt;span class="go"&gt;     4096    0.391    0.000    1.001    0.000 conway_cubes.py:28(get_active_neighbors)&lt;/span&gt;
&lt;span class="go"&gt;   327680    0.216    0.000    0.396    0.000 {built-in method builtins.any}&lt;/span&gt;
&lt;span class="go"&gt;  1557736    0.188    0.000    0.188    0.000 conway_cubes.py:37(&amp;lt;genexpr&amp;gt;)&lt;/span&gt;
&lt;span class="go"&gt;   327680    0.143    0.000    0.143    0.000 conway_cubes.py:32(&amp;lt;listcomp&amp;gt;)&lt;/span&gt;
&lt;span class="go"&gt;     4096    0.005    0.000    0.063    0.000 conway_cubes.py:23(make_directions)&lt;/span&gt;
&lt;span class="go"&gt;     4096    0.058    0.000    0.058    0.000 conway_cubes.py:25(&amp;lt;listcomp&amp;gt;)&lt;/span&gt;
&lt;span class="go"&gt;33938/8194    0.011    0.000    0.011    0.000 conway_cubes.py:56(ndenumerate)&lt;/span&gt;
&lt;span class="go"&gt;        1    0.001    0.001    0.006    0.006 {built-in method builtins.sum}&lt;/span&gt;
&lt;span class="go"&gt;     4097    0.001    0.000    0.006    0.000 conway_cubes.py:138(&amp;lt;genexpr&amp;gt;)&lt;/span&gt;
&lt;span class="go"&gt;   1170/2    0.001    0.000    0.001    0.000 conway_cubes.py:89(make_world)&lt;/span&gt;
&lt;span class="go"&gt;    146/2    0.000    0.000    0.001    0.000 conway_cubes.py:93(&amp;lt;listcomp&amp;gt;)&lt;/span&gt;
&lt;span class="go"&gt;     4100    0.001    0.000    0.001    0.000 {built-in method builtins.len}&lt;/span&gt;
&lt;span class="go"&gt;        6    0.000    0.000    0.000    0.000 {built-in method builtins.print}&lt;/span&gt;
&lt;span class="go"&gt;        1    0.000    0.000    0.000    0.000 conway_cubes.py:8(parse_input)&lt;/span&gt;
&lt;span class="go"&gt;        1    0.000    0.000    0.000    0.000 conway_cubes.py:96(copy_centered_2d)&lt;/span&gt;
&lt;span class="go"&gt;        1    0.000    0.000    0.000    0.000 conway_cubes.py:9(&amp;lt;listcomp&amp;gt;)&lt;/span&gt;
&lt;span class="go"&gt;       10    0.000    0.000    0.000    0.000 {built-in method builtins.isinstance}&lt;/span&gt;
&lt;span class="go"&gt;        5    0.000    0.000    0.000    0.000 {built-in method time.perf_counter}&lt;/span&gt;
&lt;span class="go"&gt;        3    0.000    0.000    0.000    0.000 conway_cubes.py:10(&amp;lt;listcomp&amp;gt;)&lt;/span&gt;
&lt;span class="go"&gt;        1    0.000    0.000    0.000    0.000 conway_cubes.py:6(&amp;lt;dictcomp&amp;gt;)&lt;/span&gt;
&lt;span class="go"&gt;        1    0.000    0.000    0.000    0.000 {method &amp;#39;splitlines&amp;#39; of &amp;#39;str&amp;#39; objects}&lt;/span&gt;
&lt;span class="go"&gt;        1    0.000    0.000    0.000    0.000 {method &amp;#39;disable&amp;#39; of &amp;#39;_lsprof.Profiler&amp;#39; objects}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;After the script finishes,
the profiler prints the number of calls and run times for each function.
We're interested in two columns:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;cumtime&lt;/code&gt;, &amp;quot;the cumulative time spent in this and all subfunctions&amp;quot;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;tottime&lt;/code&gt;, &amp;quot;the total time spent in the given function&amp;quot; (excluding sub-functions)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By default, the results are sorted by function name, which isn't very useful;
we use use the &lt;code&gt;-s&lt;/code&gt; option to sort by cumulative time.&lt;/p&gt;
&lt;p&gt;Since the output is quite long, from now on
I'll just show the relevant rows in the middle of the table.&lt;/p&gt;
&lt;p&gt;You may notice the run time increased;
that's because profiling adds some overhead.
We are using the &lt;code&gt;cProfile&lt;/code&gt; module, a C implementation of the profiler;
if you try the pure-Python version, &lt;code&gt;profile&lt;/code&gt;,
it'll take even more, around 25x on my machine.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;If you're following along, you might find it useful
to re-run the command automatically every time you save the file.&lt;/p&gt;
&lt;p&gt;I used &lt;a class="external" href="https://eradman.com/entrproject/"&gt;entr&lt;/a&gt; to do it:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;conway_cubes.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;entr&lt;span class="w"&gt; &lt;/span&gt;-rcs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="s2"&gt;python3 -m cProfile -s cumulative conway_cubes.py test 8 4 1 \&lt;/span&gt;
&lt;span class="s2"&gt;| grep -A20 ncalls&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;h2 id="worst-offenders"&gt;Worst offenders&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#worst-offenders" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Looking at the data, we see that almost the whole 1 second run time
is spent in &lt;code&gt;get_active_neighbors()&lt;/code&gt; and subfunctions,
which is consistent with our initial calculation.&lt;/p&gt;
&lt;p&gt;Let's see it in full:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;
&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;
&lt;span class="normal"&gt;52&lt;/span&gt;
&lt;span class="normal"&gt;53&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_active_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;offsets&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;make_directions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;

        &lt;span class="n"&gt;neighbor_coords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;coord&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;coord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offsets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coord&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;coord&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;neighbor_coords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;active on edge: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;neighbor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;coord&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;neighbor_coords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;neighbor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;neighbor&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;coord&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;IndexError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;active on edge: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;neighbor&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;active_neighbors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Of the total time, about .4s are spent in the function itself (&lt;code&gt;tottime&lt;/code&gt;),
and the rest in subfunctions:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;.4s in the &lt;code&gt;any(coord &amp;lt; 0 ...)&lt;/code&gt; check, .2s of which in the generator expression&lt;/li&gt;
&lt;li&gt;.15s in the &lt;code&gt;neighbor_coords&lt;/code&gt; list comprehension&lt;/li&gt;
&lt;li&gt;.06s in &lt;code&gt;make_directions()&lt;/code&gt;, almost all of it in the list comprehension&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;(Comprehensions and generator expressions are treated as functions as well.)&lt;/p&gt;
&lt;h2 id="make-directions"&gt;make_directions()&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#make-directions" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Let's start small.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;make_directions()&lt;/code&gt; only takes 6% of the total time,
but should be quite easy to speed up –
it is a pure function (the result only depends on the arguments),
and has a single argument with only a handful of values (2, 3, 4).&lt;/p&gt;
&lt;p&gt;We could pre-compute the results for each dimension,
save them in a global dict, and reuse them from there.&lt;/p&gt;
&lt;p&gt;Turns out, the &lt;a class="external" href="https://docs.python.org/3/library/functools.html"&gt;functools.lru_cache&lt;/a&gt; decorator from the standard library
allows us to do just that in a transparent way:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;2&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;functools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;lru_cache&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@lru_cache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;make_directions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Now, &lt;code&gt;make_directions()&lt;/code&gt; will save the return value
for a specific argument on the first call,
and subsequent calls with the same argument
will return the already computed value.&lt;/p&gt;
&lt;p&gt;Here's the result:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     4096    0.390    0.000    0.940    0.000 conway_cubes.py:30(get_active_neighbors)
      ...
        1    0.000    0.000    0.000    0.000 conway_cubes.py:24(make_directions)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Not bad, for this little work.&lt;/p&gt;
&lt;h2 id="any-coord-0"&gt;any(coord &amp;lt; 0 ...)&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#any-coord-0" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Emboldened by this momentous achievement,
we'll go straight to the &lt;code&gt;any(coord &amp;lt; 0 ...)&lt;/code&gt; check.&lt;/p&gt;
&lt;p&gt;There's more than one way to approach it,
but before exploring any of them,
let's look a bit harder at &lt;code&gt;get_active_neighbors()&lt;/code&gt;:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;for every neighbor, we're checking if any of its coordinates is &amp;lt; 0;&lt;/li&gt;
&lt;li&gt;but by definition, the lowest a neighbor can be is -1 from the cell;&lt;/li&gt;
&lt;li&gt;so that's equivalent to checking that the cell coordinate is &amp;lt; 1;&lt;/li&gt;
&lt;li&gt;since the cell isn't moving, we can do it just once, outside the neighbor loop.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It might not seem like a lot,
but remember the neighbor count increases exponentially:
in 2D, we're doing the check 8 times;
in 4D, we're doing it 80 times!&lt;/p&gt;
&lt;p&gt;So:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_active_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coord&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;coord&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;active on edge: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Which gives us a 57% improvement!&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     4096    0.265    0.000    0.406    0.000 conway_cubes.py:30(get_active_neighbors)
      ...
     4096    0.003    0.000    0.005    0.000 {built-in method builtins.any}
    17656    0.003    0.000    0.003    0.000 conway_cubes.py:31(&amp;lt;genexpr&amp;gt;)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There's a slight issue, though:
in the original version, if the check failed, we'd skip that neighbor
(assume it's not active); now we're not doing that anymore.&lt;/p&gt;
&lt;p&gt;This means that for a cell on the top/left/... edge of the world (index 0),
we will be getting the state for its neighbors at the far end (index -1).&lt;/p&gt;
&lt;p&gt;As long as the neighbors on the far end are inactive, it will still work;
thankfully, we are checking that as well –
that's what the &lt;code&gt;if active&lt;/code&gt; in &lt;code&gt;except IndexError&lt;/code&gt; does.&lt;/p&gt;
&lt;p&gt;But now we've made a bit of logic dependent on another that's quite far from it.
Instead of just reasoning through it every time we change something,
we rely on &lt;a class="internal" href="/conway-cubes#bonus-more-tests"&gt;the edge case tests&lt;/a&gt;
to verify it for us (nothing to do, since we've already written them :).&lt;/p&gt;
&lt;p&gt;&lt;a class="attachment" href="/_file/fast-conway-cubes/10-any-coord-0/conway_cubes.py"&gt;The script up to this point.&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="neighbor-coords"&gt;neighbor_coords&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#neighbor-coords" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Next up is the &lt;code&gt;neighbor_coords&lt;/code&gt; list comprehension.&lt;/p&gt;
&lt;p&gt;Using functions written in C may remove some of the comprehension overhead;
let's see if it works:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;38&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="n"&gt;neighbor_coords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offsets&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;It's not much better:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     4096    0.376    0.000    0.382    0.000 conway_cubes.py:30(get_active_neighbors)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What if instead of using sum, which is generic to any iterable,
we used a function that's made specifically for 2 numbers?&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;operator&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;add&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;itertools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;starmap&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;41&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="n"&gt;neighbor_coords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;starmap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;add&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offsets&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;This fares slighly better, with a 26% improvement:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     4096    0.296    0.000    0.301    0.000 conway_cubes.py:33(get_active_neighbors)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We've now exhausted all the obvious things to improve;
most of the time is spent in &lt;code&gt;get_active_neighbors&lt;/code&gt;,
not its subfunctions.&lt;/p&gt;
&lt;p&gt;You may remember the &lt;code&gt;any()&lt;/code&gt; call spent almost as much time
in the function itself (&lt;code&gt;tottime&lt;/code&gt;) as in the generator expression;
indeed, calling &lt;em&gt;any&lt;/em&gt; function seems to have significant overhead:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;timeit&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;from operator import add&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;add(1, 2)&amp;#39;&lt;/span&gt;
&lt;span class="go"&gt;5000000 loops, best of 5: 49.3 nsec per loop&lt;/span&gt;
&lt;span class="gp"&gt;$ &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;timeit&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;1 + 2&amp;#39;&lt;/span&gt;
&lt;span class="go"&gt;50000000 loops, best of 5: 7.98 nsec per loop&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Let's try something different.&lt;/p&gt;
&lt;p&gt;In the &lt;code&gt;for offsets ...&lt;/code&gt; loop, we're calling 3 functions to add 4 pairs of numbers.
What if we didn't?
Having more general code did help with testing,
but we may be reaching a point where it's not worth it anymore.&lt;/p&gt;
&lt;p&gt;We can validate it with a quick experiment
(this will break non-4D temporarily):&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="n"&gt;neighbor_coords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;offsets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;offsets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;offsets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;offsets&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Which gives us a 49% improvement!&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     4096    0.202    0.000    0.207    0.000 conway_cubes.py:30(get_active_neighbors)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;What if make everything non-generic, and get rid of intermediary variables?&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_active_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;c0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c3&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;c0&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;c1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;c2&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;c3&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;active on edge: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;o0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;o3&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;make_directions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;
        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;o0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;c0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;o1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;c1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;o2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;c2&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;o3&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;c3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;IndexError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;active on edge: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;active_neighbors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;That yields a 76% improvement!&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     4096    0.095    0.000    0.095    0.000 conway_cubes.py:30(get_active_neighbors)
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="going-multidimensional-again"&gt;Going multidimensional, again&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#going-multidimensional-again" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;At this point, we could be OK with part of the code not being generic anymore,
implement one &lt;code&gt;get_active_neighbors()&lt;/code&gt; per dimension, and use them like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;GET_ACTIVE_NEIGHBORS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;get_active_neighbors_2d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;get_active_neighbors_3d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;get_active_neighbors_4d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_active_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;GET_ACTIVE_NEIGHBORS&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;)](&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This reminds me of the pattern initially proposed for &lt;code&gt;make_directions()&lt;/code&gt;...
If only there was a way to do the same thing for a function.&lt;/p&gt;
&lt;p&gt;Well, this is Python, we &lt;em&gt;can&lt;/em&gt; generate code at runtime.
&lt;em&gt;Let's do something stupid:&lt;/em&gt;&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;3&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;textwrap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;
&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;
&lt;span class="normal"&gt;52&lt;/span&gt;
&lt;span class="normal"&gt;53&lt;/span&gt;
&lt;span class="normal"&gt;54&lt;/span&gt;
&lt;span class="normal"&gt;55&lt;/span&gt;
&lt;span class="normal"&gt;56&lt;/span&gt;
&lt;span class="normal"&gt;57&lt;/span&gt;
&lt;span class="normal"&gt;58&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;make_get_active_neighbors_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ids&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="s2"&gt;        def get_active_neighbors(world, active, coords):&lt;/span&gt;
&lt;span class="s2"&gt;            &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;, &amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;c&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; = coords&lt;/span&gt;

&lt;span class="s2"&gt;            if &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39; or &amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;c&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt; &amp;lt; 1&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;
&lt;span class="s2"&gt;                if active:&lt;/span&gt;
&lt;span class="s2"&gt;                    raise RuntimeError(f&amp;quot;active on edge: &lt;/span&gt;&lt;span class="se"&gt;{{&lt;/span&gt;&lt;span class="s2"&gt;coords&lt;/span&gt;&lt;span class="se"&gt;}}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;)&lt;/span&gt;

&lt;span class="s2"&gt;            active_neighbors = 0&lt;/span&gt;
&lt;span class="s2"&gt;            for &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;, &amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;o&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; in &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;make_directions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;
&lt;span class="s2"&gt;                try:&lt;/span&gt;
&lt;span class="s2"&gt;                    active_neighbors += world[&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;][&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;o&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt; + c&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ow"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ids&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;]&lt;/span&gt;
&lt;span class="s2"&gt;                except IndexError:&lt;/span&gt;
&lt;span class="s2"&gt;                    if active:&lt;/span&gt;
&lt;span class="s2"&gt;                        raise RuntimeError(f&amp;quot;active on edge: &lt;/span&gt;&lt;span class="se"&gt;{{&lt;/span&gt;&lt;span class="s2"&gt;coords&lt;/span&gt;&lt;span class="se"&gt;}}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;)&lt;/span&gt;

&lt;span class="s2"&gt;            return active_neighbors&lt;/span&gt;

&lt;span class="s2"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nd"&gt;@lru_cache&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;make_get_active_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;context&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{}&lt;/span&gt;
    &lt;span class="n"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;make_get_active_neighbors_str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;context&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;get_active_neighbors&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;We build the string with the source code of a function,
and then use &lt;a class="external" href="https://docs.python.org/3/library/functions.html#exec"&gt;exec&lt;/a&gt; to execute it in private global context.
We're using an explicit context to avoid polluting module globals
– everything that gets defined in the source code string is a &lt;code&gt;context&lt;/code&gt; item.&lt;/p&gt;
&lt;p&gt;You may notice that we're embedding the representation of the directions list
directly in the code, instead of calling &lt;code&gt;make_directions()&lt;/code&gt;;
I'm not sure this brings a great speed-up, but it can't hurt.&lt;/p&gt;
&lt;section class="admonition attention"&gt;
&lt;p class="admonition-title"&gt;Attention&lt;/p&gt;
&lt;p&gt;Never use &lt;a class="external" href="https://docs.python.org/3/library/functions.html#exec"&gt;exec&lt;/a&gt;/eval with untrusted code,
unless you want to get hacked;
&lt;a class="external" href="https://nedbatchelder.com/blog/201206/eval_really_is_dangerous.html"&gt;details&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Security issues aside, it makes code way harder to understand,
and breaks a lot of conventions about where classes and functions come from;
&lt;a class="external" href="https://lucumr.pocoo.org/2011/2/1/exec-in-python/"&gt;more details&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We're doing it here for ... didactic purposes.
And speed. Mostly speed.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;Before we try it out, we need to pull the dimension counting heuristic
from &lt;code&gt;ndenumerate()&lt;/code&gt; into a separate function:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;61&lt;/span&gt;
&lt;span class="normal"&gt;62&lt;/span&gt;
&lt;span class="normal"&gt;63&lt;/span&gt;
&lt;span class="normal"&gt;64&lt;/span&gt;
&lt;span class="normal"&gt;65&lt;/span&gt;
&lt;span class="normal"&gt;66&lt;/span&gt;
&lt;span class="normal"&gt;67&lt;/span&gt;
&lt;span class="normal"&gt;68&lt;/span&gt;
&lt;span class="normal"&gt;69&lt;/span&gt;
&lt;span class="normal"&gt;70&lt;/span&gt;
&lt;span class="normal"&gt;71&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;guess_dimensions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
        &lt;span class="n"&gt;world&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ndenumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;guess_dimensions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;... so we can also use it in &lt;code&gt;simulate()&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;81&lt;/span&gt;
&lt;span class="normal"&gt;82&lt;/span&gt;
&lt;span class="normal"&gt;83&lt;/span&gt;
&lt;span class="normal"&gt;84&lt;/span&gt;
&lt;span class="normal"&gt;85&lt;/span&gt;
&lt;span class="normal"&gt;86&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;guess_dimensions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;get_active_neighbors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_get_active_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ndenumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;... which leaves us with something slighly faster than our initial experiment:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
     4096    0.091    0.000    0.091    0.000 &amp;lt;string&amp;gt;:3(get_active_neighbors)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;a class="attachment" href="/_file/fast-conway-cubes/99-end/conway_cubes.py"&gt;The final version of the script.&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="the-real-world"&gt;The real world&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-real-world" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;So, with our &lt;code&gt;test 8 4 1&lt;/code&gt; profiling parameters,
we got a 91% (~11x) improvement in &lt;code&gt;get_active_neighbors()&lt;/code&gt; cumulative time.&lt;/p&gt;
&lt;p&gt;Does this reflect in the real-world performance?&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;conway_cubes.py&lt;span class="w"&gt; &lt;/span&gt;real&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;
&lt;span class="go"&gt;after cycle #0 (0.01s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #1 (3.39s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #2 (3.43s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #3 (3.34s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #4 (3.34s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #5 (3.34s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #6 (3.35s): ...&lt;/span&gt;
&lt;span class="go"&gt;the result is 2276 (20.21s)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Almost. That's still an 86% (~7x) improvement.&lt;/p&gt;
&lt;h2 id="bonus-pypy"&gt;Bonus: PyPy&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bonus-pypy" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;a class="external" href="https://www.pypy.org/"&gt;PyPy&lt;/a&gt; is an alternative Python implementation that's
often faster than CPython (the standard implementation)
due to its use of &lt;a class="external" href="https://en.wikipedia.org/wiki/Just-in-time_compilation"&gt;just-in-time compilation&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Currently, it works mostly out of the box for Python code up to version 3.7
(with the exception of some CPython extensions).&lt;/p&gt;
&lt;p&gt;First, let's see how it performs on the unoptimized script:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;pypy3&lt;span class="w"&gt; &lt;/span&gt;conway_cubes.py&lt;span class="w"&gt; &lt;/span&gt;real&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;
&lt;span class="go"&gt;after cycle #0 (0.02s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #1 (6.43s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #2 (5.53s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #3 (5.47s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #4 (5.45s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #5 (5.47s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #6 (5.46s): ...&lt;/span&gt;
&lt;span class="go"&gt;the result is 2276 (33.82s)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;4.2x; not bad, for essentially zero work!
Funnily enough, their website says that
&amp;quot;on average, PyPy is 4.2 times faster than CPython&amp;quot;.&lt;/p&gt;
&lt;p&gt;So, was it all for nothing, could have we just used PyPy from the start?
Yes and no, but mostly no:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;pypy3&lt;span class="w"&gt; &lt;/span&gt;conway_cubes.py&lt;span class="w"&gt; &lt;/span&gt;real&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;
&lt;span class="go"&gt;after cycle #0 (0.02s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #1 (0.52s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #2 (0.33s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #3 (0.33s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #4 (0.33s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #5 (0.34s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #6 (0.33s): ...&lt;/span&gt;
&lt;span class="go"&gt;the result is 2276 (2.21s)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That is:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;a 10x improvement over the same script run with CPython,&lt;/li&gt;
&lt;li&gt;4.2x over the unoptimized script with PyPy, and&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;65x&lt;/strong&gt; over the unoptimized script with CPython!&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#conclusion" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;&lt;a class="external" href="https://wiki.c2.com/?ProfileBeforeOptimizing"&gt;Profile before optimizing&lt;/a&gt;.&lt;/strong&gt;
Most often, your intuition about where the code is slow is wrong.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Many small optimizations add up.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;A different algorithm can be better than many small optimizations.&lt;/strong&gt;
At some point, there are no other small optimization left.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Optimizations have costs.&lt;/strong&gt;
Usually, they make code harder to understand.
More changes increases the likelihood of bugs.
More changes on harder to understand code even more so.
Tests help you know you're not breaking anything.
Profiling helps minimize the amount of code you change.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;&lt;a class="external" href="https://docs.python.org/3/faq/programming.html#my-program-is-too-slow-how-do-i-speed-it-up"&gt;My program is too slow. How do I speed it up?&lt;/a&gt;
from the Python Programming FAQ has more, better advice
on the points above.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;&lt;strong&gt;&lt;a class="external" href="https://www.pypy.org/"&gt;PyPy&lt;/a&gt; is amazing&lt;/strong&gt;, give it a try if you can.&lt;/p&gt;
&lt;p&gt;Function calls are slow in Python;
that only matters if you're doing it millions of times.&lt;/p&gt;
&lt;p&gt;You can do cool stuff with Python code generation;
most of the time, it's not worth it.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/fast-conway-cubes&amp;t=Optimizing%20Advent%20of%20Code%202020%20day%2017"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Optimizing%20Advent%20of%20Code%202020%20day%2017%20https%3A//death.andgravity.com/fast-conway-cubes"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/fast-conway-cubes&amp;title=Optimizing%20Advent%20of%20Code%202020%20day%2017"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/fast-conway-cubes"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Optimizing%20Advent%20of%20Code%202020%20day%2017&amp;url=https%3A//death.andgravity.com/fast-conway-cubes&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


</content>
    <link href="https://death.andgravity.com/fast-conway-cubes" rel="alternate"/>
    <summary>... in which we optimize our Advent of Code 2020 day 17 (Conway Cubes) solution, focusing on profiling and optimizing existing code, in a way that helps you translate those skills to your regular, non-puzzle coding. With a touch of code generation and some help from PyPy, we end up with a 65x improvement.</summary>
    <published>2021-02-08T16:20:00+00:00</published>
  </entry>
  <entry xml:base="https://death.andgravity.com/conway-cubes">
    <id>https://death.andgravity.com/conway-cubes</id>
    <title>Solving Advent of Code 2020 day 17 by not solving it</title>
    <updated>2021-02-08T16:00:00+00:00</updated>
    <content type="html">&lt;p&gt;... in which we take a look at &lt;a class="external" href="https://adventofcode.com/2020/day/17"&gt;the day 17 problem from
Advent of Code 2020&lt;/a&gt;, Conway Cubes.
We will end up solving it &lt;em&gt;eventually&lt;/em&gt;,
but in a very roundabout way; you will see why &lt;a class="anchor" href="#the-problem"&gt;shortly&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Along the way, we will:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;test and refactor,&lt;/li&gt;
&lt;li&gt;avoid hard problems, and&lt;/li&gt;
&lt;li&gt;write some idiomatic Python, from scratch.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;There are both shorter and faster ways of solving this,
but we'll take the scenic route, in a way that helps you
translate those skills to your regular, non-puzzle coding.&lt;/p&gt;
&lt;p&gt;You can read this without having solved the problem,
but to get the most from it, I recommend you do it on your own first,
and then follow along as we do it again here.&lt;/p&gt;
&lt;details class="toc"&gt;
&lt;summary&gt;Contents&lt;/summary&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#the-problem"&gt;The problem&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#parsing-the-input"&gt;Parsing the input&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-world"&gt;The world&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#simulation"&gt;Simulation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#cleaning-up"&gt;Cleaning up&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#going-multidimensional"&gt;Going multidimensional&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#3d"&gt;3D&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#4d"&gt;4D&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#conclusion"&gt;Conclusion&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#bonus-cli-and-tests"&gt;Bonus: CLI and tests&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#bonus-more-tests"&gt;Bonus: more tests&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/details&gt;
&lt;h2 id="the-problem"&gt;The problem&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-problem" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;The problem might look familiar
(if you didn't read it yet, &lt;a class="external" href="https://adventofcode.com/2020/day/17"&gt;go do that first&lt;/a&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;we have a grid of cells that can be either &lt;em&gt;active&lt;/em&gt; or &lt;em&gt;inactive&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;cells change state at each step in time depending on their neighbors&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These things are called &lt;a class="external" href="https://en.wikipedia.org/wiki/Cellular_automaton"&gt;cellular automata&lt;/a&gt;,
the most famous of which is probably Conway's &lt;a class="external" href="https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life"&gt;Game of Life&lt;/a&gt;.
Our problem seems to have the same rules as Life:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;any &lt;em&gt;active&lt;/em&gt; cell with 2 or 3 &lt;em&gt;active&lt;/em&gt; neighbors remains &lt;em&gt;active&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;any &lt;em&gt;inactive&lt;/em&gt; cell with 3 &lt;em&gt;active&lt;/em&gt; neighbors becomes &lt;em&gt;active&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;all other cells become/remain &lt;em&gt;inactive&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;... except the grid is 3-dimensional.&lt;/p&gt;
&lt;details&gt;
&lt;summary&gt;Minor spoiler alert:&lt;/summary&gt;

&lt;p&gt;In part two of the problem, the grid is &lt;em&gt;4-dimensional&lt;/em&gt;.&lt;/p&gt;
&lt;/details&gt;

&lt;p&gt;You might notice that this is relatively hard to visualize:
the example output has 5 layers after 3 cycles in 3 dimensions,
and 25 layers after only 2 cycles in 4 dimensions!
Understanding what's happening in 4D seems difficult
even assuming the number of planes remains 25 for future iterations.&lt;sup class="footnote-ref" id="fnref-1"&gt;&lt;a href="#fn-1"&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;The normal flow of solving the problem would be something like this:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;implement the 3D version; then,&lt;/li&gt;
&lt;li&gt;after seeing the second part, either&lt;ul&gt;
&lt;li&gt;do 4D in a similar fashion, or&lt;/li&gt;
&lt;li&gt;generalize 3D to an arbitrary number of dimensions.&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;We'll do the opposite&lt;/strong&gt;:
implement 2D, which is trivial to check by hand,
and once we have some tests, generalize &lt;em&gt;that&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="parsing-the-input"&gt;Parsing the input&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#parsing-the-input" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;First, let's parse the input.&lt;/p&gt;
&lt;p&gt;We'll transform the input string into a list of lists of integers,
where 0 and 1 represent inactive and active cubes, respectively.
For the test input, it should look like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Using numbers &lt;a class="external" href="https://en.wikipedia.org/wiki/Separation_of_concerns"&gt;separates concerns&lt;/a&gt;, but also allows for nicer code:
we can write &lt;code&gt;if active&lt;/code&gt; instead of &lt;code&gt;if active == '#'&lt;/code&gt;, and
&lt;code&gt;sum(cubes)&lt;/code&gt; instead of &lt;code&gt;sum(active == '#' for active in cubes)&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;1&lt;/span&gt;
&lt;span class="normal"&gt;2&lt;/span&gt;
&lt;span class="normal"&gt;3&lt;/span&gt;
&lt;span class="normal"&gt;4&lt;/span&gt;
&lt;span class="normal"&gt;5&lt;/span&gt;
&lt;span class="normal"&gt;6&lt;/span&gt;
&lt;span class="normal"&gt;7&lt;/span&gt;
&lt;span class="normal"&gt;8&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;CHARACTERS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;.#&amp;#39;&lt;/span&gt;
&lt;span class="n"&gt;CHR_TO_NUM&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CHARACTERS&lt;/span&gt;&lt;span class="p"&gt;)}&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;CHR_TO_NUM&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;splitlines&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Later on, we'll need to turn that back into a string; let's take care of it now:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;NUM_TO_CHR&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CHARACTERS&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;format_2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;plane&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;NUM_TO_CHR&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;plane&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;We're not bothering to minimize the output like the example does
(hide empty rows/columns),
since we won't be looking at more than one layer per cycle anyway.&lt;/p&gt;
&lt;p&gt;Having both functions, we can write a basic round-trip test:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;TEST_INPUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;.#.&lt;/span&gt;
&lt;span class="s2"&gt;..#&lt;/span&gt;
&lt;span class="s2"&gt;###&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;format_2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_INPUT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;TEST_INPUT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;h2 id="the-world"&gt;The world&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#the-world" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;There's more than one way to represent the world,
but most straightforward should be the same we did the input,
as a nested list of integers.&lt;/p&gt;
&lt;p&gt;The grid is supposed to be infinite;
there are no infinite lists in Python, so we have to cheat:
we'll make a big enough world, and check if we've reached the edges.&lt;/p&gt;
&lt;p&gt;For now, 8 cubes ought to be enough for anyone:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;x_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y_size&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;8&lt;/span&gt;

&lt;span class="n"&gt;world&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x_size&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_size&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;&lt;code&gt;[[0] * x_size] * y_size&lt;/code&gt; won't work, because the outer list
will contain the (same) inner list y_size times:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python console session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;
&lt;span class="go"&gt;[[0, 0, 0], [0, 0, 0], [0, 0, 0]]&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="gp"&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class="n"&gt;l&lt;/span&gt;
&lt;span class="go"&gt;[[1, 0, 0], [1, 0, 0], [1, 0, 0]]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;p&gt;We'll place the input in the middle, to give it room to grow in all directions:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_INPUT&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;x_offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;y_offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_size&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;y_offset&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x_offset&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;

&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;format_2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;At this point, the script should output this:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;........
........
...#....
....#...
..###...
........
........
........
&lt;/code&gt;&lt;/pre&gt;
&lt;h2 id="simulation"&gt;Simulation&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#simulation" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Let's simulate one cycle.&lt;/p&gt;
&lt;p&gt;There are &lt;a class="external" href="https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life#Algorithms"&gt;many optimizations&lt;/a&gt; to this,
but again, we'll do the &lt;a class="external" href="http://wiki.c2.com/?DoTheSimplestThingThatCouldPossiblyWork"&gt;straightforward thing&lt;/a&gt;:
go through each cell, count its neighbors,
and change its state according to the rules.&lt;/p&gt;
&lt;p&gt;The cube state changes &lt;em&gt;simultaneously&lt;/em&gt; for all cubes,
so we cannot update the world in-place:
given two neighboring cubes updated in order,
the first cube's old state will be gone when we get to the second one.
To avoid this, we will use two worlds,
always reading from the old world and changing the new one.&lt;/p&gt;
&lt;p&gt;We'll put the logic in a function that takes the two worlds as arguments:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;27&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Going through each cell:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The cell's neighbors are all the cells in a 3x3 square centered on it,
&lt;em&gt;except&lt;/em&gt; the cell itself:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;            &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="k"&gt;continue&lt;/span&gt;

                    &lt;span class="n"&gt;nx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
                    &lt;span class="n"&gt;ny&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;old[ny][nx]&lt;/code&gt; should now get us the neighbor's state;
however, there are a couple of (literal) edge cases:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;The current cell has one of the coordinates 0 (is on the top or left edge).
Due to how list indexing works, the neighbors to the top/left
(coordinate -1) will be cells from the opposite edge of the world.
We don't want this: the world is supposed to be infinite, not wrap around.&lt;/li&gt;
&lt;li&gt;The current cell has one of the coordinates &lt;code&gt;size - 1&lt;/code&gt;
(is on the bottom or right edge).
We'll get an IndexError when trying to get the bottom/right neighbor.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In both cases, we want to know when the current cube is active,
since it's possible one of its out of bounds neighbors should become active:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;
&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;                    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;ny&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;active on edge: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="k"&gt;continue&lt;/span&gt;

                    &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ny&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
                    &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;IndexError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                            &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;active on edge: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                        &lt;span class="k"&gt;continue&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;For now, the exceptions will remain unhandled,
and we'll make the world bigger by hand when required.
We &lt;em&gt;could&lt;/em&gt; do something else, like catch the exception,
scale the world by some constant factor, and continue simulating.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;Having that, we set the cell's new state according to the rules:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;53&lt;/span&gt;
&lt;span class="normal"&gt;54&lt;/span&gt;
&lt;span class="normal"&gt;55&lt;/span&gt;
&lt;span class="normal"&gt;56&lt;/span&gt;
&lt;span class="normal"&gt;57&lt;/span&gt;
&lt;span class="normal"&gt;58&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;new_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;new_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_active&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Let's call that 6 times.
Instead instead of creating a new world on each iteration,
we can reuse the old one from the previous iteration,
since we'd be throwing it away anyway.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;77&lt;/span&gt;
&lt;span class="normal"&gt;78&lt;/span&gt;
&lt;span class="normal"&gt;79&lt;/span&gt;
&lt;span class="normal"&gt;80&lt;/span&gt;
&lt;span class="normal"&gt;81&lt;/span&gt;
&lt;span class="normal"&gt;82&lt;/span&gt;
&lt;span class="normal"&gt;83&lt;/span&gt;
&lt;span class="normal"&gt;84&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;
&lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x_size&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y_size&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cycle&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;after cycle #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cycle&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;format_2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;details&gt;
&lt;summary&gt;Running the script should output this:&lt;/summary&gt;

&lt;pre class="code code-container"&gt;&lt;code&gt;........
........
...#....
....#...
..###...
........
........
........

after cycle #1
........
........
........
..#.#...
...##...
...#....
........
........

after cycle #2
........
........
........
....#...
..#.#...
...##...
........
........

after cycle #3
........
........
........
...#....
....##..
...##...
........
........

after cycle #4
........
........
........
....#...
.....#..
...###..
........
........

after cycle #5
........
........
........
........
...#.#..
....##..
....#...
........

after cycle #6
........
........
........
........
.....#..
...#.#..
....##..
........
&lt;/code&gt;&lt;/pre&gt;
&lt;/details&gt;

&lt;p&gt;A quick visual inspection confirms our simulation is correct.&lt;/p&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;You may notice that after 4 cycles, we have the initial pattern,
but moved one cell to the bottom and right;
this pattern is called a &lt;a class="external" href="https://en.wikipedia.org/wiki/Glider_(Conway%27s_Life)"&gt;glider&lt;/a&gt;.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;To wrap up, we'll calculate the puzzle answer
(the number of cubes left in the active state after the sixth cycle),
and add a test to make sure we won't break anything later on:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 87&lt;/span&gt;
&lt;span class="normal"&gt; 88&lt;/span&gt;
&lt;span class="normal"&gt; 89&lt;/span&gt;
&lt;span class="normal"&gt; 90&lt;/span&gt;
&lt;span class="normal"&gt; 91&lt;/span&gt;
&lt;span class="normal"&gt; 92&lt;/span&gt;
&lt;span class="normal"&gt; 93&lt;/span&gt;
&lt;span class="normal"&gt; 94&lt;/span&gt;
&lt;span class="normal"&gt; 95&lt;/span&gt;
&lt;span class="normal"&gt; 96&lt;/span&gt;
&lt;span class="normal"&gt; 97&lt;/span&gt;
&lt;span class="normal"&gt; 98&lt;/span&gt;
&lt;span class="normal"&gt; 99&lt;/span&gt;
&lt;span class="normal"&gt;100&lt;/span&gt;
&lt;span class="normal"&gt;101&lt;/span&gt;
&lt;span class="normal"&gt;102&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;world&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;

&lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;the result is&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;........&lt;/span&gt;
&lt;span class="s2"&gt;........&lt;/span&gt;
&lt;span class="s2"&gt;........&lt;/span&gt;
&lt;span class="s2"&gt;........&lt;/span&gt;
&lt;span class="s2"&gt;.....#..&lt;/span&gt;
&lt;span class="s2"&gt;...#.#..&lt;/span&gt;
&lt;span class="s2"&gt;....##..&lt;/span&gt;
&lt;span class="s2"&gt;........&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;a class="attachment" href="/_file/conway-cubes/10-simulation/conway_cubes.py"&gt;The script up to this point.&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="cleaning-up"&gt;Cleaning up&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#cleaning-up" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Now that our 2D implementation is correct, we can make it more general.&lt;/p&gt;
&lt;p&gt;We won't do that straight away, though;
to make it possible to call in multiple ways and generalize incrementally,
we'll split the script into functions.&lt;/p&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;Throughout these changes, our test suite should keep passing.&lt;/p&gt;
&lt;p&gt;While working on the script, I used &lt;a class="external" href="https://eradman.com/entrproject/"&gt;entr&lt;/a&gt; to re-run it
automatically on every save:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;conway_cubes.py&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;entr&lt;span class="w"&gt; &lt;/span&gt;-rc&lt;span class="w"&gt; &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;conway_cubes.py
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Your editor may have have a &amp;quot;save and run&amp;quot; function;
if it doesn't, &lt;em&gt;entr&lt;/em&gt; is a nifty editor-independent way
of achieving the same thing.&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;First, let's extract the neighbor-counting logic:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;
&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;
&lt;span class="normal"&gt;33&lt;/span&gt;
&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;
&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_active_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="n"&gt;nx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;
            &lt;span class="n"&gt;ny&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;j&lt;/span&gt;

            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;nx&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;ny&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;active on edge: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

            &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ny&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;nx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
            &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;IndexError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;active on edge: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="k"&gt;continue&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;active_neighbors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;53&lt;/span&gt;
&lt;span class="normal"&gt;54&lt;/span&gt;
&lt;span class="normal"&gt;55&lt;/span&gt;
&lt;span class="normal"&gt;56&lt;/span&gt;
&lt;span class="normal"&gt;57&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_active_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Then, the construction of worlds:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;66&lt;/span&gt;
&lt;span class="normal"&gt;67&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;make_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;72&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;world&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;87&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Then, the input copying:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;70&lt;/span&gt;
&lt;span class="normal"&gt;71&lt;/span&gt;
&lt;span class="normal"&gt;72&lt;/span&gt;
&lt;span class="normal"&gt;73&lt;/span&gt;
&lt;span class="normal"&gt;74&lt;/span&gt;
&lt;span class="normal"&gt;75&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;copy_centered_2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;y_offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;x_offset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]))&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;dst&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;y_offset&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;x_offset&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;point&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;84&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;copy_centered_2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Finally, let's wrap the whole solution in a function.&lt;/p&gt;
&lt;p&gt;Again, to separate logic from presentation,
we'll split this into a generator that yields new states, forever;
it yields the initial state first for convenience:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;78&lt;/span&gt;
&lt;span class="normal"&gt;79&lt;/span&gt;
&lt;span class="normal"&gt;80&lt;/span&gt;
&lt;span class="normal"&gt;81&lt;/span&gt;
&lt;span class="normal"&gt;82&lt;/span&gt;
&lt;span class="normal"&gt;83&lt;/span&gt;
&lt;span class="normal"&gt;84&lt;/span&gt;
&lt;span class="normal"&gt;85&lt;/span&gt;
&lt;span class="normal"&gt;86&lt;/span&gt;
&lt;span class="normal"&gt;87&lt;/span&gt;
&lt;span class="normal"&gt;88&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;simulate_forever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;copy_centered_2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;
    &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;... and a function that drives it for 6 cycles, printing the resulting worlds.&lt;/p&gt;
&lt;p&gt;Since we want to use the function for more dimensions,
but don't want to implement formatting for anything except 2D,
we'll make printing optional through the magic of &lt;a class="external" href="https://python-patterns.guide/gang-of-four/factory-method/#dodge-use-dependency-injection"&gt;dependency injection&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Also, to keep the tests working,
we'll return the final state and the number of active cubes.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 91&lt;/span&gt;
&lt;span class="normal"&gt; 92&lt;/span&gt;
&lt;span class="normal"&gt; 93&lt;/span&gt;
&lt;span class="normal"&gt; 94&lt;/span&gt;
&lt;span class="normal"&gt; 95&lt;/span&gt;
&lt;span class="normal"&gt; 96&lt;/span&gt;
&lt;span class="normal"&gt; 97&lt;/span&gt;
&lt;span class="normal"&gt; 98&lt;/span&gt;
&lt;span class="normal"&gt; 99&lt;/span&gt;
&lt;span class="normal"&gt;100&lt;/span&gt;
&lt;span class="normal"&gt;101&lt;/span&gt;
&lt;span class="normal"&gt;102&lt;/span&gt;
&lt;span class="normal"&gt;103&lt;/span&gt;
&lt;span class="normal"&gt;104&lt;/span&gt;
&lt;span class="normal"&gt;105&lt;/span&gt;
&lt;span class="normal"&gt;106&lt;/span&gt;
&lt;span class="normal"&gt;107&lt;/span&gt;
&lt;span class="normal"&gt;108&lt;/span&gt;
&lt;span class="normal"&gt;109&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;format_world&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;worlds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;simulate_forever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cycle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;worlds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;after cycle #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cycle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;format_world&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;format_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;...&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;line&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;the result is&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt;


&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_INPUT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;format_world&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;format_2d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;a class="attachment" href="/_file/conway-cubes/20-cleaning-up/conway_cubes.py"&gt;The script up to this point.&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="going-multidimensional"&gt;Going multidimensional&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#going-multidimensional" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;OK, we're ready now:
let's follow &lt;code&gt;simulate_forever&lt;/code&gt; and make it work with N dimensions.&lt;/p&gt;
&lt;p&gt;First, we'll fix &lt;code&gt;make_world&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Instead of one size argument per dimension,
it will take a single argument, a tuple of sizes.
Using &lt;code&gt;size&lt;/code&gt; or &lt;code&gt;sizes&lt;/code&gt; as the name might be confusing
(since they are different &lt;em&gt;kinds&lt;/em&gt; of sizes);
how about we call it &lt;code&gt;shape&lt;/code&gt;?&lt;/p&gt;
&lt;p&gt;To handle the arbitrary number of dimensions, &lt;code&gt;make_world&lt;/code&gt; will be recursive:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;66&lt;/span&gt;
&lt;span class="normal"&gt;67&lt;/span&gt;
&lt;span class="normal"&gt;68&lt;/span&gt;
&lt;span class="normal"&gt;69&lt;/span&gt;
&lt;span class="normal"&gt;70&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;make_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;rest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shape&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;head&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;make_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rest&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;head&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;We now need to bubble this change up to &lt;code&gt;run&lt;/code&gt;.
For convenience, it will keep its &lt;code&gt;size&lt;/code&gt;, but get a new &lt;code&gt;dimensions&lt;/code&gt; argument,
so we can have an n-dimensional &amp;quot;square&amp;quot; world.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;81&lt;/span&gt;
&lt;span class="normal"&gt;82&lt;/span&gt;
&lt;span class="normal"&gt;83&lt;/span&gt;
&lt;span class="normal"&gt;84&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;simulate_forever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;94&lt;/span&gt;
&lt;span class="normal"&gt;95&lt;/span&gt;
&lt;span class="normal"&gt;96&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;format_world&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;worlds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;simulate_forever&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;112&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_INPUT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;format_world&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;format_2d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Next, input copying. For 2D, the destination was the world itself;
for higher dimensions, the destination is the (2D) plane in the middle
of each higher dimension:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;81&lt;/span&gt;
&lt;span class="normal"&gt;82&lt;/span&gt;
&lt;span class="normal"&gt;83&lt;/span&gt;
&lt;span class="normal"&gt;84&lt;/span&gt;
&lt;span class="normal"&gt;85&lt;/span&gt;
&lt;span class="normal"&gt;86&lt;/span&gt;
&lt;span class="normal"&gt;87&lt;/span&gt;
&lt;span class="normal"&gt;88&lt;/span&gt;
&lt;span class="normal"&gt;89&lt;/span&gt;
&lt;span class="normal"&gt;90&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;simulate_forever&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;old&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;new&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;make_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;copy_dst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;old&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;shape&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
        &lt;span class="n"&gt;copy_dst&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;copy_dst&lt;/span&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;//&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;copy_centered_2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;copy_dst&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Continuing with &lt;code&gt;simulate&lt;/code&gt;.
We need a generic version of the nested loop that goes over all the cells.
We'll write a version of &lt;a class="external" href="https://docs.python.org/3/library/functions.html#enumerate"&gt;enumerate&lt;/a&gt; that works with nested lists,
and instead of &lt;em&gt;i, value&lt;/em&gt; pairs, yields &lt;em&gt;(..., k, j, i), value&lt;/em&gt; pairs:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;53&lt;/span&gt;
&lt;span class="normal"&gt;54&lt;/span&gt;
&lt;span class="normal"&gt;55&lt;/span&gt;
&lt;span class="normal"&gt;56&lt;/span&gt;
&lt;span class="normal"&gt;57&lt;/span&gt;
&lt;span class="normal"&gt;58&lt;/span&gt;
&lt;span class="normal"&gt;59&lt;/span&gt;
&lt;span class="normal"&gt;60&lt;/span&gt;
&lt;span class="normal"&gt;61&lt;/span&gt;
&lt;span class="normal"&gt;62&lt;/span&gt;
&lt;span class="normal"&gt;63&lt;/span&gt;
&lt;span class="normal"&gt;64&lt;/span&gt;
&lt;span class="normal"&gt;65&lt;/span&gt;
&lt;span class="normal"&gt;66&lt;/span&gt;
&lt;span class="normal"&gt;67&lt;/span&gt;
&lt;span class="normal"&gt;68&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ndenumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
        &lt;span class="n"&gt;temp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;
        &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;list&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
            &lt;span class="n"&gt;temp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,),&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;part&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ndenumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;part&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;value&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;You may notice we stole the function name &lt;a class="external" href="https://numpy.org/doc/stable/reference/generated/numpy.ndenumerate.html"&gt;from numpy&lt;/a&gt;.
In the first draft of this article it was called &lt;code&gt;coord_enumerate&lt;/code&gt;
and it generated &lt;em&gt;..., k, j, i, value&lt;/em&gt; tuples,
but I changed it to make things easier for people already familiar with numpy.&lt;/p&gt;
&lt;p&gt;Unlike with numpy arrays, we have to infer the number of dimensions,
since our world doesn't have an explicit shape.&lt;/p&gt;
&lt;p&gt;Also unlike the numpy version, ours only works with lists;
to make it work with other sequences,
we could use something like:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nb"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;temp&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;collections&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;abc&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Sequence&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;p&gt;We'll leave &lt;code&gt;get_active_neighbors&lt;/code&gt; last, and finish &lt;code&gt;simulate&lt;/code&gt; first:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;71&lt;/span&gt;
&lt;span class="normal"&gt;72&lt;/span&gt;
&lt;span class="normal"&gt;73&lt;/span&gt;
&lt;span class="normal"&gt;74&lt;/span&gt;
&lt;span class="normal"&gt;75&lt;/span&gt;
&lt;span class="normal"&gt;76&lt;/span&gt;
&lt;span class="normal"&gt;77&lt;/span&gt;
&lt;span class="normal"&gt;78&lt;/span&gt;
&lt;span class="normal"&gt;79&lt;/span&gt;
&lt;span class="normal"&gt;80&lt;/span&gt;
&lt;span class="normal"&gt;81&lt;/span&gt;
&lt;span class="normal"&gt;82&lt;/span&gt;
&lt;span class="normal"&gt;83&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ndenumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_active_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="nb"&gt;reversed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;new_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;new_active&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;coord&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]:&lt;/span&gt;
            &lt;span class="n"&gt;target&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;coord&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;new_active&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;run&lt;/code&gt; also goes through all the cubes to count the active ones,
so we'll change it to use &lt;code&gt;ndenumerate&lt;/code&gt; as well:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;128&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ndenumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Finally, let's do &lt;code&gt;get_active_neighbors&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;First, we need to flatten the loop that generates neighbor coordinates.&lt;/p&gt;
&lt;p&gt;We could use recursion, like we did with &lt;code&gt;make_world&lt;/code&gt; and &lt;code&gt;ndenumerate&lt;/code&gt;,
but the standard library already has a tool for this:
&lt;a class="external" href="https://docs.python.org/3/library/itertools.html#itertools.product"&gt;itertools.product&lt;/a&gt; generates the cartesian product of some iterables;
even better, it can generate the product of an iterable with &lt;em&gt;itself&lt;/em&gt;.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;1&lt;/span&gt;
&lt;span class="normal"&gt;2&lt;/span&gt;
&lt;span class="normal"&gt;3&lt;/span&gt;
&lt;span class="normal"&gt;4&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;itertools&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;


&lt;span class="c1"&gt;# ...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;30&lt;/span&gt;
&lt;span class="normal"&gt;31&lt;/span&gt;
&lt;span class="normal"&gt;32&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;make_directions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;repeat&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ts&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;!=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;section class="admonition tip"&gt;
&lt;p class="admonition-title"&gt;Tip&lt;/p&gt;
&lt;p&gt;Even if itertools.product did not have a &lt;code&gt;repeat&lt;/code&gt; argument,
we could still emulate it with argument unpacking:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;product&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;/section&gt;
&lt;p&gt;After using &lt;code&gt;make_directions&lt;/code&gt;,
and making the neighbor counting parts generic as well,
&lt;code&gt;get_active_neighbors&lt;/code&gt; becomes:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;
&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;
&lt;span class="normal"&gt;52&lt;/span&gt;
&lt;span class="normal"&gt;53&lt;/span&gt;
&lt;span class="normal"&gt;54&lt;/span&gt;
&lt;span class="normal"&gt;55&lt;/span&gt;
&lt;span class="normal"&gt;56&lt;/span&gt;
&lt;span class="normal"&gt;57&lt;/span&gt;
&lt;span class="normal"&gt;58&lt;/span&gt;
&lt;span class="normal"&gt;59&lt;/span&gt;
&lt;span class="normal"&gt;60&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_active_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;offsets&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;make_directions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;)):&lt;/span&gt;

        &lt;span class="n"&gt;neighbor_coords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
            &lt;span class="n"&gt;coord&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;coord&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offset&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;offsets&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;]&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;coord&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;coord&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;neighbor_coords&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;active on edge: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;neighbor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;
            &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;coord&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;neighbor_coords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;neighbor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;neighbor&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;coord&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;IndexError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;active on edge: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="k"&gt;continue&lt;/span&gt;

        &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;neighbor&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;active_neighbors&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;The last remaining thing is to fix its call site in &lt;code&gt;simulate&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;83&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;        &lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_active_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;We're done!&lt;/p&gt;
&lt;h2 id="3d"&gt;3D&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#3d" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;After 6 cycles with the test input, there should be 112 active cubes:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;159&lt;/span&gt;
&lt;span class="normal"&gt;160&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_INPUT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;112&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;It turns out 8 cubes is not enough:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Python Traceback"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="x"&gt;after cycle #0: ...&lt;/span&gt;
&lt;span class="x"&gt;after cycle #1: ...&lt;/span&gt;
&lt;span class="x"&gt;after cycle #2: ...&lt;/span&gt;
&lt;span class="x"&gt;after cycle #3: ...&lt;/span&gt;
&lt;span class="gt"&gt;Traceback (most recent call last):&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;conway_cubes.py&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;161&lt;/span&gt;, in &lt;span class="n"&gt;&amp;lt;module&amp;gt;&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_INPUT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;conway_cubes.py&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;131&lt;/span&gt;, in &lt;span class="n"&gt;run&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cycle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;worlds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;conway_cubes.py&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;123&lt;/span&gt;, in &lt;span class="n"&gt;simulate_forever&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;simulate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;new&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;conway_cubes.py&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;84&lt;/span&gt;, in &lt;span class="n"&gt;simulate&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;active_neighbors&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;get_active_neighbors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;old&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  File &lt;span class="nb"&gt;&amp;quot;conway_cubes.py&amp;quot;&lt;/span&gt;, line &lt;span class="m"&gt;47&lt;/span&gt;, in &lt;span class="n"&gt;get_active_neighbors&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;raise&lt;/span&gt; &lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;active on edge: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;coords&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="gr"&gt;RuntimeError&lt;/span&gt;: &lt;span class="n"&gt;active on edge: (2, 3, 0)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Good thing we're checking for that.
Bumping the world size to 16 fixes it:&lt;/p&gt;
&lt;pre class="code code-container"&gt;&lt;code&gt;after cycle #0: ...
after cycle #1: ...
after cycle #2: ...
after cycle #3: ...
after cycle #4: ...
after cycle #5: ...
after cycle #6: ...
the result is 112
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Time to plug the puzzle input; I assume they are different for everyone,
so feel free to use yours instead.&lt;/p&gt;
&lt;p&gt;At first I got another &lt;code&gt;active on edge&lt;/code&gt; error;
since running with the test input already takes about a second,
I increased the world size by smaller increments instead of doubling it;
20 is enough for my input:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;162&lt;/span&gt;
&lt;span class="normal"&gt;163&lt;/span&gt;
&lt;span class="normal"&gt;164&lt;/span&gt;
&lt;span class="normal"&gt;165&lt;/span&gt;
&lt;span class="normal"&gt;166&lt;/span&gt;
&lt;span class="normal"&gt;167&lt;/span&gt;
&lt;span class="normal"&gt;168&lt;/span&gt;
&lt;span class="normal"&gt;169&lt;/span&gt;
&lt;span class="normal"&gt;170&lt;/span&gt;
&lt;span class="normal"&gt;171&lt;/span&gt;
&lt;span class="normal"&gt;172&lt;/span&gt;
&lt;span class="normal"&gt;173&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;INPUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;.##...#.&lt;/span&gt;
&lt;span class="s2"&gt;.#.###..&lt;/span&gt;
&lt;span class="s2"&gt;..##.#.#&lt;/span&gt;
&lt;span class="s2"&gt;##...#.#&lt;/span&gt;
&lt;span class="s2"&gt;#..#...#&lt;/span&gt;
&lt;span class="s2"&gt;#..###..&lt;/span&gt;
&lt;span class="s2"&gt;.##.####&lt;/span&gt;
&lt;span class="s2"&gt;..#####.&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INPUT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;the result is 386&lt;/code&gt;, which unlocks the second part of the problem.&lt;/p&gt;
&lt;h2 id="4d"&gt;4D&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#4d" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Moving on to even higher dimensions:&lt;/p&gt;
&lt;p&gt;The 4D cube count for the test input should be 848:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;175&lt;/span&gt;
&lt;span class="normal"&gt;176&lt;/span&gt;
&lt;span class="normal"&gt;177&lt;/span&gt;
&lt;span class="normal"&gt;178&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_INPUT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;848&lt;/span&gt;

&lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;INPUT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;... and after 10 seconds the first cycle hasn't ended yet.&lt;/p&gt;
&lt;p&gt;Let's add some timings in &lt;code&gt;run&lt;/code&gt; before we let this run any further.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;2&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;128&lt;/span&gt;
&lt;span class="normal"&gt;129&lt;/span&gt;
&lt;span class="normal"&gt;130&lt;/span&gt;
&lt;span class="normal"&gt;131&lt;/span&gt;
&lt;span class="normal"&gt;132&lt;/span&gt;
&lt;span class="normal"&gt;133&lt;/span&gt;
&lt;span class="normal"&gt;134&lt;/span&gt;
&lt;span class="normal"&gt;135&lt;/span&gt;
&lt;span class="normal"&gt;136&lt;/span&gt;
&lt;span class="normal"&gt;137&lt;/span&gt;
&lt;span class="normal"&gt;138&lt;/span&gt;
&lt;span class="normal"&gt;139&lt;/span&gt;
&lt;span class="normal"&gt;140&lt;/span&gt;
&lt;span class="normal"&gt;141&lt;/span&gt;
&lt;span class="normal"&gt;142&lt;/span&gt;
&lt;span class="normal"&gt;143&lt;/span&gt;
&lt;span class="normal"&gt;144&lt;/span&gt;
&lt;span class="normal"&gt;145&lt;/span&gt;
&lt;span class="normal"&gt;146&lt;/span&gt;
&lt;span class="normal"&gt;147&lt;/span&gt;
&lt;span class="normal"&gt;148&lt;/span&gt;
&lt;span class="normal"&gt;149&lt;/span&gt;
&lt;span class="normal"&gt;150&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;format_world&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;worlds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;simulate_forever&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;total_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cycle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;worlds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;after cycle #&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;cycle&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;start&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.2f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s): &amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;format_world&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;format_world&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
        &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;...&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;total_time&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="n"&gt;end&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;
        &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;ndenumerate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;the result is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;active_cubes&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; (&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;total_time&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.2f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;section class="admonition note"&gt;
&lt;p class="admonition-title"&gt;Note&lt;/p&gt;
&lt;p&gt;We use &lt;a class="external" href="https://docs.python.org/3/library/time.html#time.perf_counter"&gt;time.perf_counter&lt;/a&gt; because it is the
&amp;quot;clock with the highest available resolution to measure a short duration&amp;quot;;
since the timescales we're dealing with don't probably need it,
we could have used &lt;a class="external" href="https://docs.python.org/3/library/time.html#time.monotonic"&gt;time.monotonic&lt;/a&gt; (on Unix, they're &lt;a class="external" href="https://github.com/python/cpython/blob/ae6cd7cfdab0599139002c526953d907696d9eef/Python/pytime.c#L1056-L1064"&gt;actually the same&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;In general, you should avoid &lt;a class="external" href="https://docs.python.org/3/library/time.html#time.time"&gt;time.time&lt;/a&gt; for measuring durations,
because it can skip backward or forward (e.g. &lt;a class="external" href="https://en.wikipedia.org/wiki/Daylight_saving_time"&gt;daylight saving time&lt;/a&gt;),
or move slower or faster than real time (e.g. &lt;a class="external" href="https://en.wikipedia.org/wiki/Leap_second#Workarounds_for_leap_second_problems"&gt;leap smearing&lt;/a&gt;).&lt;/p&gt;
&lt;/section&gt;
&lt;p&gt;After about 1 minute, I get the (correct) result for the test input.&lt;/p&gt;
&lt;p&gt;After 2 more minutes, &lt;code&gt;the result is 2276&lt;/code&gt;; this is also correct.&lt;/p&gt;
&lt;p&gt;&lt;a class="attachment" href="/_file/conway-cubes/30-4d/conway_cubes.py"&gt;The script up to this point.&lt;/a&gt;&lt;/p&gt;
&lt;h2 id="conclusion"&gt;Conclusion&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#conclusion" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;What have we learned?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sometimes, it's easier to start with a simpler problem,
and iterate towards the real one&lt;/strong&gt;:
we did most of the coding for 2D, which was easier to understand and debug;
once we got that general enough, 3D and 4D &lt;em&gt;just worked&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Tests make refactoring quicker.&lt;/strong&gt;
Even for &amp;quot;exploratory&amp;quot; coding like this,
when you don't know exactly what the final code will look like,
or even what you're trying to achieve,
some parts &amp;quot;settle&amp;quot; earlier than others;
once that happens,
a simple high-level test allows you to focus on the &lt;em&gt;next thing&lt;/em&gt;
without having to constantly check
that stuff that &lt;em&gt;was&lt;/em&gt; working is &lt;em&gt;still&lt;/em&gt; working.&lt;/p&gt;
&lt;p&gt;The 4D simulation is really slow ... &lt;em&gt;exponentially&lt;/em&gt; slow.
We'll see at least one way of improving that in a future post.&lt;/p&gt;
&lt;h2 id="bonus-cli-and-tests"&gt;Bonus: CLI and tests&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bonus-cli-and-tests" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;When run, the script goes through all the tests
and then prints the results for the two parts of the problem
(3 and 4 dimensions).&lt;/p&gt;
&lt;p&gt;Let's make it a bit easier to run a single simulation
with a specific world size, dimension, and number of cycles;
this will be useful when profiling.&lt;/p&gt;
&lt;p&gt;First, we'll move the formatting test (line 22) and the other tests
to a new file, &lt;code&gt;test_conway_cubes.py&lt;/code&gt;, and into test functions;
we leave &lt;code&gt;TEST_INPUT&lt;/code&gt; in the module since we'll use it from the CLI as well.&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 1&lt;/span&gt;
&lt;span class="normal"&gt; 2&lt;/span&gt;
&lt;span class="normal"&gt; 3&lt;/span&gt;
&lt;span class="normal"&gt; 4&lt;/span&gt;
&lt;span class="normal"&gt; 5&lt;/span&gt;
&lt;span class="normal"&gt; 6&lt;/span&gt;
&lt;span class="normal"&gt; 7&lt;/span&gt;
&lt;span class="normal"&gt; 8&lt;/span&gt;
&lt;span class="normal"&gt; 9&lt;/span&gt;
&lt;span class="normal"&gt;10&lt;/span&gt;
&lt;span class="normal"&gt;11&lt;/span&gt;
&lt;span class="normal"&gt;12&lt;/span&gt;
&lt;span class="normal"&gt;13&lt;/span&gt;
&lt;span class="normal"&gt;14&lt;/span&gt;
&lt;span class="normal"&gt;15&lt;/span&gt;
&lt;span class="normal"&gt;16&lt;/span&gt;
&lt;span class="normal"&gt;17&lt;/span&gt;
&lt;span class="normal"&gt;18&lt;/span&gt;
&lt;span class="normal"&gt;19&lt;/span&gt;
&lt;span class="normal"&gt;20&lt;/span&gt;
&lt;span class="normal"&gt;21&lt;/span&gt;
&lt;span class="normal"&gt;22&lt;/span&gt;
&lt;span class="normal"&gt;23&lt;/span&gt;
&lt;span class="normal"&gt;24&lt;/span&gt;
&lt;span class="normal"&gt;25&lt;/span&gt;
&lt;span class="normal"&gt;26&lt;/span&gt;
&lt;span class="normal"&gt;27&lt;/span&gt;
&lt;span class="normal"&gt;28&lt;/span&gt;
&lt;span class="normal"&gt;29&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;textwrap&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;

&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;conway_cubes&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;TEST_INPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;format_2d&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_parse_roundtrip&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;format_2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_INPUT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;TEST_INPUT&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_2&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_INPUT&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;format_world&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;format_2d&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;        ........&lt;/span&gt;
&lt;span class="s2"&gt;        ........&lt;/span&gt;
&lt;span class="s2"&gt;        ........&lt;/span&gt;
&lt;span class="s2"&gt;        ........&lt;/span&gt;
&lt;span class="s2"&gt;        .....#..&lt;/span&gt;
&lt;span class="s2"&gt;        ...#.#..&lt;/span&gt;
&lt;span class="s2"&gt;        ....##..&lt;/span&gt;
&lt;span class="s2"&gt;        ........&lt;/span&gt;
&lt;span class="s2"&gt;    &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_3&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_INPUT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;112&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_4&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
    &lt;span class="n"&gt;_&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;TEST_INPUT&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;active_cubes&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;848&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;We want to be able to run fewer cycles, so we add a new argument to &lt;code&gt;run&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;120&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;format_world&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cycles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;125&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;cycle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;world&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cycles&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;worlds&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;And then remove the rest of the top level code
in favor of a rudimentary CLI:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;145&lt;/span&gt;
&lt;span class="normal"&gt;146&lt;/span&gt;
&lt;span class="normal"&gt;147&lt;/span&gt;
&lt;span class="normal"&gt;148&lt;/span&gt;
&lt;span class="normal"&gt;149&lt;/span&gt;
&lt;span class="normal"&gt;150&lt;/span&gt;
&lt;span class="normal"&gt;151&lt;/span&gt;
&lt;span class="normal"&gt;152&lt;/span&gt;
&lt;span class="normal"&gt;153&lt;/span&gt;
&lt;span class="normal"&gt;154&lt;/span&gt;
&lt;span class="normal"&gt;155&lt;/span&gt;
&lt;span class="normal"&gt;156&lt;/span&gt;
&lt;span class="normal"&gt;157&lt;/span&gt;
&lt;span class="normal"&gt;158&lt;/span&gt;
&lt;span class="normal"&gt;159&lt;/span&gt;
&lt;span class="normal"&gt;160&lt;/span&gt;
&lt;span class="normal"&gt;161&lt;/span&gt;
&lt;span class="normal"&gt;162&lt;/span&gt;
&lt;span class="normal"&gt;163&lt;/span&gt;
&lt;span class="normal"&gt;164&lt;/span&gt;
&lt;span class="normal"&gt;165&lt;/span&gt;
&lt;span class="normal"&gt;166&lt;/span&gt;
&lt;span class="normal"&gt;167&lt;/span&gt;
&lt;span class="normal"&gt;168&lt;/span&gt;
&lt;span class="normal"&gt;169&lt;/span&gt;
&lt;span class="normal"&gt;170&lt;/span&gt;
&lt;span class="normal"&gt;171&lt;/span&gt;
&lt;span class="normal"&gt;172&lt;/span&gt;
&lt;span class="normal"&gt;173&lt;/span&gt;
&lt;span class="normal"&gt;174&lt;/span&gt;
&lt;span class="normal"&gt;175&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;TEST_INPUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;.#.&lt;/span&gt;
&lt;span class="s2"&gt;..#&lt;/span&gt;
&lt;span class="s2"&gt;###&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="n"&gt;INPUT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="s2"&gt;.##...#.&lt;/span&gt;
&lt;span class="s2"&gt;.#.###..&lt;/span&gt;
&lt;span class="s2"&gt;..##.#.#&lt;/span&gt;
&lt;span class="s2"&gt;##...#.#&lt;/span&gt;
&lt;span class="s2"&gt;#..#...#&lt;/span&gt;
&lt;span class="s2"&gt;#..###..&lt;/span&gt;
&lt;span class="s2"&gt;.##.####&lt;/span&gt;
&lt;span class="s2"&gt;..#####.&lt;/span&gt;
&lt;span class="s2"&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="nb"&gt;input&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;test&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;TEST_INPUT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;real&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;INPUT&lt;/span&gt;&lt;span class="p"&gt;}[&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]]&lt;/span&gt;
    &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cycles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;format_world&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;format_2d&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;cycles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;cycles&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="vm"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;&amp;#39;__main__&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;sys&lt;/span&gt;
    &lt;span class="n"&gt;main&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sys&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argv&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;We now can run the tests using &lt;a class="external" href="https://docs.pytest.org/en/stable/"&gt;pytest&lt;/a&gt;.
The 4D one takes a while; we use &lt;code&gt;-k&lt;/code&gt; to skip it:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;pytest&lt;span class="w"&gt; &lt;/span&gt;test_conway_cubes.py&lt;span class="w"&gt; &lt;/span&gt;-q&lt;span class="w"&gt; &lt;/span&gt;-k&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;not 4&amp;#39;&lt;/span&gt;
&lt;span class="go"&gt;...                                                                      [100%]&lt;/span&gt;
&lt;span class="go"&gt;3 passed, 1 deselected in 1.14s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The script is invoked like this:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;python3&lt;span class="w"&gt; &lt;/span&gt;conway_cubes.py&lt;span class="w"&gt; &lt;/span&gt;real&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;20&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;
&lt;span class="go"&gt;after cycle #0 (0.01s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #1 (24.15s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #2 (23.45s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #3 (23.79s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #4 (24.31s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #5 (24.15s): ...&lt;/span&gt;
&lt;span class="go"&gt;after cycle #6 (24.07s): ...&lt;/span&gt;
&lt;span class="go"&gt;the result is 2276 (143.94s)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id="bonus-more-tests"&gt;Bonus: more tests&lt;span class="headerlink"&gt;&amp;nbsp;&lt;a href="#bonus-more-tests" title="permalink"&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Our tests cover the most common cases,
but there's something we neglected entirely:
edge cases.&lt;/p&gt;
&lt;p&gt;At the moment, that's not necessarily an issue:
the code dealing with it is straightforward,
and the puzzle itself is testing it for us –
if we go over the edge and the script doesn't fail,
we won't get the right result.&lt;/p&gt;
&lt;p&gt;However, the code &lt;em&gt;may&lt;/em&gt; become less straightforward after optimizing it,
and we don't want to rely on the website for validation
every time we make a change.&lt;/p&gt;
&lt;p&gt;Let's fix that.&lt;/p&gt;
&lt;p&gt;The first test checks we get errors when there are active cells on the edges:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;3&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pytest&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt;34&lt;/span&gt;
&lt;span class="normal"&gt;35&lt;/span&gt;
&lt;span class="normal"&gt;36&lt;/span&gt;
&lt;span class="normal"&gt;37&lt;/span&gt;
&lt;span class="normal"&gt;38&lt;/span&gt;
&lt;span class="normal"&gt;39&lt;/span&gt;
&lt;span class="normal"&gt;40&lt;/span&gt;
&lt;span class="normal"&gt;41&lt;/span&gt;
&lt;span class="normal"&gt;42&lt;/span&gt;
&lt;span class="normal"&gt;43&lt;/span&gt;
&lt;span class="normal"&gt;44&lt;/span&gt;
&lt;span class="normal"&gt;45&lt;/span&gt;
&lt;span class="normal"&gt;46&lt;/span&gt;
&lt;span class="normal"&gt;47&lt;/span&gt;
&lt;span class="normal"&gt;48&lt;/span&gt;
&lt;span class="normal"&gt;49&lt;/span&gt;
&lt;span class="normal"&gt;50&lt;/span&gt;
&lt;span class="normal"&gt;51&lt;/span&gt;
&lt;span class="normal"&gt;52&lt;/span&gt;
&lt;span class="normal"&gt;53&lt;/span&gt;
&lt;span class="normal"&gt;54&lt;/span&gt;
&lt;span class="normal"&gt;55&lt;/span&gt;
&lt;span class="normal"&gt;56&lt;/span&gt;
&lt;span class="normal"&gt;57&lt;/span&gt;
&lt;span class="normal"&gt;58&lt;/span&gt;
&lt;span class="normal"&gt;59&lt;/span&gt;
&lt;span class="normal"&gt;60&lt;/span&gt;
&lt;span class="normal"&gt;61&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;input&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;        #..&lt;/span&gt;
&lt;span class="sd"&gt;        ...&lt;/span&gt;
&lt;span class="sd"&gt;        ...&lt;/span&gt;
&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;        .#.&lt;/span&gt;
&lt;span class="sd"&gt;        ...&lt;/span&gt;
&lt;span class="sd"&gt;        ...&lt;/span&gt;
&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;        ...&lt;/span&gt;
&lt;span class="sd"&gt;        ..#&lt;/span&gt;
&lt;span class="sd"&gt;        ...&lt;/span&gt;
&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;        ...&lt;/span&gt;
&lt;span class="sd"&gt;        ...&lt;/span&gt;
&lt;span class="sd"&gt;        .#.&lt;/span&gt;
&lt;span class="sd"&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nd"&gt;@pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;dimensions&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_edge_errors&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;raises&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="ne"&gt;RuntimeError&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dimensions&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;cycles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Here, the &lt;a class="external" href="https://docs.pytest.org/en/stable/parametrize.html#pytest-mark-parametrize"&gt;parametrize&lt;/a&gt; decorator defines
4 different &lt;code&gt;input&lt;/code&gt; and 3 different &lt;code&gt;dimensions&lt;/code&gt; arguments,
so that the &lt;code&gt;test_edge_errors&lt;/code&gt; function will run using them in turn,
in all possible combinations.&lt;/p&gt;
&lt;p&gt;The second test checks that the new world can have active cells on the edges,
as long as the old one didn't:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;table class="highlighttable"&gt;&lt;tr&gt;&lt;td class="linenos"&gt;&lt;div class="linenodiv"&gt;&lt;pre class="code"&gt;&lt;code&gt;&lt;span class="normal"&gt; 64&lt;/span&gt;
&lt;span class="normal"&gt; 65&lt;/span&gt;
&lt;span class="normal"&gt; 66&lt;/span&gt;
&lt;span class="normal"&gt; 67&lt;/span&gt;
&lt;span class="normal"&gt; 68&lt;/span&gt;
&lt;span class="normal"&gt; 69&lt;/span&gt;
&lt;span class="normal"&gt; 70&lt;/span&gt;
&lt;span class="normal"&gt; 71&lt;/span&gt;
&lt;span class="normal"&gt; 72&lt;/span&gt;
&lt;span class="normal"&gt; 73&lt;/span&gt;
&lt;span class="normal"&gt; 74&lt;/span&gt;
&lt;span class="normal"&gt; 75&lt;/span&gt;
&lt;span class="normal"&gt; 76&lt;/span&gt;
&lt;span class="normal"&gt; 77&lt;/span&gt;
&lt;span class="normal"&gt; 78&lt;/span&gt;
&lt;span class="normal"&gt; 79&lt;/span&gt;
&lt;span class="normal"&gt; 80&lt;/span&gt;
&lt;span class="normal"&gt; 81&lt;/span&gt;
&lt;span class="normal"&gt; 82&lt;/span&gt;
&lt;span class="normal"&gt; 83&lt;/span&gt;
&lt;span class="normal"&gt; 84&lt;/span&gt;
&lt;span class="normal"&gt; 85&lt;/span&gt;
&lt;span class="normal"&gt; 86&lt;/span&gt;
&lt;span class="normal"&gt; 87&lt;/span&gt;
&lt;span class="normal"&gt; 88&lt;/span&gt;
&lt;span class="normal"&gt; 89&lt;/span&gt;
&lt;span class="normal"&gt; 90&lt;/span&gt;
&lt;span class="normal"&gt; 91&lt;/span&gt;
&lt;span class="normal"&gt; 92&lt;/span&gt;
&lt;span class="normal"&gt; 93&lt;/span&gt;
&lt;span class="normal"&gt; 94&lt;/span&gt;
&lt;span class="normal"&gt; 95&lt;/span&gt;
&lt;span class="normal"&gt; 96&lt;/span&gt;
&lt;span class="normal"&gt; 97&lt;/span&gt;
&lt;span class="normal"&gt; 98&lt;/span&gt;
&lt;span class="normal"&gt; 99&lt;/span&gt;
&lt;span class="normal"&gt;100&lt;/span&gt;
&lt;span class="normal"&gt;101&lt;/span&gt;
&lt;span class="normal"&gt;102&lt;/span&gt;
&lt;span class="normal"&gt;103&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;td class="code"&gt;&lt;div&gt;&lt;pre class="code" data-lang="Python"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nd"&gt;@pytest&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mark&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;parametrize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;input, expected_output&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;            .....&lt;/span&gt;
&lt;span class="sd"&gt;            .#...&lt;/span&gt;
&lt;span class="sd"&gt;            .#...&lt;/span&gt;
&lt;span class="sd"&gt;            .#...&lt;/span&gt;
&lt;span class="sd"&gt;            .....&lt;/span&gt;
&lt;span class="sd"&gt;            &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;            .....&lt;/span&gt;
&lt;span class="sd"&gt;            .....&lt;/span&gt;
&lt;span class="sd"&gt;            ###..&lt;/span&gt;
&lt;span class="sd"&gt;            .....&lt;/span&gt;
&lt;span class="sd"&gt;            .....&lt;/span&gt;
&lt;span class="sd"&gt;            &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;            .....&lt;/span&gt;
&lt;span class="sd"&gt;            .....&lt;/span&gt;
&lt;span class="sd"&gt;            .....&lt;/span&gt;
&lt;span class="sd"&gt;            .###.&lt;/span&gt;
&lt;span class="sd"&gt;            .....&lt;/span&gt;
&lt;span class="sd"&gt;            &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="sd"&gt;&amp;quot;&amp;quot;&amp;quot;\&lt;/span&gt;
&lt;span class="sd"&gt;            .....&lt;/span&gt;
&lt;span class="sd"&gt;            .....&lt;/span&gt;
&lt;span class="sd"&gt;            ..#..&lt;/span&gt;
&lt;span class="sd"&gt;            ..#..&lt;/span&gt;
&lt;span class="sd"&gt;            ..#..&lt;/span&gt;
&lt;span class="sd"&gt;            &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;
        &lt;span class="p"&gt;),&lt;/span&gt;

    &lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;test_edge_ok&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;expected_output&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;parse_input&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;input&lt;/span&gt;&lt;span class="p"&gt;)),&lt;/span&gt; &lt;span class="n"&gt;cycles&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="n"&gt;format_2d&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;world&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="n"&gt;dedent&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;expected_output&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/td&gt;&lt;/tr&gt;&lt;/table&gt;&lt;/div&gt;
&lt;p&gt;Because of parametrization, we get 14 actual tests:
12 for &lt;code&gt;test_edge_errors&lt;/code&gt;, and 2 for &lt;code&gt;test_edge_ok&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight code-container"&gt;&lt;pre class="code" data-lang="Bash Session"&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="gp"&gt;$ &lt;/span&gt;pytest&lt;span class="w"&gt; &lt;/span&gt;test_conway_cubes.py&lt;span class="w"&gt; &lt;/span&gt;-q&lt;span class="w"&gt; &lt;/span&gt;-k&lt;span class="w"&gt; &lt;/span&gt;edge
&lt;span class="go"&gt;..............                                                           [100%]&lt;/span&gt;
&lt;span class="go"&gt;14 passed, 4 deselected in 0.06s&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The &lt;a class="attachment" href="/_file/conway-cubes/99-end/conway_cubes.py"&gt;final version of the script&lt;/a&gt;
and &lt;a class="attachment" href="/_file/conway-cubes/99-end/test_conway_cubes.py"&gt;test file&lt;/a&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;&lt;strong&gt;Learned something new today?&lt;/strong&gt; Share it with others, it really helps! &lt;span class="text-large"&gt;
&lt;span class="share-icons"&gt;
&lt;a
    class="share-icon pycoders color"
    href="https://pycoders.com/submissions"
    target="_blank"
&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a
    class="share-icon hacker-news color"
    href="https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/conway-cubes&amp;t=Solving%20Advent%20of%20Code%202020%20day%2017%20by%20not%20solving%20it"
&gt;HN&lt;/a&gt;
&lt;a
    class="share-icon bluesky color"
    href="https://bsky.%61%70%70/intent/compose?text=Solving%20Advent%20of%20Code%202020%20day%2017%20by%20not%20solving%20it%20https%3A//death.andgravity.com/conway-cubes"
&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class="share-icon reddit color"
    href="https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/conway-cubes&amp;title=Solving%20Advent%20of%20Code%202020%20day%2017%20by%20not%20solving%20it"
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a
    class="share-icon linkedin color"
    href="https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/conway-cubes"
&gt;linkedin&lt;/a&gt;
&lt;a
    class="share-icon twitter color"
    href="https://twitter.%63%6f%6d/%73%68%61%72%65?text=Solving%20Advent%20of%20Code%202020%20day%2017%20by%20not%20solving%20it&amp;url=https%3A//death.andgravity.com/conway-cubes&amp;via=_andgravity"
&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;


&lt;section class="footnotes"&gt;
&lt;ol&gt;
&lt;li id="fn-1"&gt;&lt;p&gt;If you think that's a very conservative assumption, trust your instincts. &lt;a href="#fnref-1" class="footnote"&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/section&gt;
</content>
    <link href="https://death.andgravity.com/conway-cubes" rel="alternate"/>
    <summary>... in which we solve the day 17 problem from Advent of Code 2020, Conway Cubes, in a generic way, focusing on testing, refactoring, and idiomatic Python, in a way that helps you translate those skills to your regular, non-puzzle coding.</summary>
    <published>2021-01-22T15:10:00+00:00</published>
  </entry>
</feed>
