<?xml version="1.0"?>
<rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/">

<channel>
	<title>Planet Python</title>
	<link>http://planetpython.org/</link>
	<language>en</language>
	<description>Planet Python - http://planetpython.org/</description>

<item>
	<title>Django Weblog: DSF 2026 Fundraising Goals</title>
	<guid>https://www.djangoproject.com/weblog/2026/jun/10/dsf-2026-fundraising-goals/</guid>
	<link>https://www.djangoproject.com/weblog/2026/jun/10/dsf-2026-fundraising-goals/</link>
	<description>&lt;p&gt;Django has grown far beyond a web framework. It powers businesses, nonprofits, startups, educational institutions, and critical infrastructure around the world. The Django Software Foundation exists to support that ecosystem, and none of that work is possible without funding. This year, the board set an ambitious new fundraising goal, and I want to be transparent about what we are aiming for and why it matters.&lt;/p&gt;
&lt;p&gt;Before talking about where we want to go, it's important to recognize that everything the DSF does today is possible because of the organizations and individuals who already support Django. Their contributions fund the work that keeps Django healthy, secure, and sustainable, and we are deeply grateful for that support. &lt;/p&gt;
&lt;h2 id=&quot;s-our-2026-goal-500000&quot;&gt;&lt;strong&gt;Our 2026 Goal: $500,000&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;This year, we are raising our annual fundraising goal from $300,000 to $500,000.&lt;/p&gt;
&lt;p&gt;That is a meaningful increase, and it reflects where the foundation needs to be. Our current monthly recurring donations are around $9,000 per month. To reach $500,000 annually, we need to grow that to approximately $15,000 per month.&lt;/p&gt;
&lt;p&gt;Reaching this goal will require both new supporters and increased support from existing donors. Doing so will help us maintain the programs the community relies on while creating room for future growth.&lt;/p&gt;
&lt;h2 id=&quot;s-what-the-money-funds&quot;&gt;&lt;strong&gt;What the Money Funds&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;Before asking for support, it is only fair to explain where the money goes.&lt;/p&gt;
&lt;p&gt;The largest line item in our budget is the &lt;a href=&quot;https://www.djangoproject.com/fundraising/#fellowship-program&quot;&gt;Django Fellows program&lt;/a&gt;. Our three Fellows dedicate their time to triaging tickets, reviewing pull requests, managing releases, handling security issues, and doing the essential work that keeps Django moving forward. Without sustained funding, we cannot maintain this program.&lt;/p&gt;
&lt;p&gt;Beyond the Fellows, the DSF:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manages the Django trademark and legal protections  &lt;/li&gt;
&lt;li&gt;Funds the infrastructure that keeps djangoproject.com and related services running  &lt;/li&gt;
&lt;li&gt;Provides grants to DjangoCon events around the world, including DjangoCon US, DjangoCon Europe, and DjangoCon Africa  &lt;/li&gt;
&lt;li&gt;Funds regional Django Days, sprints, and community events  &lt;/li&gt;
&lt;li&gt;Supports Django Girls events through grants  &lt;/li&gt;
&lt;li&gt;Invests in community programs like Djangonaut Space&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Taken together, $500,000 in annual funding would allow us to sustain our three Fellows, maintain operational support for the DSF, create a clear path to hiring an Executive Director, and expand our ability to support the Django ecosystem at scale.&lt;/p&gt;
&lt;h2 id=&quot;s-hiring-an-executive-director&quot;&gt;Hiring an Executive Director&lt;/h2&gt;
&lt;p&gt;For most of its history, the DSF has been powered almost entirely by volunteers, with board members handling fundraising, grants, trademarks, and operations on top of their day jobs. That commitment has carried the foundation a long way, but it also limits how much we can take on.&lt;/p&gt;
&lt;p&gt;That is why we are working toward hiring an Executive Director this year. An Executive Director would give the foundation dedicated, day-to-day leadership: someone who can build lasting relationships with sponsors, grow our fundraising programs, strengthen support for our volunteers and working groups, and turn the board's long-term plans into steady progress.&lt;/p&gt;
&lt;p&gt;We are optimistic about what this role would unlock. With dedicated operational support, the DSF could pursue larger partnerships, launch new programs, and respond more quickly to the community's needs. Reaching our fundraising goal is a key part of making that a reality.&lt;/p&gt;
&lt;h2 id=&quot;s-ways-to-support-django&quot;&gt;&lt;strong&gt;Ways to Support Django&lt;/strong&gt;&lt;/h2&gt;
&lt;h3 id=&quot;s-sponsored-fellow-the-highest-impact-way-to-support-django&quot;&gt;&lt;strong&gt;Sponsored Fellow: The Highest-Impact Way to Support Django&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;This year, the DSF is introducing a Sponsored Fellow corporate membership tier, a new way for organizations to make a direct, visible investment in Django's future.&lt;/p&gt;
&lt;p&gt;As a Sponsored Fellow sponsor, your company directly funds one of the Django Fellows who keep the framework running every day. In return, you receive the highest level of recognition the DSF offers. Depending on the partnership, that can include your company's logo and information featured in Django release announcements, recognition through the Fellows' work at conferences and community events, advertising opportunities across DSF communications, and visibility across DSF publications and promotional materials throughout the year.&lt;/p&gt;
&lt;p&gt;Django releases reach tens of thousands of developers. The Fellows represent Django at DjangoCon events around the world. If you want your company's name and logo in front of the global Django community, this is the most direct path to get there.&lt;/p&gt;
&lt;p&gt;This tier is designed for organizations that depend on Django at scale and want to do more than write a check. It is a partnership, and we will work with you to make sure your sponsorship is visible and meaningful.&lt;/p&gt;
&lt;p&gt;To learn more or start a conversation about the Sponsored Fellow tier, reach out through our &lt;a href=&quot;https://www.djangoproject.com/sponsor/&quot;&gt;Contact the DSF&lt;/a&gt; page.&lt;/p&gt;
&lt;h3 id=&quot;s-corporate-membership&quot;&gt;&lt;strong&gt;Corporate Membership&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Corporate membership is a proven way for organizations to support the DSF. Tiers range from Bronze at $2,000 per year up to Platinum at $150,000 per year. Member organizations receive recognition on djangoproject.com, benefits in our community channels, and the knowledge that they are directly funding the framework their teams depend on.&lt;/p&gt;
&lt;p&gt;To learn more or get started, visit &lt;a href=&quot;https://www.djangoproject.com/foundation/corporate-membership/&quot;&gt;djangoproject.com/foundation/corporate-membership/&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id=&quot;s-individual-donations&quot;&gt;&lt;strong&gt;Individual Donations&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Individual donations add up. Whether it is a one-time gift or a small monthly contribution, every bit helps us reach our monthly target and plan ahead with more confidence.&lt;/p&gt;
&lt;p&gt;You can donate via our &lt;a href=&quot;https://www.djangoproject.com/fundraising/&quot;&gt;donate page&lt;/a&gt; or through &lt;a href=&quot;https://opencollective.com/django&quot;&gt;Open Collective&lt;/a&gt;, which we added last year to make recurring donations easier.&lt;/p&gt;
&lt;h3 id=&quot;s-employer-donation-matching&quot;&gt;&lt;strong&gt;Employer Donation Matching&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;Many companies offer donation matching programs that can double or even triple the impact of an individual contribution. If your employer has a matching program, the DSF is typically eligible. Check with your HR or finance team and put that benefit to work.&lt;/p&gt;
&lt;h3 id=&quot;s-github-sponsors&quot;&gt;&lt;strong&gt;GitHub Sponsors&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;We have also raised our &lt;a href=&quot;https://github.com/sponsors/django&quot;&gt;GitHub Sponsors&lt;/a&gt; goal to $15,000 per month to better reflect the level of ongoing support Django needs. We are currently over $9,000 per month, so we are well on our way, but there is still ground to cover. If you already sponsor Django through GitHub, thank you. If you have been thinking about it, now is a great time to start.&lt;/p&gt;
&lt;p&gt;Thanks to all our existing sponsors and donors, Django has been able to sustain community initiatives over the past several years.&lt;/p&gt;
&lt;h3 id=&quot;s-spread-the-word&quot;&gt;&lt;strong&gt;Spread the Word&lt;/strong&gt;&lt;/h3&gt;
&lt;p&gt;If you cannot contribute financially right now, you can still help by spreading the word. Share this post. Mention Django's funding needs the next time someone asks how to give back to open source. Tell your employer about corporate membership.&lt;/p&gt;
&lt;h2 id=&quot;s-a-note-on-transparency&quot;&gt;&lt;strong&gt;A Note on Transparency&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;We publish monthly balance snapshots in our &lt;a href=&quot;https://github.com/django/dsf-minutes&quot;&gt;board minutes&lt;/a&gt;. The foundation started 2026 with around $222,000 in operating reserves. We take stewardship of those funds seriously, and you should always be able to see where we stand. Those reserves help ensure continuity of operations and provide financial stability for the foundation's ongoing commitments.&lt;/p&gt;
&lt;h2 id=&quot;s-looking-ahead&quot;&gt;&lt;strong&gt;Looking Ahead&lt;/strong&gt;&lt;/h2&gt;
&lt;p&gt;A significant portion of our funding comes directly from the community through individual donations, memberships, sponsorships, and fundraising campaigns. That ongoing support is one of the clearest signals that Django still matters to the people who build with it every day, and we are deeply grateful for it.&lt;/p&gt;
&lt;p&gt;Every Django release, security advisory, ticket review, and mentoring interaction represents countless hours of work from people who care deeply about the framework and community. The DSF exists to make sure that work remains sustainable and that contributors have the support they need to keep Django healthy for everyone who depends on it.&lt;/p&gt;
&lt;p&gt;Raising our goal is not about growth for growth's sake. It is about stability, sustainability, and making sure the project, the Fellows, and the broader community have what they need for the years ahead.&lt;/p&gt;
&lt;p&gt;We believe $500,000 is achievable. If you have ever benefited from Django, professionally or personally, now is a great time to give back.&lt;/p&gt;
&lt;p&gt;Thank you for being part of this community.&lt;/p&gt;</description>
	<pubDate>Wed, 10 Jun 2026 20:00:00 +0000</pubDate>
</item>
<item>
	<title>Mike Driscoll: How to Get TIFF MetaData with Python</title>
	<guid>https://blog.pythonlibrary.org/2026/06/10/how-to-get-tiff-metadata-with-python/</guid>
	<link>https://blog.pythonlibrary.org/2026/06/10/how-to-get-tiff-metadata-with-python/</link>
	<description>&lt;p&gt;In previous &lt;a href=&quot;https://blog.pythonlibrary.org/2010/03/28/getting-photo-metadata-exif-using-python/&quot;&gt;articles&lt;/a&gt; on this website, you learned how to extract EXIF data from JPG image files. This week, you will learn how to get similar data from the TIFF image format.&lt;/p&gt;
&lt;p&gt;The TIFF format also has its metadata. Pillow provides a similar dictionary for TIFF images in its &lt;code&gt;TiffTags&lt;/code&gt; module. If you need a TIFF image, you can use this one, which is a cover from one of the author&amp;#8217;s other books on &lt;strong&gt;ReportLab&lt;/strong&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img class=&quot;aligncenter size-full wp-image-12796&quot; src=&quot;https://blog.pythonlibrary.org/wp-content/uploads/2026/06/reportlab.png&quot; alt=&quot;ReportLab cover&quot; width=&quot;546&quot; height=&quot;762&quot; /&gt;&lt;/p&gt;
&lt;p&gt;You can create your own TIFF metadata extractor utility by making a new file named &lt;code&gt;tiff_metadata.py&lt;/code&gt; and adding this code to it:&lt;/p&gt;
&lt;pre class=&quot;EnlighterJSRAW&quot;&gt;# tiff_metadata.py

from PIL import Image
from PIL.TiffTags import TAGS


def get_metadata(image_file_path):
    image = Image.open(image_file_path)
    metadata = {}
    for tag in image.tag.items():
        metadata[TAGS.get(tag[0])] = tag[1]
    return metadata


if __name__ == &quot;__main__&quot;:
    metadata = get_metadata(&quot;reportlab_cover.tiff&quot;)
    print(metadata)&lt;/pre&gt;
&lt;p dir=&quot;auto&quot;&gt;Here you import the &lt;code&gt;TAGS&lt;/code&gt; dictionary from the &lt;code&gt;PIL.TiffTags&lt;/code&gt; submodule. Then in &lt;code&gt;get_metadata()&lt;/code&gt;, you access the tag elements in the image by iterating over the contents of &lt;code&gt;tag.items()&lt;/code&gt;. To make that information more readable, you use the &lt;code&gt;TAGS&lt;/code&gt; dictionary that you imported.&lt;/p&gt;
&lt;p dir=&quot;auto&quot;&gt;Here is a sample of the output you will get when you run this code:&lt;/p&gt;
&lt;pre class=&quot;EnlighterJSRAW&quot;&gt;{'ImageWidth': (400,),
 'ImageLength': (562,),
 'BitsPerSample': (8, 8, 8),
 'Compression': (1,),
 'PhotometricInterpretation': (2,),
 'FillOrder': (1,),
 'StripOffsets': (82, 130882, 261682, 392482, 523282, 654082),
 'Orientation': (1,),
 'SampleFormat': (1, 1, 1),
 'SamplesPerPixel': (3,),
 'RowsPerStrip': (109,),
 'StripByteCounts': (130800, 130800, 130800, 130800, 130800, 20400),
 'XResolution': ((300, 1),),
 'YResolution': ((300, 1),),
 'PlanarConfiguration': (1,),
 'ResolutionUnit': (2,),
 'ExifIFD': (8,),
 'Software': ('Pixelmator 3.9',),
 'DateTime': ('2020:10:27 12:10:37',),
}&lt;/pre&gt;
&lt;p&gt;You can see that the value entries above are all tuples. This is because of how the data is returned from the tag data. If you would like a challenge, you can attempt to clean up this data a bit in your version of the metadata extraction utility.&lt;/p&gt;
&lt;h2 class=&quot;header-anchor-post&quot;&gt;Wrapping Up&lt;/h2&gt;
&lt;div class=&quot;pencraft pc-display-flex pc-alignItems-center pc-position-absolute pc-reset header-anchor-parent&quot;&gt;
&lt;div class=&quot;pencraft pc-display-contents pc-reset pubTheme-yiXxQA&quot;&gt;
&lt;div id=&quot;§wrapping-up&quot; class=&quot;pencraft pc-reset header-anchor offset-top&quot;&gt;&lt;span&gt;EXIF and TIFF metadata are really useful for encoding lots of information in your images. However, most people don’t even know that data is there! Knowing how to access your photo’s metadata allows you to do all kinds of programmatic tasks, such as resizing, sorting files by various parameters, and much more.&lt;/span&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;You can use Pillow and Python to do all kinds of other image processing, so this is just scratching the surface. Download Pillow today and start learning!&lt;/p&gt;
&lt;h2 class=&quot;header-anchor-post&quot;&gt;&lt;strong&gt;Want to Learn More?&lt;/strong&gt;&lt;/h2&gt;
&lt;div class=&quot;pencraft pc-display-flex pc-alignItems-center pc-position-absolute pc-reset header-anchor-parent&quot;&gt;
&lt;div class=&quot;pencraft pc-display-contents pc-reset pubTheme-yiXxQA&quot;&gt;&lt;span&gt;You can learn more about what you can do with Python and Pillow in Mike’s book, &lt;/span&gt;&lt;strong&gt;Pillow: Image Processing with Python&lt;/strong&gt;&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Purchase at &lt;a href=&quot;https://driscollis.gumroad.com/l/pypillow&quot;&gt;Gumroad&lt;/a&gt;, &lt;a href=&quot;https://leanpub.com/pillow&quot;&gt;Leanpub&lt;/a&gt;, or &lt;a href=&quot;https://amzn.to/3ZvLfav&quot;&gt;Amazon&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The post &lt;a href=&quot;https://blog.pythonlibrary.org/2026/06/10/how-to-get-tiff-metadata-with-python/&quot;&gt;How to Get TIFF MetaData with Python&lt;/a&gt; appeared first on &lt;a href=&quot;https://blog.pythonlibrary.org&quot;&gt;Mouse Vs Python&lt;/a&gt;.&lt;/p&gt;</description>
	<pubDate>Wed, 10 Jun 2026 19:15:42 +0000</pubDate>
</item>
<item>
	<title>Python Morsels: Stacks and queues in Python</title>
	<guid>https://www.pythonmorsels.com/stacks-and-queues/</guid>
	<link>https://www.pythonmorsels.com/stacks-and-queues/</link>
	<description>&lt;p&gt;Use a Python list for stack operations (last-in, first-out) and a &lt;code&gt;deque&lt;/code&gt; from the &lt;code&gt;collections&lt;/code&gt; module for queue operations (first-in, first-out).&lt;/p&gt;


&lt;div&gt;
  
    &lt;a href=&quot;https://www.pythonmorsels.com/stacks-and-queues/&quot;&gt;&lt;img width=&quot;480&quot; height=&quot;270&quot; src=&quot;https://i.vimeocdn.com/filter/overlay?src0=https%3A%2F%2Fi.vimeocdn.com%2Fvideo%2F2167158606-f99d799f0637fd953d7cc87b7a735dbf8ae232db8f1099f1dd4a9ac594aa576c-d_1920x1080%3F%26region%3Dus&amp;src1=http%3A%2F%2Ff.vimeocdn.com%2Fp%2Fimages%2Fcrawler_play.png&quot; /&gt;&lt;/a&gt;
  
  &lt;p&gt;
  &lt;strong&gt;Table of contents&lt;/strong&gt;
  &lt;ol&gt;
  
  &lt;li&gt;&lt;a href=&quot;https://www.pythonmorsels.com/stacks-and-queues/#stacks-versus-queues&quot; target=&quot;_blank&quot;&gt;Stacks versus Queues&lt;/a&gt;&lt;/li&gt;
  
  &lt;li&gt;&lt;a href=&quot;https://www.pythonmorsels.com/stacks-and-queues/#stacks-in-python&quot; target=&quot;_blank&quot;&gt;Stacks in Python&lt;/a&gt;&lt;/li&gt;
  
  &lt;li&gt;&lt;a href=&quot;https://www.pythonmorsels.com/stacks-and-queues/#queues-in-python&quot; target=&quot;_blank&quot;&gt;Queues in Python&lt;/a&gt;&lt;/li&gt;
  
  &lt;li&gt;&lt;a href=&quot;https://www.pythonmorsels.com/stacks-and-queues/#a-deque-is-a-double-ended-queue&quot; target=&quot;_blank&quot;&gt;A &lt;code&gt;deque&lt;/code&gt; is a &quot;double-ended queue&quot;&lt;/a&gt;&lt;/li&gt;
  
  &lt;li&gt;&lt;a href=&quot;https://www.pythonmorsels.com/stacks-and-queues/#stack-like-and-queue-like-operations&quot; target=&quot;_blank&quot;&gt;Stack-like and queue-like operations&lt;/a&gt;&lt;/li&gt;
  
  &lt;/ol&gt;
  &lt;/p&gt;
&lt;/div&gt;
&lt;div&gt;
  
    &lt;h2&gt;Stacks versus Queues&lt;/h2&gt;
    
      
        &lt;p&gt;In Computer Science, stacks and queues are data structures that are optimized to make it inexpensive to remove either the &lt;em&gt;most&lt;/em&gt; recently added item &lt;em&gt;or&lt;/em&gt; the &lt;em&gt;least&lt;/em&gt; recently added item.&lt;/p&gt;

      
        &lt;p&gt;A queue is often called a &lt;strong&gt;FIFO&lt;/strong&gt; data structure: &lt;strong&gt;first in, first out&lt;/strong&gt;.&lt;/p&gt;

      
        &lt;p&gt;You can think of a queue as... well, a queue.
Or a &quot;line&quot;, for Americans like me.
The first person to enter a queue will be the first person to reach the front of the queue.&lt;/p&gt;

      
        &lt;p&gt;And in programming queues, &lt;strong&gt;the first item added will be the first item removed&lt;/strong&gt;.&lt;/p&gt;

      
        &lt;p&gt;A stack is often called a &lt;strong&gt;LIFO&lt;/strong&gt; data structure: &lt;strong&gt;last in, first out&lt;/strong&gt;.&lt;/p&gt;

      
        &lt;p&gt;You can think of a stack as a stack of plates... specifically one of those spring-loaded ones from a self-service lunch counter.
The last plate that's added to the top of the stack will be the first plate removed from the top of the stack.&lt;/p&gt;

      
        &lt;p&gt;And in programming stacks, &lt;strong&gt;the last item added will be the first item that's removed&lt;/strong&gt;.&lt;/p&gt;

      
        &lt;p&gt;But how do these terms apply to Python?&lt;/p&gt;

      
    
  
    &lt;h2&gt;Stacks in Python&lt;/h2&gt;
    
      &lt;p&gt;You can think of &lt;a href=&quot;https://www.pythonmorsels.com/what-are-lists/&quot; target=&quot;_blank&quot;&gt;Python …&lt;/a&gt;&lt;/p&gt;
    
  
&lt;/div&gt;
&lt;h3&gt;&lt;a href=&quot;https://www.pythonmorsels.com/stacks-and-queues/&quot; target=&quot;_blank&quot;&gt;Read the full article: https://www.pythonmorsels.com/stacks-and-queues/&lt;/a&gt;&lt;/h3&gt;</description>
	<pubDate>Wed, 10 Jun 2026 16:30:00 +0000</pubDate>
</item>
<item>
	<title>Real Python: Cursor vs Windsurf: Which AI Code Editor Is Best for Python?</title>
	<guid>https://realpython.com/cursor-vs-windsurf-python/</guid>
	<link>https://realpython.com/cursor-vs-windsurf-python/</link>
	<description>&lt;div&gt;&lt;p&gt;AI-powered code editors have moved beyond novelty to become everyday tools for many Python developers. Instead of having to switch between your editor and a separate AI chat, you can use tools like Cursor and Windsurf that bring AI directly into your workflow. As a result, the Cursor vs Windsurf question is a common one for developers deciding which to adopt.&lt;/p&gt;
&lt;p&gt;Both Cursor and Windsurf are &lt;a href=&quot;https://realpython.com/ref/code-editors-ides/visual-studio-code/&quot; class=&quot;ref-link&quot;&gt;VS Code&lt;/a&gt; forks that import your keybindings, themes, and Python extensions, and both run the same frontier models. They look similar at first but diverge in how they handle changes as you build.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://realpython.com/ref/ai-coding-tools/cursor/&quot; class=&quot;ref-link&quot;&gt;Cursor&lt;/a&gt; focuses on control, surfacing AI-generated edits as reviewable diffs and relying on explicit rules to guide agent behavior. &lt;a href=&quot;https://realpython.com/ref/ai-coding-tools/windsurf/&quot; class=&quot;ref-link&quot;&gt;Windsurf&lt;/a&gt; focuses on flow, applying edits directly in the editor while using broader workspace context, including terminal output, recent edits, and conversation history, to shape its behavior.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;In this tutorial, you’ll compare both editors across:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;AI code completion:&lt;/strong&gt; How each editor’s completion system behaves and what context it draws on&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agentic multi-file editing:&lt;/strong&gt; How each editor handles tasks involving multiple files, directories, and the terminal&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debugging and error correction:&lt;/strong&gt; How each editor reviews generated code and integrates with your linter&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By the end, you’ll have a clear picture of which editor fits your Python workflow. If you’re coming from VS Code, the &lt;a href=&quot;https://realpython.com/python-development-visual-studio-code/&quot;&gt;Python Development in Visual Studio Code&lt;/a&gt; tutorial covers the baseline configuration that carries over to both forks.&lt;/p&gt;
&lt;p&gt;The table below helps you choose the right editor at a glance:&lt;/p&gt;
&lt;div class=&quot;table-responsive&quot;&gt;
&lt;table class=&quot;table table-hover&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th class=&quot;text-left&quot;&gt;Use case&lt;/th&gt;
&lt;th class=&quot;text-left&quot;&gt;Cursor&lt;/th&gt;
&lt;th class=&quot;text-left&quot;&gt;Windsurf&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class=&quot;text-left&quot;&gt;You want AI-generated changes shown as reviewable diffs before they’re written to your files, guided by explicit rules&lt;/td&gt;
&lt;td class=&quot;text-left&quot;&gt;✅&lt;/td&gt;
&lt;td class=&quot;text-left&quot;&gt;—&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td class=&quot;text-left&quot;&gt;You want edits applied directly as the agent works, using a broader workspace context (terminal output, recent edits, conversation history, and memory)&lt;/td&gt;
&lt;td class=&quot;text-left&quot;&gt;—&lt;/td&gt;
&lt;td class=&quot;text-left&quot;&gt;✅&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;Cursor is the better fit if you want to review changes before they’re applied. Windsurf is the better fit if you prefer the agent to apply edits directly in your files as it works, drawing on the broader workspace context. To see how this plays out in completion, context management, and debugging, read on.&lt;/p&gt;
&lt;div class=&quot;alert alert-warning&quot;&gt;
&lt;p&gt;&lt;strong&gt;Get Your Code:&lt;/strong&gt; &lt;a href=&quot;https://realpython.com/bonus/cursor-vs-windsurf-python-code/&quot; class=&quot;alert-link&quot;&gt;Click here to download the free sample code&lt;/a&gt; for the resilient HTTP client you’ll build with Cursor and Windsurf in this tutorial.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;container border rounded text-wrap-pretty my-3&quot;&gt;

  &lt;p class=&quot;my-3&quot;&gt;&lt;strong&gt;&lt;span class=&quot;icon baseline&quot;&gt;&lt;/span&gt; Take the Quiz:&lt;/strong&gt; Test your knowledge with our interactive “Cursor vs Windsurf: Which AI Code Editor Is Best for Python?” quiz. You’ll receive a score upon completion to help you track your learning progress:&lt;/p&gt;

  &lt;hr /&gt;

  &lt;div class=&quot;row my-3&quot;&gt;
    &lt;div class=&quot;col-xs-12 col-sm-4 col-md-3 align-self-center&quot;&gt;

      &lt;a href=&quot;https://realpython.com/quizzes/cursor-vs-windsurf-python/&quot; tabindex=&quot;-1&quot;&gt;
        &lt;div class=&quot;embed-responsive embed-responsive-16by9&quot;&gt;

            &lt;img class=&quot;card-img-top m-0 p-0 embed-responsive-item rounded&quot; alt=&quot;Two people operating a factory machine with conveyor belts and panels labeled Manual Control Center and Automatic Flow Center, with a Python logo on a chip.&quot; src=&quot;https://files.realpython.com/media/Windsurf-vs-Cursor-Which-AI-Powered-Code-Editor-Is-Best-for-Python_Watermarked.5c2bcb8d1965.jpg&quot; width=&quot;1920&quot; height=&quot;1080&quot; /&gt;


          &lt;div class=&quot;card-img-overlay d-flex align-items-center&quot;&gt;
            &lt;div class=&quot;mx-auto&quot;&gt;
              &lt;span class=&quot;text-light&quot;&gt;&lt;span class=&quot;icon baseline scale2x&quot;&gt;&lt;/span&gt;&lt;/span&gt;
            &lt;/div&gt;
          &lt;/div&gt;
        &lt;/div&gt;
      &lt;/a&gt;

    &lt;/div&gt;
    &lt;div class=&quot;col&quot;&gt;
      &lt;div class=&quot;mt-3 d-md-none&quot;&gt;&lt;/div&gt; 
      &lt;p class=&quot;small text-muted mb-0&quot;&gt;&lt;strong&gt;Interactive Quiz&lt;/strong&gt;&lt;/p&gt;
      &lt;a href=&quot;https://realpython.com/quizzes/cursor-vs-windsurf-python/&quot; class=&quot;stretched-link&quot;&gt;&lt;span class=&quot;my-0 h4&quot;&gt;Cursor vs Windsurf: Which AI Code Editor Is Best for Python?&lt;/span&gt;&lt;/a&gt; 
      &lt;p class=&quot;text-muted mb-0 small&quot;&gt;Test your understanding of how Cursor and Windsurf compare for Python across AI completion, agentic edits, and debugging workflows.&lt;/p&gt;
    &lt;/div&gt;
  &lt;/div&gt;

&lt;/div&gt;

&lt;h2 id=&quot;metrics-comparison-cursor-vs-windsurf&quot;&gt;Metrics Comparison: Cursor vs Windsurf&lt;a class=&quot;headerlink&quot; href=&quot;https://realpython.com/atom.xml#metrics-comparison-cursor-vs-windsurf&quot; title=&quot;Permanent link&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;As you work through the hands-on sections and eventually bring either editor into your own Python projects, the table below gives you a quick reference for some key differences you might expect from each tool:&lt;/p&gt;
&lt;div class=&quot;table-responsive&quot;&gt;
&lt;table class=&quot;table table-hover&quot;&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Cursor&lt;/th&gt;
&lt;th&gt;Windsurf&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;IDE support&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Standalone VS Code fork plus a &lt;a href=&quot;https://plugins.jetbrains.com/plugin/30583-cursorj&quot;&gt;JetBrains plugin&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;Standalone VS Code fork plus &lt;a href=&quot;https://docs.windsurf.com/plugins/compatibility#supported-ides-and-versions&quot;&gt;plugins&lt;/a&gt; for JetBrains IDEs, Vim, Neovim, Xcode, Visual Studio, and more&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;AI code completion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Fast, line-by-line prediction; strong on single-file typed structures&lt;/td&gt;
&lt;td&gt;Slower but more structurally aware across interconnected files&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Startup performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Faster. Uses lightweight text search that requires no upfront project indexing.&lt;/td&gt;
&lt;td&gt;Slower initial response. Builds a semantic map of your project structure before it begins.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Debugging performance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Identifies and fixes the root cause in one pass&lt;/td&gt;
&lt;td&gt;Reaches passing tests by working around the root cause over multiple iterations&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Resource impact&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Light. Low background CPU and RAM usage.&lt;/td&gt;
&lt;td&gt;Heavy. Background indexing can spike local CPU during initial project load.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Billing model&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Monthly credit pool with unlimited &lt;em&gt;Tab&lt;/em&gt; and &lt;em&gt;Auto&lt;/em&gt; mode on Pro&lt;/td&gt;
&lt;td&gt;Daily and weekly usage quotas that refresh automatically on a schedule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pro plan pricing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;$20/month&lt;/td&gt;
&lt;td&gt;$20/month&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Ideal project size&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Small to medium codebases where you already know the structure and can target files manually&lt;/td&gt;
&lt;td&gt;Large, highly interconnected codebases that benefit from its RAG-based context engine and automatic semantic indexing&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;/div&gt;
&lt;p&gt;In the next sections, you’ll build a resilient HTTP client in Python from scratch and then send the same prompts to both editors to compare their responses.&lt;/p&gt;
&lt;h2 id=&quot;getting-started-installation&quot;&gt;Getting Started: Installation&lt;a class=&quot;headerlink&quot; href=&quot;https://realpython.com/atom.xml#getting-started-installation&quot; title=&quot;Permanent link&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;Both editors ship as standalone desktop applications that closely match the VS Code experience. On first launch, they offer to import your local VS Code configuration, copying your keybindings, extensions, themes, and settings so your environment carries over with minimal setup.&lt;/p&gt;
&lt;p&gt;To follow the hands-on project later in this tutorial, you’ll also want Python 3.12 or later installed on your system. Beyond that, if you need a full VS Code baseline before starting, the &lt;a href=&quot;https://realpython.com/courses/python-development-visual-studio-code-setup-guide/&quot;&gt;Python Development in Visual Studio Code (Setup Guide)&lt;/a&gt; course covers the editor setup from scratch.&lt;/p&gt;
&lt;p&gt;Both Cursor and Windsurf offer free plans with enough model access to work through this comparison, though keep in mind that free-tier usage is limited and may run out under heavy use.&lt;/p&gt;
&lt;h3 id=&quot;installing-cursor&quot;&gt;Installing Cursor&lt;a class=&quot;headerlink&quot; href=&quot;https://realpython.com/atom.xml#installing-cursor&quot; title=&quot;Permanent link&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Head to the &lt;a href=&quot;https://cursor.com/download&quot;&gt;Cursor download page&lt;/a&gt; and download the correct version for your system. During setup, Cursor offers to import your VS Code configuration, including extensions, keybindings, and themes, so your environment carries over with minimal setup.&lt;/p&gt;
&lt;p&gt;Once the editor opens, you’re ready to go. You don’t need to configure anything else yet.&lt;/p&gt;
&lt;p&gt;If Cursor is new to you, Real Python’s video course on &lt;a href=&quot;https://realpython.com/courses/tips-using-ai-coding-editor-cursor/&quot;&gt;Tips for Using the AI Coding Editor Cursor&lt;/a&gt; covers setup, &lt;em&gt;Agent&lt;/em&gt; mode, &lt;em&gt;Plan&lt;/em&gt; mode, and model selection in a practical context, making the comparisons later in this tutorial easier to follow.&lt;/p&gt;
&lt;h3 id=&quot;installing-windsurf&quot;&gt;Installing Windsurf&lt;a class=&quot;headerlink&quot; href=&quot;https://realpython.com/atom.xml#installing-windsurf&quot; title=&quot;Permanent link&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Download Windsurf from the &lt;a href=&quot;https://windsurf.com/download&quot;&gt;Windsurf download page&lt;/a&gt; and run the installer. The VS Code profile import works identically to Cursor’s.&lt;/p&gt;
&lt;/div&gt;&lt;h2&gt;&lt;a href=&quot;https://realpython.com/cursor-vs-windsurf-python/?utm_source=realpython&amp;utm_medium=rss&quot;&gt;Read the full article at https://realpython.com/cursor-vs-windsurf-python/ »&lt;/a&gt;&lt;/h2&gt;
        &lt;hr /&gt;
        &lt;p&gt;&lt;em&gt;[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short &amp;amp; sweet Python Trick delivered to your inbox every couple of days. &lt;a href=&quot;https://realpython.com/python-tricks/?utm_source=realpython&amp;utm_medium=rss&amp;utm_campaign=footer&quot;&gt;&amp;gt;&amp;gt; Click here to learn more and see examples&lt;/a&gt; ]&lt;/em&gt;&lt;/p&gt;</description>
	<pubDate>Wed, 10 Jun 2026 14:00:00 +0000</pubDate>
</item>
<item>
	<title>Python GUIs: How to Set Row Background Colors in a QTableView — Use Qt's BackgroundRole to color entire rows based on your data</title>
	<guid>https://www.pythonguis.com/faq/backgroundcolor-for-row-in-qtableview/</guid>
	<link>https://www.pythonguis.com/faq/backgroundcolor-for-row-in-qtableview/</link>
	<description>&lt;blockquote&gt;
&lt;p&gt;I have a QTableView table showing some data about connected devices. How can I highlight rows to give a visual indicator of the current status of the device?&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;When you're working with a &lt;code&gt;QTableView&lt;/code&gt; and a custom model, it's common to want to highlight entire rows based on some condition in your data. For example, you might want to color a row blue when a device has a &lt;em&gt;connected&lt;/em&gt; status, or red when something has gone wrong.&lt;/p&gt;
&lt;h2 id=&quot;understanding-how-data-works&quot;&gt;Understanding How &lt;code&gt;data()&lt;/code&gt; Works&lt;/h2&gt;
&lt;p&gt;In Qt's &lt;a href=&quot;https://www.pythonguis.com/tutorials/modelview-architecture/&quot;&gt;Model/View architecture&lt;/a&gt;, the view calls your model's &lt;code&gt;data()&lt;/code&gt; method for &lt;em&gt;every cell&lt;/em&gt; in the table &amp;mdash; and for each cell, it asks about multiple &lt;strong&gt;roles&lt;/strong&gt;. One of those roles is &lt;code&gt;Qt.BackgroundRole&lt;/code&gt;, which tells the view what background color to use for that cell.&lt;/p&gt;
&lt;p&gt;The view asks for &lt;code&gt;Qt.BackgroundRole&lt;/code&gt; on every single cell, not just one column. So if your &lt;code&gt;data()&lt;/code&gt; method returns a color for &lt;code&gt;Qt.BackgroundRole&lt;/code&gt; based on the &lt;em&gt;row&lt;/em&gt; data (ignoring the column), the color will be applied to every cell in that row.&lt;/p&gt;
&lt;p&gt;Let's build a working example.&lt;/p&gt;
&lt;h2 id=&quot;a-complete-working-example&quot;&gt;A Complete Working Example&lt;/h2&gt;
&lt;p&gt;Here's a full example you can run directly. It creates a &lt;code&gt;QTableView&lt;/code&gt; with colored rows based on the &lt;code&gt;PRESENT_STATUS&lt;/code&gt; field in each row of data:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;import sys
from typing import Union

from PyQt6.QtCore import QAbstractTableModel, QModelIndex, Qt
from PyQt6.QtGui import QColor
from PyQt6.QtWidgets import QApplication, QMainWindow, QTableView


class TableModel(QAbstractTableModel):

    def __init__(self, data: Union[list, None] = None):
        super().__init__()
        self._data = data or []
        self._hdr = self._gen_hdr_data() if data else []
        self._base_color = {
            &quot;NewConnection&quot;: QColor(&quot;blue&quot;),
            &quot;Registered&quot;: QColor(&quot;green&quot;),
        }

    def _gen_hdr_data(self):
        &quot;&quot;&quot;Build a sorted list of all unique keys across all row dicts.&quot;&quot;&quot;
        all_keys = set()
        for d in self._data:
            all_keys.update(d.keys())
        return sorted(all_keys)

    def rowCount(self, parent=QModelIndex()):
        return len(self._data)

    def columnCount(self, parent=QModelIndex()):
        return len(self._hdr)

    def headerData(self, section, orientation, role):
        if role == Qt.DisplayRole and orientation == Qt.Horizontal:
            return self._hdr[section]

    def data(self, index: QModelIndex, role: int):
        if not index.isValid():
            return None

        row_dict = self._data[index.row()]
        state = row_dict.get(&quot;PRESENT_STATUS&quot;, &quot;&quot;)

        if role == Qt.DisplayRole:
            col_key = self._hdr[index.column()]
            value = row_dict.get(col_key, &quot;&quot;)
            return str(value) if value else &quot;&quot;

        if role == Qt.BackgroundRole:
            color = self._base_color.get(state)
            if color:
                return color

        return None


class MainWindow(QMainWindow):
    def __init__(self):
        super().__init__()
        self.setWindowTitle(&quot;Row Background Colors in QTableView&quot;)

        data = [
            {&quot;IP&quot;: &quot;192.168.1.10&quot;, &quot;PRESENT_STATUS&quot;: &quot;NewConnection&quot;},
            {&quot;IP&quot;: &quot;192.168.1.108&quot;, &quot;FORMER_STATUS&quot;: &quot;NewConnection&quot;,
             &quot;PRESENT_STATUS&quot;: &quot;Registered&quot;},
            {&quot;IP&quot;: &quot;192.168.1.50&quot;, &quot;PRESENT_STATUS&quot;: &quot;Unknown&quot;},
        ]

        self.table = QTableView()
        model = TableModel(data)
        self.table.setModel(model)
        self.setCentralWidget(self.table)


app = QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec()
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p class=&quot;admonition admonition-note&quot;&gt;&lt;span class=&quot;admonition-kind&quot;&gt;&lt;i class=&quot;fas fa-sticky-note&quot;&gt;&lt;/i&gt;&lt;/span&gt;  The method that Qt calls on the model is called &lt;code&gt;data&lt;/code&gt;, so in the example above, the list is stored as &lt;code&gt;self._data&lt;/code&gt; (with a leading underscore) to avoid this.&lt;/p&gt;
&lt;p&gt;Run this and you'll see three rows. The first row (&quot;NewConnection&quot;) has a blue background, the second row (&quot;Registered&quot;) has a green background, and the third row (&quot;Unknown&quot;) has no special coloring because it isn't in the &lt;code&gt;_base_color&lt;/code&gt; dictionary.&lt;/p&gt;
&lt;p&gt;&lt;img alt=&quot;QTableView with colored rows based on status values&quot; src=&quot;https://www.pythonguis.com/feeds/images/qtableview-row-background-colors.png&quot; /&gt;&lt;/p&gt;
&lt;h2 id=&quot;how-colors-are-set-on-rows&quot;&gt;How Colors are Set on Rows&lt;/h2&gt;
&lt;p&gt;To understand how the color is being set to the entire row, take a look at the  &lt;code&gt;Qt.BackgroundRole&lt;/code&gt; section of &lt;code&gt;data()&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;if role == Qt.BackgroundRole:
    color = self._base_color.get(state)
    if color:
        return color
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Notice that &lt;code&gt;index.column()&lt;/code&gt; isn't used here at all. The color decision is based entirely on the row's &lt;code&gt;PRESENT_STATUS&lt;/code&gt; value. Since the view calls &lt;code&gt;data()&lt;/code&gt; for &lt;em&gt;every cell&lt;/em&gt; in the row &amp;mdash; column 0, column 1, column 2, etc. &amp;mdash; and each call gets the same color back, the entire row ends up painted.&lt;/p&gt;
&lt;p&gt;If you &lt;em&gt;only&lt;/em&gt; wanted to color a specific column (say, just the status column), you would add a column check:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;if role == Qt.BackgroundRole:
    # Only color the PRESENT_STATUS column
    if self._hdr[index.column()] == &quot;PRESENT_STATUS&quot;:
        color = self._base_color.get(state)
        if color:
            return color
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;h2 id=&quot;making-the-text-readable&quot;&gt;Making the Text Readable&lt;/h2&gt;
&lt;p&gt;One thing you'll notice with a dark background color like blue is that the default black text becomes hard to read. You can fix this by also handling &lt;code&gt;Qt.ForegroundRole&lt;/code&gt; and returning a light text color when the background is dark:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;def data(self, index: QModelIndex, role: int):
    if not index.isValid():
        return None

    row_dict = self._data[index.row()]
    state = row_dict.get(&quot;PRESENT_STATUS&quot;, &quot;&quot;)

    if role == Qt.DisplayRole:
        col_key = self._hdr[index.column()]
        value = row_dict.get(col_key, &quot;&quot;)
        return str(value) if value else &quot;&quot;

    if role == Qt.BackgroundRole:
        color = self._base_color.get(state)
        if color:
            return color

    if role == Qt.ForegroundRole:
        # If this row has a background color, use white text.
        if state in self._base_color:
            return QColor(&quot;white&quot;)

    return None
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;Now blue and green rows will have white text, making everything easy to read.&lt;/p&gt;
&lt;h2 id=&quot;updating-colors-dynamically&quot;&gt;Updating Colors Dynamically&lt;/h2&gt;
&lt;p&gt;If your data changes at runtime &amp;mdash; for example, a device's status changes from &lt;code&gt;&quot;NewConnection&quot;&lt;/code&gt; to &lt;code&gt;&quot;Registered&quot;&lt;/code&gt; &amp;mdash; you need to tell the view that something has changed so it repaints. You do this by emitting the &lt;code&gt;dataChanged&lt;/code&gt; signal:&lt;/p&gt;
&lt;div class=&quot;code-block&quot;&gt;
&lt;span class=&quot;code-block-language code-block-python&quot;&gt;python&lt;/span&gt;
&lt;pre&gt;&lt;code class=&quot;python&quot;&gt;def update_status(self, row, new_status):
    self._data[row][&quot;PRESENT_STATUS&quot;] = new_status
    # Emit dataChanged for the entire row.
    top_left = self.index(row, 0)
    bottom_right = self.index(row, self.columnCount() - 1)
    self.dataChanged.emit(top_left, bottom_right)
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;p&gt;This tells the view to re-query &lt;code&gt;data()&lt;/code&gt; for every cell in that row, which picks up both the new display text and the new background color. For a deeper look at how signals work to keep your model and view in sync, see &lt;a href=&quot;https://www.pythonguis.com/tutorials/pyqt-signals-slots-events/&quot;&gt;Signals, Slots &amp;amp; Events&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id=&quot;summary&quot;&gt;Summary&lt;/h2&gt;
&lt;p&gt;Once you understand how the model's &lt;code&gt;data()&lt;/code&gt; method works, coloring entire rows in a &lt;code&gt;QTableView&lt;/code&gt; is relatively straightforward. The view asks for each role on every cell, so returning a color from &lt;code&gt;Qt.BackgroundRole&lt;/code&gt; based on row-level data &amp;mdash; without filtering by column &amp;mdash; naturally paints the whole row. Pair that with &lt;code&gt;Qt.ForegroundRole&lt;/code&gt; for readable text, and you've got a clean, data-driven way to highlight rows in your table.&lt;/p&gt;
&lt;p&gt;To learn more about using &lt;code&gt;QTableView&lt;/code&gt; with custom models and data from numpy or pandas, see the &lt;a href=&quot;https://www.pythonguis.com/tutorials/qtableview-modelviews-numpy-pandas/&quot;&gt;QTableView with numpy and pandas tutorial&lt;/a&gt;. If you want to add sorting and filtering to your table, take a look at &lt;a href=&quot;https://www.pythonguis.com/tutorials/pyqt6-modelview-sort-filter-tables/&quot;&gt;Sorting and Filtering Tables&lt;/a&gt;.&lt;/p&gt;
            &lt;p&gt;For an in-depth guide to building Python GUIs with PyQt6 see my book, &lt;a href=&quot;https://www.pythonguis.com/pyqt6-book/&quot;&gt;Create GUI Applications with Python &amp;amp; Qt6.&lt;/a&gt;&lt;/p&gt;</description>
	<pubDate>Wed, 10 Jun 2026 06:00:00 +0000</pubDate>
</item>
<item>
	<title>Python Insider: Python 3.14.6 and 3.13.14 are now available!</title>
	<guid>https://blog.python.org/2026/06/python-3146-31314/</guid>
	<link>https://blog.python.org/2026/06/python-3146-31314/</link>
	<description>A pair of bug fix releases await your upgrade.</description>
	<pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate>
</item>
<item>
	<title>Seth Michael Larson: Are insecure code completions a vulnerability?</title>
	<guid>https://sethmlarson.dev/are-insecure-code-completions-a-vulnerability?utm_campaign=rss</guid>
	<link>https://sethmlarson.dev/are-insecure-code-completions-a-vulnerability?utm_campaign=rss</link>
	<description>&lt;p&gt;Three months ago I saw that PyCharm shipped with a
“&lt;a href=&quot;https://www.jetbrains.com/help/pycharm/full-line-code-completion.html&quot;&gt;Full Line Completion&lt;/a&gt;” plugin that “uses a local deep
learning model to suggest entire lines of code”. These
suggestions manifest as whole-line suggestions after
you start typing and can be accepted with &lt;code&gt;Tab&lt;/code&gt;. Essentially
auto-complete for entire lines.&lt;/p&gt;

&lt;p&gt;I decide to test this functionality. I started by
writing &lt;code&gt;import urllib3&lt;/code&gt;, created a new line,
and then typed &lt;code&gt;u&lt;/code&gt; and received a suggested completion for the line
marked below with a 
&lt;span&gt;dashed border&lt;/span&gt;.
I was not impressed by the result:&lt;/p&gt;

&lt;!-- more --&gt;

&lt;div class=&quot;codehilite&quot;&gt;
&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;urllib3&lt;/span&gt;
&lt;span class=&quot;n&quot;&gt;u&lt;/span&gt;&lt;span&gt;&lt;span class=&quot;n&quot;&gt;rllib3&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;disable_warnings&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;urllib3&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;exceptions&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;InsecureRequestWarning&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;Accepting this line would mean that any insecure
requests made with &lt;code&gt;urllib3&lt;/code&gt; would not result in a user-visible warning.
I didn't accept this suggestion and then began to instantiate a
&lt;code&gt;urllib3.PoolManager&lt;/code&gt; and what I feared would come next was confirmed:&lt;/p&gt;

&lt;div class=&quot;codehilite&quot;&gt;
&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;urllib3&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;urllib3&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;PoolManager&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
&lt;span&gt;    &lt;span class=&quot;n&quot;&gt;cert_reqs&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'CERT_NONE'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;The suggestion offered to disable certificate verification (&lt;a href=&quot;https://docs.python.org/3/library/ssl.html#ssl.CERT_NONE&quot;&gt;&lt;code&gt;CERT_NONE&lt;/code&gt;&lt;/a&gt;) which
would make every request made by the &lt;code&gt;PoolManager&lt;/code&gt; susceptible to
monster-in-the-middle (MITM) attacks. Accepting this code as-is would
mean the program I am writing has a severe vulnerability. If I
had accepted the prior suggestion too, then &lt;code&gt;urllib3&lt;/code&gt; would
have no chance to warn the user about this mistake prior
to productionizing this code.&lt;/p&gt;

&lt;p&gt;Clearly &lt;em&gt;something&lt;/em&gt; insecure is going on here, but for a CVE
to be assigned we have to decide which software component is
vulnerable. Does this behavior warrant a CVE at all? I am not sure
which is unfortunate, without a security-angle to a bug
report companies are less likely to prioritize reports.&lt;/p&gt;

&lt;p&gt;I reported this behavior to JetBrains for “Full Line Code Completion” v253.29346.142
and clearly their support staff weren't certain whether this defect
was a security vulnerability or not either. When I asked to
publish a blog post about this behavior after they confirmed
this report wasn’t a “direct security vulnerability” (which
I agree with) but then was asked not to publicize my report and referred to
PyCharm’s &lt;a href=&quot;https://www.jetbrains.com/legal/docs/terms/coordinated-disclosure/&quot;&gt;Coordinated Disclosure Policy&lt;/a&gt;
so... which is it? Security vulnerability or not?&lt;/p&gt;

&lt;p&gt;I ended up waiting the 90 days anyway and I didn't hear
back with any substantive update from the development team. I double-checked again
today using “Full Line Code Completion” v261.24374.152
and the behavior is identical, suggesting the same
insecure code for both contexts. &lt;/p&gt;

&lt;p&gt;This isn’t meant to be a specific dig at PyCharm or JetBrains,
I have no-doubt that examples like this exist in every code generation
model available. I don’t think using CVEs for this purpose is
appropriate or helpful for users, either. But not prioritizing and addressing this
behavior at the source means more work to mitigate
the potential for insecure code to be accepted by users who are trusting
what is offered to them by their IDE.&lt;/p&gt;

&lt;p&gt;What do you think? I am interested in knowing your thoughts
about this specific class of issue with code generation models.&lt;/p&gt;
&lt;br /&gt;&lt;hr /&gt;&lt;p&gt;Thanks for reading ♥ I would love to hear your thoughts! Contact me via &lt;a href=&quot;https://mastodon.social/@sethmlarson&quot;&gt;Mastodon&lt;/a&gt;, &lt;a href=&quot;https://bsky.app/profile/sethmlarson.dev&quot;&gt;Bluesky&lt;/a&gt;, or &lt;a href=&quot;mailto:sethmichaellarson@gmail.com&quot;&gt;email&lt;/a&gt;. Browse the &lt;a href=&quot;https://sethmlarson.dev/&quot;&gt;blog archive&lt;/a&gt;. Check out my &lt;a href=&quot;https://sethmlarson.dev/blogroll&quot;&gt;blogroll&lt;/a&gt;.&lt;/p&gt;&lt;hr /&gt;&lt;br /&gt;</description>
	<pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate>
</item>
<item>
	<title>Armin Ronacher: Gaslighting Openness</title>
	<guid>https://lucumr.pocoo.org/2026/6/10/gaslighting/</guid>
	<link>https://lucumr.pocoo.org/2026/6/10/gaslighting/</link>
	<description>&lt;p&gt;I have been a staunch supporter of Open Source for a long time, including
&lt;a href=&quot;https://lucumr.pocoo.org/2023/11/19/cathedral-and-bazaaar-licensing/&quot;&gt;experiments&lt;/a&gt;
&lt;a href=&quot;https://lucumr.pocoo.org/2024/9/23/fsl-agpl-open-source-businesses/&quot;&gt;in&lt;/a&gt;
&lt;a href=&quot;https://lucumr.pocoo.org/2024/9/19/open-source-tax/&quot;&gt;funding&lt;/a&gt; &lt;a href=&quot;https://lucumr.pocoo.org/2024/10/14/mixing-oss-and-money/&quot;&gt;it&lt;/a&gt;.
I&amp;#8217;m a true believer in the idea that Open Source always wins in the long run,
but not automatically and not quickly.  Right now it is being stressed by AI
slop, shifting contributor dynamics, the falling cost of producing code, and
large companies learning to close doors behind them.&lt;/p&gt;
&lt;p&gt;A lot of that battle today is manipulation of the narrative.  Opinion makers on
social media and in business circles increasingly frame access as
irresponsibility.  That is why the EU&amp;#8217;s DMA matters, even if many people
(including myself) reflexively hate EU regulation.  Apple&amp;#8217;s fight over &lt;a href=&quot;https://www.apple.com/newsroom/2026/06/due-to-dma-siri-ai-delayed-in-eu-for-ios-27-and-ipados-27/&quot;&gt;delayed
AI features in
Europe&lt;/a&gt;
is not about Brussels being annoying: it is about whether users can access their
own devices and data.  The phone is yours, the data is yours, yet Apple decides
who may reach it and takes the agency away from you and then tries to make that
sound like it is in your interest (supposedly it&amp;#8217;s for your safety and security).&lt;/p&gt;
&lt;p&gt;The closer you get to the core of AI, the more this shows up.  Anthropic has
every financial incentive to restrict what people can do with &lt;a href=&quot;https://www.anthropic.com/news/claude-fable-5-mythos-5&quot;&gt;Mythos and
Fable&lt;/a&gt;, and they wrap
those restrictions in safety and (national) security language.  Some
restrictions may be defensible, but not all of them are.  They trained their
models on public works, then block Open Source attempts to learn from and
distill these systems.&lt;/p&gt;
&lt;p&gt;Disliking the EU, China, or any other large government should not make us forget
that true democratized access to technology including AI is in all our interest.
Some temporary product pain, including delayed Apple AI features, will be worth
paying if it keeps gates open.  We should not let companies own the narrative
that preventing access is in our interest, particularly not as Europeans where
the odds are already stacked against us by our underdeveloped capital markets,
brain drain and internal fighting.&lt;/p&gt;</description>
	<pubDate>Wed, 10 Jun 2026 00:00:00 +0000</pubDate>
</item>
<item>
	<title>Kay Hayen: Nuitka Release 4.1</title>
	<guid>https://nuitka.net/posts/nuitka-release-41.html</guid>
	<link>https://nuitka.net/posts/nuitka-release-41.html</link>
	<description>&lt;p&gt;This is to inform you about the new stable release of &lt;a class=&quot;reference external&quot; href=&quot;https://nuitka.net&quot;&gt;Nuitka&lt;/a&gt;. It is the extremely compatible Python compiler,
&lt;a class=&quot;reference external&quot; href=&quot;https://nuitka.net/doc/download.html&quot;&gt;“download now”&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This release adds many new features and corrections with a focus on
async code compatibility, missing generics features, and Python 3.14
compatibility and Python compilation scalability yet again.&lt;/p&gt;

&lt;h2&gt;Bug Fixes&lt;/h2&gt;
&lt;ul class=&quot;simple&quot;&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.14:&lt;/strong&gt; Fix, decorators were breaking when disabling
deferred annotations. (Fixed in 4.0.1 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix, nested loops could have wrong traces lead to mis-optimization.
(Fixed in 4.0.1 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugins:&lt;/strong&gt; Fix, run-time check of package configuration was
incorrect. (Fixed in 4.0.1 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compatibility:&lt;/strong&gt; Fix, &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;__builtins__&lt;/span&gt;&lt;/code&gt; lacked necessary
compatibility in compiled functions. (Fixed in 4.0.1 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Distutils:&lt;/strong&gt; Fix, incorrect UTF-8 decoding was used for TOML input
file parsing. (Fixed in 4.0.1 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix, multiple hard value assignments could cause compile time
crashes. (Fixed in 4.0.1 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix, string concatenation was not properly annotating exception
exits. (Fixed in 4.0.2 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt; Fix, &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--verbose-output&lt;/span&gt;&lt;/code&gt; and &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--show-modules-output&lt;/span&gt;&lt;/code&gt;
did not work with forward slashes. (Fixed in 4.0.2 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.14:&lt;/strong&gt; Fix, there were various compatibility issues
including dictionary watchers and inline values. (Fixed in 4.0.2
already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.14:&lt;/strong&gt; Fix, stack pointer initialization to &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;localsplus&lt;/span&gt;&lt;/code&gt;
was incorrect to avoid garbage collection issues. (Fixed in 4.0.2
already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.12+:&lt;/strong&gt; Fix, generic type variable scoping in classes was
incorrect. (Fixed in 4.0.2 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.12+:&lt;/strong&gt; Fix, there were various issues with function
generics. (Fixed in 4.0.2 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.8+:&lt;/strong&gt; Fix, names in named expressions were not mangled.
(Fixed in 4.0.2 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugins:&lt;/strong&gt; Fix, module checksums were not robust against quoting
style of module-name entry in YAML configurations. (Fixed in 4.0.2
already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugins:&lt;/strong&gt; Fix, doing imports in queried expressions caused
corruption. (Fixed in 4.0.2 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Fix, support for &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;uv_build&lt;/span&gt;&lt;/code&gt; in the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--project&lt;/span&gt;&lt;/code&gt; option was
broken. (Fixed in 4.0.2 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compatibility:&lt;/strong&gt; Fix, names assigned in assignment expressions were
not mangled. (Fixed in 4.0.2 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.12+:&lt;/strong&gt; Fix, there were still various issues with function
generics. (Fixed in 4.0.3 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Clang:&lt;/strong&gt; Fix, debug mode was disabled for clang generally, but only
ClangCL and macOS Clang didn’t want it. (Fixed in 4.0.3 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zig:&lt;/strong&gt; Fix, &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--windows-console-mode=attach|disable&lt;/span&gt;&lt;/code&gt; was not
working when using Zig. (Fixed in 4.0.3 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt; Fix, yet another way self dependencies can look like,
needed to have support added. (Fixed in 4.0.3 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.12+:&lt;/strong&gt; Fix, generic types in classes had bugs with
multiple type variables. (Fixed in 4.0.3 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scons:&lt;/strong&gt; Fix, repeated builds were not producing binary identical
results. (Fixed in 4.0.3 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scons:&lt;/strong&gt; Fix, compiling with newer Python versions did not fall
back to Zig when the developer prompt MSVC was unusable, and error
reporting could crash. (Fixed in 4.0.4 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Zig:&lt;/strong&gt; Fix, the workaround for Windows console mode &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;attach&lt;/span&gt;&lt;/code&gt; or
&lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;disable&lt;/span&gt;&lt;/code&gt; was incorrectly applied on non-Windows platforms. (Fixed
in 4.0.4 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone:&lt;/strong&gt; Fix, linking with Python Build Standalone failed
because &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;libHacl_Hash_SHA2&lt;/span&gt;&lt;/code&gt; was not filtered out unconditionally.
(Fixed in 4.0.4 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.6+:&lt;/strong&gt; Fix, exceptions like &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;CancelledError&lt;/span&gt;&lt;/code&gt; thrown into
an async generator awaiting an inner awaitable could be swallowed,
causing crashes. (Fixed in 4.0.4 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix, not all ordered set modules accepted generators for update.
(Fixed in 4.0.5 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugins:&lt;/strong&gt; Disabled warning about rebuilding the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;pytokens&lt;/span&gt;&lt;/code&gt;
extension module. (Fixed in 4.0.5 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone:&lt;/strong&gt; Filtered &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;libHacl_Hash_SHA2&lt;/span&gt;&lt;/code&gt; from link libs
unconditionally. (Fixed in 4.0.5 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debugging:&lt;/strong&gt; Disabled unusable unicode consistency checks for
Python versions 3.4 to 3.6. (Fixed in 4.0.5 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python3.12+&lt;/strong&gt; Avoided cloning call nodes on class level which
caused issues with generic functions in combination with decorators.
(Added in 4.0.5 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.12+:&lt;/strong&gt; Added support for generic type variables in &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;async&lt;/span&gt;
&lt;span class=&quot;pre&quot;&gt;def&lt;/span&gt;&lt;/code&gt; functions. (Added in 4.0.5 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Fix, flushing outputs for prompts was not working in all
cases when progress bars were enabled. (Fixed in 4.0.6 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Fix, unused variable warnings were missing at C compile time
when using &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;zig&lt;/span&gt;&lt;/code&gt; as a C compiler. (Fixed in 4.0.6 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scons:&lt;/strong&gt; Fix, forced stdout and stderr paths as a feature was
broken. (Fixed in 4.0.6 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix, replacing a branch did not accurately track shared active
variables causing optimization crashes. (Fixed in 4.0.7 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt; Fix, failed to remove extended attributes because files
need to be made writable first. (Fixed in 4.0.7 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix, dict &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;pop&lt;/span&gt;&lt;/code&gt; and &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;setdefault&lt;/span&gt;&lt;/code&gt; using with &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;:=&lt;/span&gt;&lt;/code&gt; rewrites
lacked exception-exit annotations for un-hashable keys. (Fixed in
4.0.8 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.13:&lt;/strong&gt; Fix, the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;__parameters__&lt;/span&gt;&lt;/code&gt; attribute of generic
classes was not working. (Fixed in 4.0.8 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.11+:&lt;/strong&gt; Fix, starred arguments were not working as type
variables. (Fixed in 4.0.8 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python2:&lt;/strong&gt; Fix, &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;FileNotFoundError&lt;/span&gt;&lt;/code&gt; compatibility fallback
handling was not working properly. (Fixed in 4.0.8 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compatibility:&lt;/strong&gt; Fix, loop ownership check in value traces was
missing, causing issues with nested loops.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt; Improved &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--windows-console-mode=attach&lt;/span&gt;&lt;/code&gt; to properly
handle console handles, enabling cases like &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;os.system&lt;/span&gt;&lt;/code&gt; to work
nicely.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python2:&lt;/strong&gt; Fix, there was a compatibility issue where providing
default values to the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;mkdtemp&lt;/span&gt;&lt;/code&gt; function was failing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt; Fix, there were spurious issues with C23 embedding in
32-bit MinGW64 by switching to &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;coff_obj&lt;/span&gt;&lt;/code&gt; resource mode for it as
well.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugins:&lt;/strong&gt; Fix, the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;post-import-code&lt;/span&gt;&lt;/code&gt; execution could fail
because the triggering sub-package was not yet available in
&lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;sys.modules&lt;/span&gt;&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Fix, listing package DLLs with &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--list-package-dlls&lt;/span&gt;&lt;/code&gt; was
broken due to recent plugin lifecycle changes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Fix, &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--list-package-exe&lt;/span&gt;&lt;/code&gt; was not working properly on
non-Windows platforms failing to detect executable files correctly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Handled paths starting with &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;{PROGRAM_DIR}&lt;/span&gt;&lt;/code&gt; the same as a
relative path when parsing the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--onefile-tempdir-spec&lt;/span&gt;&lt;/code&gt; option.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugins:&lt;/strong&gt; Followed multiprocessing &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;forkserver&lt;/span&gt;&lt;/code&gt; changes for
newer Python versions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.12+:&lt;/strong&gt; Fix, generic class type parameters handling was
incorrect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.12:&lt;/strong&gt; Fix, deferred evaluation of type aliases was
failing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.12+:&lt;/strong&gt; Aligned &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;sum&lt;/span&gt;&lt;/code&gt; built-in float summation with
CPython’s compensated sum for better accuracy.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.10+:&lt;/strong&gt; Fix, uncompiled coroutine &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;throw()&lt;/span&gt;&lt;/code&gt; return
handling was incorrect, restoring completed coroutine results via
&lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;StopIteration.value&lt;/span&gt;&lt;/code&gt; rather than exposing them as ordinary return
values to the outer await chain.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.13+:&lt;/strong&gt; Fix, uncompiled coroutine &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;cancel()/await&lt;/span&gt;&lt;/code&gt;
suspension handling was incorrect, improved to ensure integration
compatibility.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt; Made finding &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;create-dmg&lt;/span&gt;&lt;/code&gt; more robustly by also checking
the Homebrew path for Intel and from &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;PATH&lt;/span&gt;&lt;/code&gt; properly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compatibility:&lt;/strong&gt; Fix, class frames were not exposing frame locals.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Detected &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;static-libpython&lt;/span&gt;&lt;/code&gt; problems, which affected some
forms of Anaconda.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Distutils:&lt;/strong&gt; Rejected &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--project&lt;/span&gt;&lt;/code&gt; mixed with &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--main&lt;/span&gt;&lt;/code&gt; arguments
as it is not useful.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt; Fix, &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;zig&lt;/span&gt;&lt;/code&gt; from &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;PATH&lt;/span&gt;&lt;/code&gt; or from &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;ziglang&lt;/span&gt;&lt;/code&gt; was not
being used.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Distutils:&lt;/strong&gt; Fix, the wrong &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;module-root&lt;/span&gt;&lt;/code&gt; config value was being
checked for &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;uv&lt;/span&gt;&lt;/code&gt; build backend.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt; Fix, was attempting to change removed (rejected) DLLs,
which of course failed and errored out.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.14:&lt;/strong&gt; Fix, tuple reuse was not fully compatible,
potentially causing crashes due to outdated hash caches.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix, fake modules were still being attempted to located when imported
by other code, which could conflict with existing modules.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.5+:&lt;/strong&gt; Fix, failed to send uncompiled coroutines the sent
in value in &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;pre&quot;&gt;from&lt;/span&gt;&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix, older &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;gcc&lt;/span&gt;&lt;/code&gt; compilers lacking newer intrinsic methods had
compilation issues that needed to be addressed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone:&lt;/strong&gt; Fix, multiphase module extension modules with
post-load code were not working properly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix, Avoid using the non-inline copy of &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;pkg_resources&lt;/span&gt;&lt;/code&gt; with the
inline copy of Jinja2. These could mismatch and cause errors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix, loops could make releasing of previous values very unclear,
causing optimization errors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix, &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;incbin&lt;/span&gt;&lt;/code&gt; resource mode was not working with old &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;gcc&lt;/span&gt;&lt;/code&gt; C++
fallback.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.4 to 3.6:&lt;/strong&gt; Fix, bytecode demotion was not working
properly for these versions, also bytecode only files not working.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugins:&lt;/strong&gt; Added a check for the broken &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;patchelf&lt;/span&gt;&lt;/code&gt; versions 0.10
and 0.11 to prevent breaking Qt plugins.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Android:&lt;/strong&gt; Allowed &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;patchelf&lt;/span&gt;&lt;/code&gt; version 0.18 on Android.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt; Fix, the header path for self uninstalled Python was not
detected correctly.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; Fix, inclusion of the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;pkg_resources&lt;/span&gt;&lt;/code&gt; inline copy for
Python 2 to source distributions was missing.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Detected the OBS versions of SUSE Linux better.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Suse:&lt;/strong&gt; Allowed using &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;patchelf&lt;/span&gt;&lt;/code&gt; 0.18.0 there too.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.11:&lt;/strong&gt; Fix, package and module dicts were not aligned close
enough to avoid a CPython bug.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix, unbound compiled methods could crash when called without an
object passed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone:&lt;/strong&gt; Fix, multiphase module extension modules with
postload. (Fixed in 4.0.8 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Onefile:&lt;/strong&gt; Fix, while waiting for the child, it may already be
terminated.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt; Removed existing absolute rpaths for Homebrew and
MacPorts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.14:&lt;/strong&gt; Avoided warning in CPython headers.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.14:&lt;/strong&gt; Followed allocator changes more closely.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Compatibility:&lt;/strong&gt; Avoided using &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;pkg_resources&lt;/span&gt;&lt;/code&gt; for Jinja2
template location for loading.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;No-GIL:&lt;/strong&gt; Applied some bug fixes to get basic things to work.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;Package Support&lt;/h2&gt;
&lt;ul class=&quot;simple&quot;&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone:&lt;/strong&gt; Add support for newer &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;paddle&lt;/span&gt;&lt;/code&gt; version. (Added in
4.0.1 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone:&lt;/strong&gt; Add workaround for refcount checks of &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;pandas&lt;/span&gt;&lt;/code&gt;.
(Fixed in 4.0.1 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone:&lt;/strong&gt; Add support for newer &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;h5py&lt;/span&gt;&lt;/code&gt; version. (Added in
4.0.2 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone:&lt;/strong&gt; Add support for newer &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;scipy&lt;/span&gt;&lt;/code&gt; package. (Added in
4.0.2 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugins:&lt;/strong&gt; Revert accidental &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;os.getenv&lt;/span&gt;&lt;/code&gt; over &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;os.environ.get&lt;/span&gt;&lt;/code&gt;
changes in anti-bloat configurations that stopped them from working.
Affected packages are &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;networkx&lt;/span&gt;&lt;/code&gt;, &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;persistent&lt;/span&gt;&lt;/code&gt;, and
&lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;tensorflow&lt;/span&gt;&lt;/code&gt;. (Fixed in 4.0.5 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone:&lt;/strong&gt; Added missing DLLs for &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;openvino&lt;/span&gt;&lt;/code&gt;. (Added in 4.0.7
already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enhanced the package configuration YAML schema by adding the
&lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;relative_to&lt;/span&gt;&lt;/code&gt; parameter for &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;from_filenames&lt;/span&gt;&lt;/code&gt; DLL specification,
avoiding error-prone purely relative paths.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone:&lt;/strong&gt; Fix, &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;flet_desktop&lt;/span&gt;&lt;/code&gt; app assets were missing, now
preserving the packaged runtime and sidecar DLLs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone:&lt;/strong&gt; Added support for the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;tyro&lt;/span&gt;&lt;/code&gt; package.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone:&lt;/strong&gt; Added data files for the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;perfetto&lt;/span&gt;&lt;/code&gt; package.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone:&lt;/strong&gt; Added support for &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;anyio&lt;/span&gt;&lt;/code&gt; process forking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone:&lt;/strong&gt; Added support for the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;plotly.graph&lt;/span&gt;&lt;/code&gt; package.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Anaconda:&lt;/strong&gt; Fix, dependencies for the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;numpy&lt;/span&gt;&lt;/code&gt; conda package on
Windows were incorrect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugins:&lt;/strong&gt; Enhanced the auto-icon hack in PySide6 to use compatible
class names.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Standalone:&lt;/strong&gt; Fix, Qt libraries were duplicated with &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;PySide6&lt;/span&gt;&lt;/code&gt;
WebEngine framework support on macOS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugins:&lt;/strong&gt; Fix, automatic detection of &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;mypyc&lt;/span&gt;&lt;/code&gt; runtime
dependencies was including all top level modules of the containing
package by accident. (Fixed in 4.0.5 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Anaconda:&lt;/strong&gt; Fix, &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;delvewheel&lt;/span&gt;&lt;/code&gt; plugin was not working with Python
3.8+. This enhances compatibility with installed PyPI packages that
use it for their DLLs. (Fixed in 4.0.6 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugins:&lt;/strong&gt; Fix, our protection workaround could confuse methods
used with &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;PySide6&lt;/span&gt;&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;New Features&lt;/h2&gt;
&lt;ul class=&quot;simple&quot;&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Added the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--recommended-python-version&lt;/span&gt;&lt;/code&gt; option to display
recommended Python versions for supported, working, or commercial
usage.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Add message to inform users about &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;Nuitka[onefile]&lt;/span&gt;&lt;/code&gt; if
compression is not installed. (Added in 4.0.1 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Add support for &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;uv_build&lt;/span&gt;&lt;/code&gt; in the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--project&lt;/span&gt;&lt;/code&gt; option.
(Added in 4.0.1 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Onefile:&lt;/strong&gt; Allow extra includes as well. (Added in 4.0.2 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Add &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;nuitka-project-set&lt;/span&gt;&lt;/code&gt; feature to define project
variables, checking for collisions with reserved runtime variables.
(Added in 4.0.2 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scons:&lt;/strong&gt; Added new option to select &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--reproducible&lt;/span&gt;&lt;/code&gt; builds or
not. (Added in 4.0.6 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.10+:&lt;/strong&gt; Added support for
&lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;importlib.metadata.package_distributions()&lt;/span&gt;&lt;/code&gt;. (Added in 4.0.8
already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugins:&lt;/strong&gt; Added support for the multiprocessing &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;forkserver&lt;/span&gt;&lt;/code&gt;
context. (Added in 4.0.8 already, for 4.1 Python 3.6 and earlier, as
well as 3.14 support were added too.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reports:&lt;/strong&gt; Added structured resource usage (&lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;rusage&lt;/span&gt;&lt;/code&gt;) performance
information to compilation reports.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reports:&lt;/strong&gt; Included individual module-level C compiler caching
(&lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;ccache&lt;/span&gt;&lt;/code&gt;/&lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;clcache&lt;/span&gt;&lt;/code&gt;) statistics in compilation reports.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Added support for detecting and correctly resolving the Python prefix
for the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;PyEnv&lt;/span&gt; &lt;span class=&quot;pre&quot;&gt;on&lt;/span&gt; &lt;span class=&quot;pre&quot;&gt;Homebrew&lt;/span&gt;&lt;/code&gt; Python flavor.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt; Added support for &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;rusage&lt;/span&gt;&lt;/code&gt; information for Scons.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Added the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;__compiled__.extension_filename&lt;/span&gt;&lt;/code&gt; attribute to
give the real filename of the containing extension module.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt; Added support for &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--clang&lt;/span&gt;&lt;/code&gt; or ARM. (Added in 4.0.8
already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Windows:&lt;/strong&gt; Added support for resources names as not just integers,
important when we copy them from template files.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;MacPorts:&lt;/strong&gt; Added basic support for this Python flavor. More work
will be needed to get it to work fully though.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;Optimization&lt;/h2&gt;
&lt;ul class=&quot;simple&quot;&gt;
&lt;li&gt;&lt;p&gt;Avoid including &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;importlib._bootstrap&lt;/span&gt;&lt;/code&gt; and
&lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;importlib._bootstrap_external&lt;/span&gt;&lt;/code&gt;. (Added in 4.0.1 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Linux:&lt;/strong&gt; Cached the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;syscall&lt;/span&gt;&lt;/code&gt; used for time keeping during
compilation to avoid loading &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;libc&lt;/span&gt;&lt;/code&gt; for each trace. (Added in 4.0.8
already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Output a warning for modules that remain unfinished after the
third optimization pass.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Added an extra micro pass trigger when new variables are introduced
or variable usage changes severely, ensuring optimizations are fully
propagated, avoiding unnecessary extra full passes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Provided scripts to compile Python statically with PGO tailored for
Nuitka on Linux, Windows, and macOS.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Added support for running the Data Composer tool from a compiled
Nuitka binary without spawning an uncompiled Python process.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enhanced the usage of &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;vectorcall&lt;/span&gt;&lt;/code&gt; for &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;PyCFunction&lt;/span&gt;&lt;/code&gt; objects by
directly checking for its presence instead of relying purely on
flags, allowing more frequent use of this faster execution path.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Cached frequently used declarations for top-level variables to speed
up C code generation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Sped up trace collection merging by avoiding unnecessary set creation
and using a set instead of a list for escaped traces.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Optimized plugin hook execution by tracking overloaded methods and
added an option to show plugin usage statistics.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Improved performance of module location by avoiding unnecessary
module name reconstruction and redundant filesystem checks for
pre-loaded packages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Improved the caching of distribution name lookups to effectively
avoid repeated IO operations across all package types.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Plugins:&lt;/strong&gt; Cached callback plugin dispatch for
&lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;onFunctionBodyParsing&lt;/span&gt;&lt;/code&gt; and &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;onClassBodyParsing&lt;/span&gt;&lt;/code&gt; to skip argument
computation when no plugin overrides them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.13:&lt;/strong&gt; Handled sub-packages of &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;pathlib&lt;/span&gt;&lt;/code&gt; as hard modules.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Handled hard attributes through merge traces as well.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Made constant blobs more compact by avoiding repeated identifiers and
unnecessary fields.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Enhanced Python compilation scripts further. (Fixed in 4.0.8
already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Recognized late incomplete variables better. (Fixed in 4.0.8
already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Made constant blobs more compact. (Fixed in 4.0.8 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Optimized calls with only constant keywords and variable posargs too.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;Anti-Bloat&lt;/h2&gt;
&lt;ul class=&quot;simple&quot;&gt;
&lt;li&gt;&lt;p&gt;Fix, memory bloat occurred when C compiling &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;sqlalchemy&lt;/span&gt;&lt;/code&gt;. (Fixed in
4.0.2 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoid using &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;pydoc&lt;/span&gt;&lt;/code&gt; in &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;PySimpleGUI&lt;/span&gt;&lt;/code&gt;. (Added in 4.0.2 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoided using &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;doctest&lt;/span&gt;&lt;/code&gt; from &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;zodbpickle&lt;/span&gt;&lt;/code&gt;. (Added in 4.0.5
already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoided inclusion of &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;cython&lt;/span&gt;&lt;/code&gt; when using &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;pyav&lt;/span&gt;&lt;/code&gt;. (Added in 4.0.7
already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoided including &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;typing_extensions&lt;/span&gt;&lt;/code&gt; when using &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;numpy&lt;/span&gt;&lt;/code&gt;. (Added
in 4.0.7 already.)&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;Organizational&lt;/h2&gt;
&lt;ul class=&quot;simple&quot;&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Relocated the warning about the available source code of
extension modules to be evaluated at a more appropriate time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debian:&lt;/strong&gt; Remove recommendation for &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;libfuse2&lt;/span&gt;&lt;/code&gt; package as it is
no longer useful.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debian:&lt;/strong&gt; Used &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;platformdirs&lt;/span&gt;&lt;/code&gt; instead of &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;appdirs&lt;/span&gt;&lt;/code&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debugging:&lt;/strong&gt; Removed Python 3.11+ restriction for &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;clang-format&lt;/span&gt;&lt;/code&gt;
as it is available everywhere, even Python 2.7, and we still want
nicely formatted code when we read things. (Added in 4.0.6 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Removed no longer useful inline copy of &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;wax_off&lt;/span&gt;&lt;/code&gt;. We have our own
stubs generator project.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Release:&lt;/strong&gt; Added missing package to the CI container for building
Nuitka Debian packages.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Developer:&lt;/strong&gt; Updated AI instructions for creating Minimal
Reproducible Examples (MRE) to skip unneeded C compilation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debugging:&lt;/strong&gt; Added an internal function for checking if a string is
a valid Python identifier.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI:&lt;/strong&gt; Added a task in Visual Studio Code to export the currently
selected Python interpreter path to a file, making it available as
“python” and “pip” matching the selected interpreter. This makes it
easier to use a specific version with no instructions needed.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;AI:&lt;/strong&gt; Updated the rules to instruct AI to only generate useful
comments that add context not present in the code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Containers:&lt;/strong&gt; Added template rendering support for Jinja2 (&lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;.j2&lt;/span&gt;&lt;/code&gt;)
container files in our internal Podman tools.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Projects:&lt;/strong&gt; Clarified the current status and rationale of Python
2.6 support in the developer manual.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debugging:&lt;/strong&gt; Added experimental flag
&lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--experimental=ignore-extra-micro-pass&lt;/span&gt;&lt;/code&gt; to allow ignoring extra
micro pass detection.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Visual Code:&lt;/strong&gt; Added integration scripts for &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;bash&lt;/span&gt;&lt;/code&gt; and &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;zsh&lt;/span&gt;&lt;/code&gt;
autocompletion of Nuitka CLI options. These are now also integrated
into Visual Studio Code terminal profiles and the Debian package.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;RPM:&lt;/strong&gt; Included the Python compile script for Linux.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;RPM:&lt;/strong&gt; Removed the requirement for &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;distutils&lt;/span&gt;&lt;/code&gt; in the spec.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;Tests&lt;/h2&gt;
&lt;ul class=&quot;simple&quot;&gt;
&lt;li&gt;&lt;p&gt;Install only necessary build tools for test cases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Avoided spurious failures in reference counting tests due to Python
internal caching differences. (Fixed in 4.0.3 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix, the parsing of the compilation report for reflected tests was
incorrect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.14:&lt;/strong&gt; Ignored a syntax error message change.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Python 3.14:&lt;/strong&gt; Added test execution support options to the main
test runner to use this version as well.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix, the runner binary path was mishandled for the third pass of
reflected compilations.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Removed the usage of obsolete plugins in reflected compilation tests.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debugging:&lt;/strong&gt; Prevented boolean testing of &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;namedtuples&lt;/span&gt;&lt;/code&gt; to avoid
unexpected bugs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Added the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;Test&lt;/span&gt;&lt;/code&gt; suffix to syntax test files and disabled “python”
mode and spell checking for them to resolve issues reported in IDEs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Fix, newline handling in diff outputs from the output comparison tool
was incorrect.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Covered &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;post-import-code&lt;/span&gt;&lt;/code&gt; functionality with a new subpackage test
case.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Prevented the program test suite from running an unnecessary variant
to save execution time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt; Ignored differences from GUI framework error traces in
headless runs in output comparisons.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Reflected test for Nuitka, where it compiles itself and compares its
operation has been restored to functional state.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Used the new method to clear internal caches if available for
reference counts.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Disabled running nested loops test with Python 2.6.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Containers:&lt;/strong&gt; Detected Python 2 defaulting containers in Podman
tooling.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;Cleanups&lt;/h2&gt;
&lt;ul class=&quot;simple&quot;&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;UI:&lt;/strong&gt; Fix, there was a double space in the Windows Runtime DLLs
inclusion message. (Fixed in 4.0.1 already.)&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Onefile:&lt;/strong&gt; Separated files and defines for extra includes for
onefile boot and Python build.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scons:&lt;/strong&gt; Provided nicer errors in case of “unset” variables being
used, so we can tell it.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Refactored the process execution results to correctly utilize our
&lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;namedtuples&lt;/span&gt;&lt;/code&gt; variant, that makes it easier to understand what code
does with the results.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quality:&lt;/strong&gt; Enabled automatic conversion of em-dashes and en-dashes
in code comments to the autoformat tool. AI won’t stop producing them
and they can cause &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;SyntaxError&lt;/span&gt;&lt;/code&gt; for older Python versions, nor is
unnecessarily using UTF-8 welcome.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Ensured that cloned outline nodes are assigned their correct names
immediately upon creation, that avoids inconsistencies during their
creation.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quality:&lt;/strong&gt; Updated to the latest versions of &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;black&lt;/span&gt;&lt;/code&gt; and adopted
a faster &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;isort&lt;/span&gt;&lt;/code&gt; execution by caching results.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quality:&lt;/strong&gt; Modified the PyLint wrapper to exit gracefully instead
of raising an error when no matching files require checking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quality:&lt;/strong&gt; Avoided checking YAML package configuration files twice,
since autoformat already handles them.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quality:&lt;/strong&gt; Ensured that YAML package configuration checks output
the original filename instead of the temporary one when a failure
occurs.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quality:&lt;/strong&gt; Prevented pushing of tags from triggering git pre-push
quality checks.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quality:&lt;/strong&gt; Silenced the output of &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;optipng&lt;/span&gt;&lt;/code&gt; and &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;jpegoptim&lt;/span&gt;&lt;/code&gt;
during image optimization auto-formatting.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Visual Code:&lt;/strong&gt; Added the generated Python alias path file to the
ignore list.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quality:&lt;/strong&gt; Enabled auto-formatting for the Nuitka devcontainer
configuration file.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Watch:&lt;/strong&gt; Avoided absolute paths in compilation to make reports more
comparable across machines.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quality:&lt;/strong&gt; Changed &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;mdformat&lt;/span&gt;&lt;/code&gt; checks to run only once and
silently.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scons:&lt;/strong&gt; Disabled format security errors in debug mode and moved
Python-related warning disables into common build setup code.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Quality:&lt;/strong&gt; Updated to the latest &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;deepdiff&lt;/span&gt;&lt;/code&gt; version.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Scons:&lt;/strong&gt; Avoided MSVC telemetry since it can produce outputs that
break CI.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debugging:&lt;/strong&gt; Enhanced non-deployment handler for importing excluded
modules.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Split import module finding functionality into more pieces for
enhanced readability.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debugging:&lt;/strong&gt; Added more assertions for constants loading and
checking.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;macOS:&lt;/strong&gt; Dropped the &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;universal&lt;/span&gt;&lt;/code&gt; target arch.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Debugging:&lt;/strong&gt; Added more traces for deep hash verification.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;


&lt;h2&gt;Summary&lt;/h2&gt;
&lt;p&gt;This release builds on the scalability improvements established in 4.0,
with enhanced Python 3.14 support, expanded package compatibility, and
significant optimization work.&lt;/p&gt;
&lt;p&gt;The &lt;code class=&quot;docutils literal notranslate&quot;&gt;&lt;span class=&quot;pre&quot;&gt;--project&lt;/span&gt;&lt;/code&gt; option seems usable now.&lt;/p&gt;
&lt;p&gt;Python 3.14 support remains experimental, but only barely made the cut,
and probably will get there in hotfixes. Some of the corrections came in
so late before the release, that it was just not possible to feel good
about declaring it fully supported just yet.&lt;/p&gt;</description>
	<pubDate>Tue, 09 Jun 2026 22:00:00 +0000</pubDate>
</item>
<item>
	<title>PyCoder’s Weekly: Issue #738: sleep(), Polars Workflows, Iterators, and More (2026-06-09)</title>
	<guid>https://pycoders.com/issues/738</guid>
	<link>https://pycoders.com/issues/738</link>
	<description>&lt;p&gt; &lt;span&gt;#738 – JUNE 9, 2026&lt;/span&gt;&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/issues/738/feed&quot;&gt;View in Browser »&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;p&gt;&lt;a href=&quot;https://pycoders.com&quot;&gt;&lt;img alt=&quot;The PyCoder&amp;rsquo;s Weekly Logo&quot; src=&quot;https://cdn.pycoders.com/37bdf31dc645f968ffb90196e5d38ff5&quot; /&gt;&lt;/a&gt;&lt;/p&gt; &lt;hr /&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16589/feed&quot; target=&quot;_blank&quot;&gt;Python &lt;code&gt;sleep()&lt;/code&gt;: How to Add Time Delays to Your Code&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Learn how to use Python&amp;rsquo;s sleep() function to add time delays and pause your code with time.sleep(), decorators, threads, and asyncio.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16589/feed&quot; target=&quot;_blank&quot;&gt;REAL PYTHON&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16590/feed&quot; target=&quot;_blank&quot;&gt;Libraries for Your Python Polars Workflows&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Four excellent libraries for your data science workflow with support for Polars DataFrames&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16590/feed&quot; target=&quot;_blank&quot;&gt;ISABELLA VELÁSQUEZ&lt;/a&gt; • Shared by &lt;a href=&quot;https://pycoders.com/link/16584/feed&quot; target=&quot;_blank&quot;&gt;Isabella Velásquez&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16591/feed&quot; target=&quot;_blank&quot;&gt;B2B AI Agent Auth Support&lt;/a&gt;&lt;/h3&gt; &lt;a href=&quot;https://pycoders.com/link/16591/feed&quot; target=&quot;_blank&quot;&gt;&lt;img src=&quot;https://cdn.pycoders.com/012e0a1cbc39d888c748f46d3b3a4e1a&quot; alt=&quot;alt&quot; /&gt;&lt;/a&gt; &lt;p&gt; Your users are asking if they can connect their AI agent to your product, but you want to make sure they can do it safely and securely. &lt;a href=&quot;https://pycoders.com/link/16591/feed&quot; target=&quot;_blank&quot;&gt;PropelAuth makes that possible →&lt;/a&gt;&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16591/feed&quot; target=&quot;_blank&quot;&gt;PROPELAUTH&lt;/a&gt;&lt;/span&gt; &lt;span&gt;sponsor&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16569/feed&quot; target=&quot;_blank&quot;&gt;Down the Iterator Rabbit Hole&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Following the trail when you have a chain of iterators&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16569/feed&quot; target=&quot;_blank&quot;&gt;STEPHEN GRUPPETTA&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16585/feed&quot; target=&quot;_blank&quot;&gt;PEP 833: Freezing the HTML Simple Repository API (Accepted)&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16585/feed&quot; target=&quot;_blank&quot;&gt;PYTHON.ORG&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16563/feed&quot; target=&quot;_blank&quot;&gt;PEP 800: Solid Bases in the Type System (Final)&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16563/feed&quot; target=&quot;_blank&quot;&gt;PYTHON.ORG&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16574/feed&quot; target=&quot;_blank&quot;&gt;PEP 798: Unpacking in Comprehensions (Final)&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16574/feed&quot; target=&quot;_blank&quot;&gt;PYTHON.ORG&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16575/feed&quot; target=&quot;_blank&quot;&gt;Python 3.15.0b2 Released&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16575/feed&quot; target=&quot;_blank&quot;&gt;PYTHON.ORG&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16560/feed&quot; target=&quot;_blank&quot;&gt;Django Security Releases Issued: 6.0.6 and 5.2.15&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16560/feed&quot; target=&quot;_blank&quot;&gt;DJANGO SOFTWARE FOUNDATION&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;h2&gt;Articles &amp;amp; Tutorials&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16567/feed&quot; target=&quot;_blank&quot;&gt;olmOCR-2 vs PaddleOCR-VL: Which Extracts PDF Tables Better?&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Compare olmOCR-2 and PaddleOCR-VL on a real arXiv PDF with dense technical tables. This article walks through a Python-based OCR workflow, then evaluates how each model handles table detection, runtime, numeric accuracy, merged cells, and multi-tier headers.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16567/feed&quot; target=&quot;_blank&quot;&gt;KHUYEN TRAN&lt;/a&gt; • Shared by &lt;a href=&quot;https://pycoders.com/link/16577/feed&quot; target=&quot;_blank&quot;&gt;Khuyen Tran&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16568/feed&quot; target=&quot;_blank&quot;&gt;Using Typing in Python Leads to Different Sorts of Code&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Chris has been moving lots of code from Python 2 to 3 and experimenting with more rigid type hints as he goes along. He&amp;rsquo;s found that keeping the type checker happy makes him write code in a different way, almost like writing in a second language.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16568/feed&quot; target=&quot;_blank&quot;&gt;CHRIS SIEBENMANN&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16578/feed&quot; target=&quot;_blank&quot;&gt;Django: Introducing Django-Integrity-Policy&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Recently, browsers have added support for the new Integrity-Policy response header (Firefox 145+, Chrome 138+). Adam quickly went to work to build a library that enables your Django project to take advantage of the feature.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16578/feed&quot; target=&quot;_blank&quot;&gt;ADAM JOHNSON&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16564/feed&quot; target=&quot;_blank&quot;&gt;PSF Strategic Plan 2026 Draft&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; The Python Software Foundation board has been developing a strategic plan to guide the foundation&amp;rsquo;s direction over the next five years. The first draft has been released and they&amp;rsquo;re looking for community feedback.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16564/feed&quot; target=&quot;_blank&quot;&gt;PYTHON SOFTWARE FOUNDATION&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16558/feed&quot; target=&quot;_blank&quot;&gt;EuroPython 2026 Language Summit Talks&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; This year&amp;rsquo;s EuroPython includes a Python Language Summit. This post highlights the talks scheduled for it, including adding Rust capabilities to CPython, an update on incremental garbage collection, and more.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16558/feed&quot; target=&quot;_blank&quot;&gt;EUROPYTHON.EU&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16587/feed&quot; target=&quot;_blank&quot;&gt;Free Threading Internals: Reference Counting&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; This article describes how the lifetime of Python objects are tracked using reference counting and how that is effected by the changes brought about by removing the GIL.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16587/feed&quot; target=&quot;_blank&quot;&gt;VICTOR STINNER&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16580/feed&quot; target=&quot;_blank&quot;&gt;Keep Your Developer Instincts When AI Writes the Code&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; The promise was less friction. The cost, it turns out, is instinct, a high price to pay. Bob&amp;rsquo;s answer: add deliberate practice to your routine, and keep the struggle.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16580/feed&quot; target=&quot;_blank&quot;&gt;BOB BELDERBOS&lt;/a&gt; • Shared by &lt;a href=&quot;https://pycoders.com/link/16579/feed&quot; target=&quot;_blank&quot;&gt;Bob Belderbos&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16588/feed&quot; target=&quot;_blank&quot;&gt;How to Use GitHub Copilot Code Review in Pull Requests&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Learn how to use GitHub Copilot code review on pull requests for AI-assisted feedback, one-click fixes, and project-specific custom instructions.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16588/feed&quot; target=&quot;_blank&quot;&gt;REAL PYTHON&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16582/feed&quot; target=&quot;_blank&quot;&gt;Quiz: How to Use GitHub Copilot Code Review in Pull Requests&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16582/feed&quot; target=&quot;_blank&quot;&gt;REAL PYTHON&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16581/feed&quot; target=&quot;_blank&quot;&gt;Parsing XML EXIF From &lt;code&gt;.avif&lt;/code&gt; Files (Plus a Rant)&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; The .avif format tends to result in smaller files, but the EXIF strippers that Andrew was using didn&amp;rsquo;t support the format, so he wrote his own.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16581/feed&quot; target=&quot;_blank&quot;&gt;ANDREW STEPHENS&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16572/feed&quot; target=&quot;_blank&quot;&gt;Structuring Your Python Script&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; Master Python script structure with best practices for shebangs, ordered imports, formatting with Ruff, constants, and a clean entry point.&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16572/feed&quot; target=&quot;_blank&quot;&gt;REAL PYTHON&lt;/a&gt;&lt;/span&gt; &lt;span&gt;course&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;h2&gt;Projects &amp;amp; Code&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16561/feed&quot; target=&quot;_blank&quot;&gt;spoof: A Simple HTTP Server for Test Environments&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16561/feed&quot; target=&quot;_blank&quot;&gt;GITHUB.COM/LEXSCA&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16559/feed&quot; target=&quot;_blank&quot;&gt;django-upgrade: Automatically Upgrade Your Django Projects&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16559/feed&quot; target=&quot;_blank&quot;&gt;GITHUB.COM/ADAMCHAINZ&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16576/feed&quot; target=&quot;_blank&quot;&gt;bocpy: Behavior-Oriented Concurrency in Python&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16576/feed&quot; target=&quot;_blank&quot;&gt;GITHUB.COM/MICROSOFT&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16571/feed&quot; target=&quot;_blank&quot;&gt;cohesion: A Tool for Measuring Python Class Cohesion&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16571/feed&quot; target=&quot;_blank&quot;&gt;GITHUB.COM/MSCHWAGER&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16570/feed&quot; target=&quot;_blank&quot;&gt;pypistats.org: PyPI Downloads Analytics Dashboard&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16570/feed&quot; target=&quot;_blank&quot;&gt;GITHUB.COM/PSF&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;h2&gt;Events&lt;/h2&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16566/feed&quot; target=&quot;_blank&quot;&gt;Weekly Real Python Office Hours Q&amp;amp;A (Virtual)&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; June 10, 2026&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16566/feed&quot; target=&quot;_blank&quot;&gt;REALPYTHON.COM&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16562/feed&quot; target=&quot;_blank&quot;&gt;Python Atlanta&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; June 11 to June 12, 2026&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16562/feed&quot; target=&quot;_blank&quot;&gt;MEETUP.COM&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16565/feed&quot; target=&quot;_blank&quot;&gt;PyDelhi User Group Meetup&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; June 13, 2026&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16565/feed&quot; target=&quot;_blank&quot;&gt;MEETUP.COM&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16573/feed&quot; target=&quot;_blank&quot;&gt;DFW Pythoneers 2nd Saturday Teaching Meeting&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; June 13, 2026&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16573/feed&quot; target=&quot;_blank&quot;&gt;MEETUP.COM&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16586/feed&quot; target=&quot;_blank&quot;&gt;DjangoCologne&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; June 16, 2026&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16586/feed&quot; target=&quot;_blank&quot;&gt;MEETUP.COM&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;div&gt; &lt;h3&gt;&lt;a href=&quot;https://pycoders.com/link/16583/feed&quot; target=&quot;_blank&quot;&gt;PyCon Singapore 2026&lt;/a&gt;&lt;/h3&gt; &lt;p&gt; June 19 to June 22, 2026&lt;br /&gt; &lt;span&gt;&lt;a href=&quot;https://pycoders.com/link/16583/feed&quot; target=&quot;_blank&quot;&gt;PYCON.SG&lt;/a&gt;&lt;/span&gt; &lt;/p&gt; &lt;/div&gt; &lt;hr /&gt; &lt;p&gt;Happy Pythoning!&lt;br /&gt;This was PyCoder&amp;rsquo;s Weekly Issue #738.&lt;br /&gt;&lt;a href=&quot;https://pycoders.com/issues/738/feed&quot;&gt;View in Browser »&lt;/a&gt;&lt;/p&gt; &lt;img src=&quot;https://pycoders.com/issues/738/open/feed&quot; width=&quot;1&quot; height=&quot;1&quot; alt=&quot;alt&quot; /&gt; 
        &lt;hr /&gt;
        &lt;p&gt;&lt;em&gt;[ Subscribe to 🐍 PyCoder&amp;rsquo;s Weekly 💌 – Get the best Python news, articles, and tutorials delivered to your inbox once a week &lt;a href=&quot;https://pycoders.com/?utm_source=pycoders&amp;utm_medium=feed&amp;utm_campaign=footer&quot;&gt;&amp;gt;&amp;gt; Click here to learn more&lt;/a&gt; ]&lt;/em&gt;&lt;/p&gt;</description>
	<pubDate>Tue, 09 Jun 2026 19:30:00 +0000</pubDate>
</item>
<item>
	<title>Python Docs Editorial Board: Meeting Minutes: Jun 9, 2026</title>
	<guid>https://python.github.io/editorial-board/updates/2026-06-09/</guid>
	<link>https://python.github.io/editorial-board/updates/2026-06-09/</link>
	<description>Meeting Minutes from Python Docs Editorial Board: Jun 9, 2026</description>
	<pubDate>Tue, 09 Jun 2026 19:00:00 +0000</pubDate>
</item>
<item>
	<title>Real Python: Accessing Multiple AI Models With the OpenRouter API</title>
	<guid>https://realpython.com/courses/multiple-ai-models-openrouter-api/</guid>
	<link>https://realpython.com/courses/multiple-ai-models-openrouter-api/</link>
	<description>&lt;p&gt;One of the quickest ways to call multiple AI models from a single Python script is to use OpenRouter&amp;rsquo;s API, which acts as a unified routing layer between your code and multiple AI providers. By the end of this course, you&amp;rsquo;ll be able to access models from several providers through one unified API.&lt;/p&gt;
&lt;p&gt;This convenience matters because the AI ecosystem is highly fragmented: each provider exposes its own API, authentication scheme, rate limits, and model lineup. Working with multiple providers often requires additional setup and integration effort, especially when you want to experiment with different models, compare outputs, or evaluate trade-offs for a specific task.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://openrouter.ai/&quot;&gt;OpenRouter&lt;/a&gt; gives you access to thousands of models from leading providers like OpenAI, Anthropic, Mistral, Google, and Meta. You can switch between them without changing your application code.&lt;/p&gt;
        &lt;hr /&gt;
        &lt;p&gt;&lt;em&gt;[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short &amp;amp; sweet Python Trick delivered to your inbox every couple of days. &lt;a href=&quot;https://realpython.com/python-tricks/?utm_source=realpython&amp;utm_medium=rss&amp;utm_campaign=footer&quot;&gt;&amp;gt;&amp;gt; Click here to learn more and see examples&lt;/a&gt; ]&lt;/em&gt;&lt;/p&gt;</description>
	<pubDate>Tue, 09 Jun 2026 14:00:00 +0000</pubDate>
</item>
<item>
	<title>Real Python: Quiz: Embeddings and Vector Databases With ChromaDB</title>
	<guid>https://realpython.com/quizzes/chromadb-vector-database/</guid>
	<link>https://realpython.com/quizzes/chromadb-vector-database/</link>
	<description>&lt;p&gt;In this quiz, you&amp;rsquo;ll test your understanding of &lt;a href=&quot;https://realpython.com/chromadb-vector-database/&quot;&gt;Embeddings and Vector Databases With ChromaDB&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;By working through this quiz, you&amp;rsquo;ll revisit key concepts like vectors, cosine similarity, word and text embeddings, ChromaDB collections, metadata filtering, and retrieval-augmented generation (RAG).&lt;/p&gt;
        &lt;hr /&gt;
        &lt;p&gt;&lt;em&gt;[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short &amp;amp; sweet Python Trick delivered to your inbox every couple of days. &lt;a href=&quot;https://realpython.com/python-tricks/?utm_source=realpython&amp;utm_medium=rss&amp;utm_campaign=footer&quot;&gt;&amp;gt;&amp;gt; Click here to learn more and see examples&lt;/a&gt; ]&lt;/em&gt;&lt;/p&gt;</description>
	<pubDate>Tue, 09 Jun 2026 12:00:00 +0000</pubDate>
</item>
<item>
	<title>Real Python: Quiz: Accessing Multiple AI Models With the OpenRouter API</title>
	<guid>https://realpython.com/quizzes/multiple-ai-models-openrouter-api/</guid>
	<link>https://realpython.com/quizzes/multiple-ai-models-openrouter-api/</link>
	<description>&lt;p&gt;In this quiz, you&amp;rsquo;ll test your understanding of
&lt;a href=&quot;https://realpython.com/courses/multiple-ai-models-openrouter-api/&quot;&gt;Accessing Multiple AI Models With the OpenRouter API&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;By working through this quiz, you&amp;rsquo;ll revisit how OpenRouter provides a unified routing layer, how to call AI models from a single Python script, how to switch between intelligent routing and a specific model, how to prioritize providers, and how to add model fallbacks for reliability.&lt;/p&gt;
&lt;p&gt;It also reinforces how to weigh trade-offs like cost, latency, and quality when you choose a model for your use case.&lt;/p&gt;
        &lt;hr /&gt;
        &lt;p&gt;&lt;em&gt;[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short &amp;amp; sweet Python Trick delivered to your inbox every couple of days. &lt;a href=&quot;https://realpython.com/python-tricks/?utm_source=realpython&amp;utm_medium=rss&amp;utm_campaign=footer&quot;&gt;&amp;gt;&amp;gt; Click here to learn more and see examples&lt;/a&gt; ]&lt;/em&gt;&lt;/p&gt;</description>
	<pubDate>Tue, 09 Jun 2026 12:00:00 +0000</pubDate>
</item>
<item>
	<title>Python Bytes: #483 Thanks Brian</title>
	<guid>https://pythonbytes.fm/episodes/show/483/thanks-brian</guid>
	<link>https://pythonbytes.fm/episodes/show/483/thanks-brian</link>
	<description>&amp;lt;strong&amp;gt;Topics covered in this episode:&amp;lt;/strong&amp;gt;&amp;lt;br&amp;gt;

&amp;lt;ul&amp;gt;
	&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Vulnerability and malware checks in uv&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;a href=&quot;https://alexwlchan.net/2026/python-http-with-the-stdlib/?featured_on=pythonbytes&quot;&amp;gt;HTTP GET requests with the Python standard library&amp;lt;/a&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Millions of AI agents imperiled by critical vulnerability in open source package&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;a href=&quot;https://github.com/Mergifyio/alembic-git-revisions?featured_on=pythonbytes&quot;&amp;gt;alembic-git-revisions&amp;lt;/a&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Extras&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Joke&amp;lt;/strong&amp;gt;&amp;lt;/li&amp;gt;

&amp;lt;/ul&amp;gt;&amp;lt;a href='https://www.youtube.com/watch?v=WIykgbceuVg' style='font-weight: bold;'data-umami-event=&quot;Livestream-Past&quot; data-umami-event-episode=&quot;483&quot;&amp;gt;Watch on YouTube&amp;lt;/a&amp;gt;&amp;lt;br&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;About the show&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Goodbye and Thanks Brian&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Thanks Calvin for being part of this and future episodes! Also new time for the live show. Thanks Brian for all the hard work over the years.&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Calvin #1: Vulnerability and malware checks in uv&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;release just yesterday by Astral https://astral.sh/blog/uv-audit&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;&amp;lt;code&amp;gt;uv audit&amp;lt;/code&amp;gt;&amp;lt;/strong&amp;gt; scans dependencies for known vulnerabilities and abandoned packages via the OSV database — runs 4–10x faster than &amp;lt;code&amp;gt;pip-audit&amp;lt;/code&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Malware check&amp;lt;/strong&amp;gt; runs on every install/sync, catching actively malicious packages (credential stealers, etc.) before they execute — including ones PyPI quarantined but lockfiles can still reference&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Enable malware scanning with &amp;lt;code&amp;gt;UV_MALWARE_CHECK=1&amp;lt;/code&amp;gt; — it's opt-in and in preview&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Future roadmap includes a resolver that steers toward vulnerability-free versions and install-time warnings scoped to newly added deps only&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Michael #2: &amp;lt;a href=&quot;https://alexwlchan.net/2026/python-http-with-the-stdlib/?featured_on=pythonbytes&quot;&amp;gt;HTTP GET requests with the Python standard library&amp;lt;/a&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;If you’re doing HTTP in Python, you’re probably using one of three popular libraries: &amp;lt;a href=&quot;https://requests.readthedocs.io/en/latest/?featured_on=pythonbytes&quot;&amp;gt;requests&amp;lt;/a&amp;gt;, &amp;lt;a href=&quot;https://github.com/encode/httpx?featured_on=pythonbytes&quot;&amp;gt;httpx&amp;lt;/a&amp;gt;, or &amp;lt;a href=&quot;https://github.com/urllib3/urllib3?featured_on=pythonbytes&quot;&amp;gt;urllib3&amp;lt;/a&amp;gt;.&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;There have been &amp;lt;a href=&quot;https://pythonbytes.fm/episodes/show/476/common-themes&quot;&amp;gt;issues with httpx lately&amp;lt;/a&amp;gt;.&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;a href=&quot;https://github.com/jawah/niquests?featured_on=pythonbytes&quot;&amp;gt;Niquest&amp;lt;/a&amp;gt; is another option: Drop-in replacement for Requests. Automatic HTTP/1.1, HTTP/2, and HTTP/3. WebSocket, and SSE included.&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;But maybe less is more, especially in the age of agentic AI&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;A good candidate needs two things to be true at once, not one: the &amp;lt;em&amp;gt;used surface&amp;lt;/em&amp;gt; is small, and the &amp;lt;em&amp;gt;behavior behind that surface&amp;lt;/em&amp;gt; is shallow.&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Calvin #3: Millions of AI agents imperiled by critical vulnerability in open source package&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;&quot;BadHost&quot; (CVE-2026-48710)&amp;lt;/strong&amp;gt; is a critical vulnerability in Starlette — the ASGI framework underlying FastAPI — with 325 million weekly downloads; also affects vLLM, LiteLLM, and most MCP server tooling&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;The exploit is trivial&amp;lt;/strong&amp;gt;: injecting a single character into an HTTP Host header bypasses path-based authentication, and can lead to credential theft, SSRF, and in some cases remote code execution&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;MCP servers are a prime target&amp;lt;/strong&amp;gt; since they store credentials for external services (email, databases, cloud accounts) — exposed data in the wild includes biopharma clinical trial DBs, full mailboxes, HR/PII pipelines, and AWS topology&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Fix is available&amp;lt;/strong&amp;gt; — patch to Starlette 1.0.1 immediately; use the free scanner at mcp-scan.nemesis.services to check if your servers are still running a vulnerable version&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;&amp;lt;strong&amp;gt;Open source sustainability footnote&amp;lt;/strong&amp;gt;: the maintainer triages near-daily security reports solo, in his free time — most are AI-generated noise, and real ones like this still compete for the same evenings and weekends&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Michael #4: &amp;lt;a href=&quot;https://github.com/Mergifyio/alembic-git-revisions?featured_on=pythonbytes&quot;&amp;gt;alembic-git-revisions&amp;lt;/a&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;By Julien Danjou from &amp;lt;a href=&quot;https://mergify.com/?featured_on=pythonbytes&quot;&amp;gt;Mergify&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Automatic &amp;lt;a href=&quot;https://alembic.sqlalchemy.org/?featured_on=pythonbytes&quot;&amp;gt;Alembic&amp;lt;/a&amp;gt; migration chaining based on git commit history. No more &amp;lt;code&amp;gt;Multiple head revisions are present for given argument 'head'&amp;lt;/code&amp;gt;.&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;See &amp;lt;a href=&quot;https://julien.danjou.info/blog/fixing-alembics-multiple-heads-problem-with-git/?featured_on=pythonbytes&quot;&amp;gt;the introductory article&amp;lt;/a&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;Caused by two migrations landed with the same &amp;lt;code&amp;gt;down_revision&amp;lt;/code&amp;gt;, and Alembic doesn’t know which one comes first. The fix is always the same: someone manually edits the migration file to re-chain the revisions.&amp;lt;/li&amp;gt;
&amp;lt;li&amp;gt;The insight: git already knows the order&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Extras&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;

&amp;lt;p&amp;gt;Calvin:&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;GNU &amp;lt;code&amp;gt;make&amp;lt;/code&amp;gt; can do pattern matching in the target. Not new at all, mentioned in the 1994-era docs. &amp;lt;code&amp;gt;just&amp;lt;/code&amp;gt; and &amp;lt;code&amp;gt;task&amp;lt;/code&amp;gt; don’t have this super power on the target name yet.
&amp;lt;pre&amp;gt;&amp;lt;code&amp;gt;train-%:
        uv run ./train.py $* --save-hyper-params --overwrite $(TRAIN_ARGS)
&amp;lt;/code&amp;gt;&amp;lt;/pre&amp;gt;&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;Michael:&amp;lt;/p&amp;gt;

&amp;lt;ul&amp;gt;
&amp;lt;li&amp;gt;Updated my HTTP client using packages from httpx to &amp;lt;a href=&quot;https://github.com/pydantic/httpx2?featured_on=pythonbytes&quot;&amp;gt;httpx2&amp;lt;/a&amp;gt;: &amp;lt;a href=&quot;https://pypi.org/project/listmonk/?featured_on=pythonbytes&quot;&amp;gt;listmonk&amp;lt;/a&amp;gt;, &amp;lt;a href=&quot;https://pypi.org/project/umami-analytics/?featured_on=pythonbytes&quot;&amp;gt;umami&amp;lt;/a&amp;gt;, and &amp;lt;a href=&quot;https://pypi.org/project/memberful/?featured_on=pythonbytes&quot;&amp;gt;memberful&amp;lt;/a&amp;gt;. For motivation, see &amp;lt;a href=&quot;https://www.reddit.com/r/Python/comments/1rl5kuq/anyone_know_whats_up_with_httpx/?featured_on=pythonbytes&quot;&amp;gt;this reddit thread&amp;lt;/a&amp;gt;.&amp;lt;/li&amp;gt;
&amp;lt;/ul&amp;gt;

&amp;lt;p&amp;gt;&amp;lt;strong&amp;gt;Joke: &amp;lt;a href=&quot;https://x.com/PR0GRAMMERHUM0R/status/2061508112083714478?featured_on=pythonbytes&quot;&amp;gt;Accurate&amp;lt;/a&amp;gt;&amp;lt;/strong&amp;gt;&amp;lt;/p&amp;gt;</description>
	<pubDate>Tue, 09 Jun 2026 08:00:00 +0000</pubDate>
</item>
<item>
	<title>Hynek Schlawack: How to Ditch Codecov for Python Projects</title>
	<guid>https://hynek.me/articles/ditch-codecov-python/</guid>
	<link>https://hynek.me/articles/ditch-codecov-python/</link>
	<description>&lt;p&gt;Codecov&amp;rsquo;s unreliability breaking CI on my open source projects has been a constant source of frustration for me for years. I have found a way to enforce coverage over a whole GitHub Actions build matrix that doesn&amp;rsquo;t rely on third-party services.&lt;/p&gt;</description>
	<pubDate>Tue, 09 Jun 2026 00:00:00 +0000</pubDate>
</item>
<item>
	<title>Real Python: Python 3.15 Hits Feature Freeze and Other News for June 2026</title>
	<guid>https://realpython.com/python-news-june-2026/</guid>
	<link>https://realpython.com/python-news-june-2026/</link>
	<description>&lt;div&gt;&lt;p&gt;While the Northern Hemisphere warms up for summer, Python 3.15 went the other way with its &lt;strong&gt;beta 1 feature freeze&lt;/strong&gt; 🥶. Since May 7, the list of what will be included in the next release is final. That list includes a brand-new &lt;strong&gt;&lt;code&gt;sentinel&lt;/code&gt; built-in&lt;/strong&gt; that finally standardizes a pattern Python developers have been hand-rolling for decades.&lt;/p&gt;
&lt;p&gt;And while AI kept writing code, buggy or not, developers also directed it to &lt;em&gt;look&lt;/em&gt; for bugs in code that had been sitting untouched for years. The results were hundreds of bug fixes in Python’s C extensions and in Firefox. Meanwhile, in a quieter corner of the ecosystem, Pydantic forked &lt;code&gt;httpx&lt;/code&gt;, kicking off one of the more interesting governance stories of the year.&lt;/p&gt;
&lt;p&gt;Time to dig into the &lt;strong&gt;Python news&lt;/strong&gt; from the past month!&lt;/p&gt;
&lt;div class=&quot;alert alert-warning&quot;&gt;
&lt;p&gt;&lt;strong&gt;Join Now:&lt;/strong&gt; &lt;a href=&quot;https://realpython.com/bonus/newsletter/&quot; class=&quot;alert-link&quot;&gt;Click here to join the Real Python Newsletter&lt;/a&gt; and you’ll never miss another Python tutorial, course, or news update.&lt;/p&gt;
&lt;/div&gt;
&lt;h2 id=&quot;python-releases-and-pep-highlights&quot;&gt;Python Releases and PEP Highlights&lt;a class=&quot;headerlink&quot; href=&quot;https://realpython.com/atom.xml#python-releases-and-pep-highlights&quot; title=&quot;Permanent link&quot;&gt;&lt;/a&gt;&lt;/h2&gt;
&lt;p&gt;The 3.15 release of &lt;a href=&quot;https://realpython.com/cpython-source-code-guide/&quot;&gt;CPython&lt;/a&gt; crossed from alpha into beta, which means its feature set is now frozen, and the &lt;a href=&quot;https://realpython.com/ref/glossary/python-steering-council/&quot; class=&quot;ref-link&quot;&gt;Steering Council&lt;/a&gt; cleared out a backlog of proposals before the gate closed. Two of those changes will touch the code you write every day.&lt;/p&gt;
&lt;h3 id=&quot;beta-1-marks-the-315-feature-freeze&quot;&gt;Beta 1 Marks the 3.15 Feature Freeze&lt;a class=&quot;headerlink&quot; href=&quot;https://realpython.com/atom.xml#beta-1-marks-the-315-feature-freeze&quot; title=&quot;Permanent link&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Last month, &lt;a href=&quot;https://realpython.com/python-news-may-2026/#python-3150-alpha-8-final-alpha-before-beta-freeze&quot;&gt;the eighth and final alpha&lt;/a&gt; rolled out as the runway to the beta phase. With &lt;a href=&quot;https://www.python.org/downloads/release/python-3150b1/&quot;&gt;Python 3.15.0b1&lt;/a&gt; on May 7 came the &lt;strong&gt;feature freeze&lt;/strong&gt;, which means that from here until the final release of 3.15, the core team works only on bug fixes and polishing.&lt;/p&gt;
&lt;p&gt;That makes the beta releases a good moment to step back and look at the headline features of 3.15, which are now locked:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Explicit lazy imports&lt;/strong&gt; (&lt;a href=&quot;https://realpython.com/python-news-december-2025/#pep-810-accepted-explicit-lazy-imports&quot;&gt;PEP 810&lt;/a&gt;) for faster startup&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;&lt;code&gt;frozendict&lt;/code&gt; built-in&lt;/strong&gt; (&lt;a href=&quot;https://realpython.com/python-news-march-2026/#pep-814-accepted-frozendict-joins-the-built-ins&quot;&gt;PEP 814&lt;/a&gt;) for immutable mappings&lt;/li&gt;
&lt;li&gt;A &lt;strong&gt;&lt;code&gt;sentinel&lt;/code&gt; built-in&lt;/strong&gt; (&lt;a href=&quot;https://peps.python.org/pep-0661/&quot;&gt;PEP 661&lt;/a&gt;), which you’ll dig into &lt;a href=&quot;https://realpython.com/atom.xml#a-built-in-sentinel-lands-in-python-315&quot;&gt;below&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Unpacking in comprehensions&lt;/strong&gt; (&lt;a href=&quot;https://realpython.com/python-news-march-2026/#python-3150-alpha-6-comprehension-unpacking-and-more&quot;&gt;PEP 798&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;UTF-8 as the default &lt;a href=&quot;https://realpython.com/python-encodings-guide/&quot;&gt;encoding&lt;/a&gt;&lt;/strong&gt; (&lt;a href=&quot;https://peps.python.org/pep-0686/&quot;&gt;PEP 686&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;A stable ABI for &lt;a href=&quot;https://realpython.com/python-news-may-2026/#pep-803-accepted-stable-abi-goes-free-threaded&quot;&gt;free-threaded builds&lt;/a&gt; (&lt;a href=&quot;https://peps.python.org/pep-0803/&quot;&gt;PEP 803&lt;/a&gt;), plus C-API modernization (PEPs &lt;a href=&quot;https://peps.python.org/pep-0820/&quot;&gt;820&lt;/a&gt; and &lt;a href=&quot;https://peps.python.org/pep-0793/&quot;&gt;793&lt;/a&gt;) that should make it easier to write C extensions that work across Python versions&lt;/li&gt;
&lt;li&gt;A new &lt;strong&gt;sampling profiler&lt;/strong&gt; in the &lt;a href=&quot;https://realpython.com/ref/glossary/standard-library/&quot; class=&quot;ref-link&quot;&gt;standard library&lt;/a&gt; (&lt;a href=&quot;https://peps.python.org/pep-0799/&quot;&gt;PEP 799&lt;/a&gt;) for low-overhead &lt;a href=&quot;https://realpython.com/python-profiling/&quot;&gt;profiling&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;a href=&quot;https://realpython.com/ref/glossary/jit-compiler/&quot; class=&quot;ref-link&quot;&gt;JIT compiler&lt;/a&gt; also gets faster, with the beta announcement citing an 8–9 percent geometric-mean improvement on x86-64 Linux. If you’ve been putting off testing your code against 3.15, then now is the time to get started! The &lt;a href=&quot;https://realpython.com/ref/glossary/api/&quot; class=&quot;ref-link&quot;&gt;API&lt;/a&gt; surface won’t shift under you anymore, and your feedback will help catch regressions before the release candidate phase.&lt;/p&gt;
&lt;div class=&quot;alert alert-primary&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Beta builds are for testing, not production. &lt;a href=&quot;https://realpython.com/python-pre-release/&quot;&gt;Install the pre-release version&lt;/a&gt;, run your test suite against 3.15, and report anything that breaks while there’s still time to fix it before the release candidate.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;The first round of improvements already landed with &lt;a href=&quot;https://www.python.org/downloads/release/python-3150b2/&quot;&gt;beta 2&lt;/a&gt; on June 2, and the next big checkpoint is the &lt;a href=&quot;https://realpython.com/ref/glossary/release-candidate/&quot; class=&quot;ref-link&quot;&gt;release candidate&lt;/a&gt; phase on August 4, with the final release expected, as usual, this fall.&lt;/p&gt;
&lt;h3 id=&quot;a-built-in-sentinel-lands-in-python-315&quot;&gt;A Built-in &lt;code&gt;sentinel&lt;/code&gt; Lands in Python 3.15&lt;a class=&quot;headerlink&quot; href=&quot;https://realpython.com/atom.xml#a-built-in-sentinel-lands-in-python-315&quot; title=&quot;Permanent link&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;Here’s the new feature that you’ll likely want to reach for. If you’ve ever needed to tell the difference between a caller passing &lt;code&gt;None&lt;/code&gt; and a caller passing nothing at all, then you’ve probably written something like this:&lt;/p&gt;

  &lt;div class=&quot;codeblock__header codeblock--blue&quot;&gt;
    &lt;span class=&quot;mr-2 noselect&quot;&gt;&lt;span class=&quot;sr-only&quot;&gt;Language: &lt;/span&gt;Python&lt;/span&gt;
    
    &lt;div class=&quot;noselect&quot;&gt;
      
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;codeblock__contents&quot;&gt;
    &lt;div class=&quot;highlight highlight--with-header&quot;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;_MISSING&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;object&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;_MISSING&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;_MISSING&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# No value was provided&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
    
    &lt;button class=&quot;codeblock__copy btn btn-outline-secondary border m-1 px-1 d-hover-only&quot; title=&quot;Copy to clipboard&quot;&gt;&lt;span class=&quot;icon baseline&quot;&gt;&lt;/span&gt;&lt;/button&gt;
    
  &lt;/div&gt;

&lt;p&gt;It works, but it has rough edges. The &lt;code&gt;repr()&lt;/code&gt; is an unhelpful &lt;code&gt;&amp;lt;object object at 0x7f...&amp;gt;&lt;/code&gt;, the marker can’t be used cleanly in type &lt;a href=&quot;https://realpython.com/ref/glossary/annotation/&quot; class=&quot;ref-link&quot;&gt;annotations&lt;/a&gt;, and its identity doesn’t survive copying or pickling. &lt;a href=&quot;https://peps.python.org/pep-0661/&quot;&gt;PEP 661&lt;/a&gt; replaces the idiom with a new &lt;a href=&quot;https://docs.python.org/3.15/library/functions.html#sentinel&quot;&gt;&lt;code&gt;sentinel&lt;/code&gt;&lt;/a&gt; built-in:&lt;/p&gt;

  &lt;div class=&quot;codeblock__header codeblock--blue&quot;&gt;
    &lt;span class=&quot;mr-2 noselect&quot;&gt;&lt;span class=&quot;sr-only&quot;&gt;Language: &lt;/span&gt;Python&lt;/span&gt;
    
    &lt;div class=&quot;noselect&quot;&gt;
      
    &lt;/div&gt;
  &lt;/div&gt;
  &lt;div class=&quot;codeblock__contents&quot;&gt;
    &lt;div class=&quot;highlight highlight--with-header&quot;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;MISSING&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sentinel&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&quot;MISSING&quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;update&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;|&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MISSING&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MISSING&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&amp;gt;&lt;/span&gt; &lt;span class=&quot;kc&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;if&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;value&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;is&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;MISSING&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;
        &lt;span class=&quot;o&quot;&gt;...&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# No value was provided&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
    
    &lt;button class=&quot;codeblock__copy btn btn-outline-secondary border m-1 px-1 d-hover-only&quot; title=&quot;Copy to clipboard&quot;&gt;&lt;span class=&quot;icon baseline&quot;&gt;&lt;/span&gt;&lt;/button&gt;
    
  &lt;/div&gt;

&lt;p&gt;The signature is &lt;code&gt;sentinel(name, /, *, repr=None)&lt;/code&gt;, and the result is a unique truthy object whose default &lt;code&gt;repr()&lt;/code&gt; is the name you gave it, so &lt;code&gt;MISSING&lt;/code&gt; shows up as &lt;code&gt;MISSING&lt;/code&gt; in &lt;a href=&quot;https://realpython.com/ref/glossary/traceback/&quot; class=&quot;ref-link&quot;&gt;tracebacks&lt;/a&gt; instead of a memory address.&lt;/p&gt;
&lt;div class=&quot;alert alert-primary&quot;&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; Sentinels and &lt;code&gt;None&lt;/code&gt; solve related but different problems. If you’re still fuzzy on when &lt;code&gt;None&lt;/code&gt; is the right tool, then Real Python’s guide to &lt;a href=&quot;https://realpython.com/null-in-python/&quot;&gt;Python’s &lt;code&gt;None&lt;/code&gt;&lt;/a&gt; is worth revisiting.&lt;/p&gt;
&lt;/div&gt;
&lt;p&gt;Because the sentinel is its own type, you can drop it straight into annotations like &lt;code&gt;int | MISSING&lt;/code&gt; without reaching for &lt;code&gt;Literal&lt;/code&gt;. The &lt;a href=&quot;https://realpython.com/ref/glossary/pep/&quot; class=&quot;ref-link&quot;&gt;PEP&lt;/a&gt; was first submitted back in 2021, so it’s satisfying to see it cross the finish line.&lt;/p&gt;
&lt;h3 id=&quot;pep-829-graduates-from-draft-to-accepted&quot;&gt;PEP 829 Graduates From Draft to Accepted&lt;a class=&quot;headerlink&quot; href=&quot;https://realpython.com/atom.xml#pep-829-graduates-from-draft-to-accepted&quot; title=&quot;Permanent link&quot;&gt;&lt;/a&gt;&lt;/h3&gt;
&lt;p&gt;&lt;a href=&quot;https://realpython.com/python-news-may-2026/#pep-829-draft-replacing-pth-files-for-package-startup&quot;&gt;Last month’s roundup&lt;/a&gt; featured PEP 829 while it was still a draft. It’s since been accepted for Python 3.15, so the change is now official.&lt;/p&gt;
&lt;p&gt;As a quick recap, &lt;code&gt;.pth&lt;/code&gt; files in your &lt;code&gt;site-packages&lt;/code&gt; directory can do two things:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Extend &lt;code&gt;sys.path&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Run arbitrary code through &lt;code&gt;import&lt;/code&gt; lines that Python feeds directly to &lt;a href=&quot;https://realpython.com/python-exec/&quot;&gt;&lt;code&gt;exec()&lt;/code&gt;&lt;/a&gt; at startup&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;&lt;h2&gt;&lt;a href=&quot;https://realpython.com/python-news-june-2026/?utm_source=realpython&amp;utm_medium=rss&quot;&gt;Read the full article at https://realpython.com/python-news-june-2026/ »&lt;/a&gt;&lt;/h2&gt;
        &lt;hr /&gt;
        &lt;p&gt;&lt;em&gt;[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short &amp;amp; sweet Python Trick delivered to your inbox every couple of days. &lt;a href=&quot;https://realpython.com/python-tricks/?utm_source=realpython&amp;utm_medium=rss&amp;utm_campaign=footer&quot;&gt;&amp;gt;&amp;gt; Click here to learn more and see examples&lt;/a&gt; ]&lt;/em&gt;&lt;/p&gt;</description>
	<pubDate>Mon, 08 Jun 2026 14:00:00 +0000</pubDate>
</item>
<item>
	<title>death and gravity: Ordered key sharding in DynamoDB</title>
	<guid>https://death.andgravity.com/albumtitle</guid>
	<link>https://death.andgravity.com/albumtitle</link>
	<description>&lt;p&gt;So, you want to keep a &lt;strong&gt;sorted index&lt;/strong&gt; in DynamoDB,
but for whatever reason
– usually &lt;a class=&quot;internal&quot; href=&quot;https://death.andgravity.com/dynamodb-model#partition-throughput&quot;&gt;throughput-related&lt;/a&gt; –
it &lt;strong&gt;won't fit on a single partition&lt;/strong&gt;. What do you do?&lt;/p&gt;
&lt;p&gt;Today, we look at the available solutions,
do the math, and find out which is best.&lt;/p&gt;

&lt;p class=&quot;admonition-title&quot;&gt;Tip&lt;/p&gt;
&lt;p&gt;This worked example is part of my &lt;a class=&quot;internal&quot; href=&quot;https://death.andgravity.com/dynamodb&quot;&gt;DynamoDB crash course&lt;/a&gt; series.&lt;/p&gt;


Contents
&lt;ul&gt;
&lt;li&gt;&lt;a href=&quot;https://death.andgravity.com/albumtitle#requirements&quot;&gt;Requirements&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://death.andgravity.com/albumtitle#a-sparse-index-is-almost-enough&quot;&gt;A sparse index is almost enough&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-scan-results-are-not-ordered&quot;&gt;But scan results are not ordered&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-a-single-partition-key-causes-throttling&quot;&gt;But a single partition key causes throttling&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-random-suffixes-are-random&quot;&gt;But random suffixes are random&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-hash-suffixes-are-not-ordered&quot;&gt;But hash suffixes are not ordered&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-there-are-a-lot-of-first-characters&quot;&gt;But there are a lot of first characters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-some-first-bytes-need-multiple-shards&quot;&gt;But some first bytes need multiple shards&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-tries-and-prefix-ranges-are-complicated&quot;&gt;But tries and prefix ranges are complicated&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-the-prefix-distribution-can-change&quot;&gt;But the prefix distribution can change&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;h2 id=&quot;requirements&quot;&gt;Requirements&lt;span class=&quot;headerlink&quot;&gt;&amp;nbsp;&lt;a href=&quot;https://death.andgravity.com/albumtitle#requirements&quot; title=&quot;permalink&quot;&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;Say you're using &lt;a class=&quot;internal&quot; href=&quot;https://death.andgravity.com/dynamodb-patterns#single-table-design&quot;&gt;single table design&lt;/a&gt;
with a table of artists, albums, and songs.&lt;sup class=&quot;footnote-ref&quot; id=&quot;fnref-1&quot;&gt;&lt;a href=&quot;https://death.andgravity.com/albumtitle#fn-1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;p&gt;You keep an artist's items in a single collection
(aka same partition key),
and use sort keys &lt;code&gt;artist&lt;/code&gt;, &lt;code&gt;album#{Album}&lt;/code&gt;, and &lt;code&gt;song#{Album}#{Song}&lt;/code&gt;,
depending on their type:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# table Music (partition key: Artist, sort key: sk)&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;Solar Fields&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;!btree&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'album#Leaving&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Home'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; Album&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Leaving Home&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'artist'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'song#Leaving&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Home#Air&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Song'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'song#Leaving&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Home#Monogram'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To list albums without doing a full table scan,
you need a &lt;a class=&quot;internal&quot; href=&quot;https://death.andgravity.com/dynamodb-model#global-secondary-indexes&quot;&gt;global secondary index&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Let's come up with some reasonable requirements; the &lt;abbr title=&quot;Global Secondary Index&quot;&gt;GSI&lt;/abbr&gt; should support:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;items up 500 bytes (we project additional attributes besides the keys)&lt;/li&gt;
&lt;li&gt;10,000 queries/second, max 100 items/query, &lt;strong&gt;sorted alphabetically&lt;/strong&gt;&lt;ul&gt;
&lt;li&gt;&lt;em&gt;list all albums&lt;/em&gt;&lt;/li&gt;
&lt;li&gt;&lt;em&gt;list albums by title&lt;/em&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;10,000 writes/second (to avoid &lt;a class=&quot;external&quot; href=&quot;https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/gsi-throttling.html&quot;&gt;write throttling&lt;/a&gt; during imports)&lt;/li&gt;
&lt;/ol&gt;
&lt;h2 id=&quot;a-sparse-index-is-almost-enough&quot;&gt;A sparse index is almost enough&lt;span class=&quot;headerlink&quot;&gt;&amp;nbsp;&lt;a href=&quot;https://death.andgravity.com/albumtitle#a-sparse-index-is-almost-enough&quot; title=&quot;permalink&quot;&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;One way to do it is to use a &lt;em&gt;dedicated&lt;/em&gt;
&lt;a class=&quot;internal&quot; href=&quot;https://death.andgravity.com/dynamodb-patterns#sparse-indexes&quot;&gt;sparse index&lt;/a&gt;,
taking advantage of the fact that
items with missing index keys don't appear in the index.&lt;/p&gt;
&lt;p&gt;If only albums have an &lt;em&gt;Album&lt;/em&gt; attribute, we just create a new &lt;abbr title=&quot;Global Secondary Index&quot;&gt;GSI&lt;/abbr&gt;:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# GSI Albums (partition key: Album, sort key: Artist)&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;Leaving Home&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;!btree&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;International Pony&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; sk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'album#Leaving&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Home'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;Solar Fields&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; sk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'album#Leaving&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Home'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;nt&quot;&gt;Heavy Migration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;!btree&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;Dday One&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; sk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'album#Heavy&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Migration'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;If songs have an &lt;em&gt;Album&lt;/em&gt; too,
we add a dedicated &lt;em&gt;AlbumsPK&lt;/em&gt; attribute instead.&lt;/p&gt;
&lt;p&gt;In many ways, this is the ideal solution.
To &lt;em&gt;list all albums&lt;/em&gt;, we scan the index.
To &lt;em&gt;list albums by title&lt;/em&gt;, we query an index partition key.
We have lots of unique partition keys
with items spread pretty evenly across them,
which should prevent throttling.&lt;/p&gt;
&lt;h2 id=&quot;but-scan-results-are-not-ordered&quot;&gt;But scan results are not ordered&lt;span class=&quot;headerlink&quot;&gt;&amp;nbsp;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-scan-results-are-not-ordered&quot; title=&quot;permalink&quot;&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;...except scan results are not ordered,
so we're missing the &lt;em&gt;sorted alphabetically&lt;/em&gt; part.&lt;/p&gt;
&lt;p&gt;What &lt;em&gt;is&lt;/em&gt; ordered are sort keys,
so we can use a single index collection instead:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# GSI GSI1 (partition key: gsi1pk, sort key: gsi1sk)&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;'albums'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;!btree&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;Heavy Migration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; Artist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Dday One&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; sk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'album#Heavy&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Migration'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;Leaving Home&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; Artist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Solar Fields&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; sk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'album#Leaving&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Home'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;Leaving Home&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; Artist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;International Pony&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; sk&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;'album#Leaving&lt;/span&gt;&lt;span class=&quot;nv&quot;&gt; &lt;/span&gt;&lt;span class=&quot;s&quot;&gt;Home'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This is also seemingly ideal.
To &lt;em&gt;list all albums&lt;/em&gt;, we query the entire index partition key.
To &lt;em&gt;list albums by title&lt;/em&gt;, we use a sort key.
The results are sorted as required,
and there's no limit on the number of items in a collection.&lt;/p&gt;
&lt;h2 id=&quot;but-a-single-partition-key-causes-throttling&quot;&gt;But a single partition key causes throttling&lt;span class=&quot;headerlink&quot;&gt;&amp;nbsp;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-a-single-partition-key-causes-throttling&quot; title=&quot;permalink&quot;&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;However, there are
&lt;a class=&quot;internal&quot; href=&quot;https://death.andgravity.com/dynamodb-model#partition-throughput&quot;&gt;per-partition limits&lt;/a&gt; of
24 MB/s for reads and
1 MB/s for writes.&lt;/p&gt;
&lt;p&gt;Let's see how they compare to our requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;reads: 500 bytes/item * 10k queries/s * 100 items/query = 500 MB/s (&lt;strong&gt;~21x&lt;/strong&gt;)&lt;/li&gt;
&lt;li&gt;writes: 500 bytes/item * 10k items/s = 5 MB/s (&lt;strong&gt;5x&lt;/strong&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Uh-oh, turns out we need &lt;strong&gt;21 times&lt;/strong&gt; the throughput one partition can deliver.&lt;/p&gt;
&lt;p&gt;One way to spread the load is
&lt;a class=&quot;internal&quot; href=&quot;https://death.andgravity.com/dynamodb-patterns#partition-key-sharding&quot;&gt;sharding&lt;/a&gt;,
using multiple synthetic partition keys of the form &lt;code&gt;album#{shard_id}&lt;/code&gt;.
A common option for the shard id is a &lt;strong&gt;random number&lt;/strong&gt; from a known range,
e.g. &lt;code&gt;album#{randrange(21)}&lt;/code&gt;:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# GSI GSI1 (partition key: gsi1pk, sort key: gsi1sk)&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;'album#1'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;!btree&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;Leaving Home&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; Artist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Solar Fields&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;'album#12'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;!btree&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;Heavy Migration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; Artist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Dday One&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;'album#20'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;!btree&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;Leaving Home&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; Artist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;International Pony&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To &lt;em&gt;list all albums&lt;/em&gt;, query each shard in turn:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shard&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;range&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;21&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;for&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt; &lt;span class=&quot;ow&quot;&gt;in&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;dynamodb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;album#&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shard&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
        &lt;span class=&quot;k&quot;&gt;yield&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;item&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;but-random-suffixes-are-random&quot;&gt;But random suffixes are random&lt;span class=&quot;headerlink&quot;&gt;&amp;nbsp;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-random-suffixes-are-random&quot; title=&quot;permalink&quot;&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;There's a problem, though –
with random shard ids we can't easily &lt;em&gt;list albums by title&lt;/em&gt;,
since albums with the same title may end up on any shard.&lt;/p&gt;
&lt;p&gt;A better option is to &lt;strong&gt;calculate&lt;/strong&gt; the shard id
from the album title using a hash function:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;from_bytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sha256&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;s&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;digest&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;album_shard_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;album_title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;hash&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;album_title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;%&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;21&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;c1&quot;&gt;# GSI GSI1 (partition key: gsi1pk, sort key: gsi1sk)&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;'album#6'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;!btree&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;Leaving Home&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; Artist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Solar Fields&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;Leaving Home&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; Artist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;International Pony&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;span class=&quot;s&quot;&gt;'album#8'&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;kt&quot;&gt;!btree&lt;/span&gt;
&lt;span class=&quot;w&quot;&gt;  &lt;/span&gt;&lt;span class=&quot;nt&quot;&gt;Heavy Migration&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;nt&quot;&gt; Artist&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;:&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;Dday One&lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;,&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nv&quot;&gt;...&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;p p-Indicator&quot;&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To &lt;em&gt;list albums by title&lt;/em&gt;:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;n&quot;&gt;dynamodb&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;query&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;sa&quot;&gt;f&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;album#&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;{&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;album_shard_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;album_title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;si&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;s2&quot;&gt;&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;sk&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;album_title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;index&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'GSI1'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;but-hash-suffixes-are-not-ordered&quot;&gt;But hash suffixes are not ordered&lt;span class=&quot;headerlink&quot;&gt;&amp;nbsp;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-hash-suffixes-are-not-ordered&quot; title=&quot;permalink&quot;&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;That takes care of throughput,
but now results aren't &lt;em&gt;sorted alphabetically&lt;/em&gt; anymore.
We can sort items within each shard using the sort key,
but they are spread uniformly across shards,
and there's no order &lt;em&gt;between shards&lt;/em&gt;.&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;Maybe we could use the &lt;strong&gt;first letter&lt;/strong&gt; as shard id instead?&lt;/p&gt;
&lt;p&gt;Of course, we have to account for some first letters
being &lt;a class=&quot;external&quot; href=&quot;https://en.wikipedia.org/wiki/Letter_frequency#Relative_frequencies_of_the_first_letters_of_a_word_in_the_English_language&quot;&gt;more frequent&lt;/a&gt; than others.
In this case, we can approximate the actual distribution
by using &lt;a class=&quot;external&quot; href=&quot;https://musicbrainz.org/doc/MusicBrainz_Database#Download&quot;&gt;MusicBrainz data&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;There are 5.5 million albums:&lt;/p&gt;
&lt;!-- (incidentally this gives us an idea of how many albums we can have, although we should figured this out already): --&gt;

&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;polars&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;k&quot;&gt;as&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;pl&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;titles&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;pl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;read_csv&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;... &lt;/span&gt;    &lt;span class=&quot;s1&quot;&gt;'mbdump/release'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;... &lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;has_header&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;False&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;... &lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;separator&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;se&quot;&gt;\t&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;... &lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;quote_char&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;None&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;... &lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;columns&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;... &lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;new_columns&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'title'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;],&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;... &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)[:,&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;titles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;5535986&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;...but only 3.3 million unique titles,
partly due to different releases of the same album,
partly due to some titles being more popular
– a few of them, &lt;em&gt;really&lt;/em&gt; popular:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;titles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;shape: (3_370_505, 2)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; title            count&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; Greatest Hits    4638&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; Demo             3140&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; …                …&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; Salsa salsa      1&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; Glamour: Deluxe  1&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;titles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unique_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;quantile&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;([&lt;/span&gt;&lt;span class=&quot;mf&quot;&gt;.9&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;.99&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;.999&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mf&quot;&gt;.9999&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;])&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;[2.0, 11.0, 58.0, 221.0, 4638.0]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Let's look at first characters:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;normalized&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;titles&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;to_lowercase&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;normalize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'NFKD'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;normalized&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;shape: (5_402, 2)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; title  count&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; t      584760&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; s      509065&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; a      317513&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; l      298757&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; …      …&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; 🫀     1&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; 🫂     1&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; 🫧     1&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; 󠀼       1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;but-there-are-a-lot-of-first-characters&quot;&gt;But there are a lot of first characters&lt;span class=&quot;headerlink&quot;&gt;&amp;nbsp;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-there-are-a-lot-of-first-characters&quot; title=&quot;permalink&quot;&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;5402?!
Indeed, there's more to Unicode than the Latin alphabet:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;normalized&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;unique&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sample&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;10&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;to_list&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;['學', '舒', 'і', '进', '੦', '潮', '向', '妳', '陳', '🍅']&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And it's actually worse than that –
there are five thousand characters &lt;em&gt;in our dataset&lt;/em&gt;,
but there are hundreds of thousands of &lt;em&gt;possible&lt;/em&gt; Unicode characters.&lt;/p&gt;
&lt;p&gt;This is not a problem when adding the albums,
but it is a problem when listing them,
since we need to enumerate all the shards in a reasonable amount of time
(and most shards being empty doesn't help, either).&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;As a very bad compromise,
we could use the &lt;strong&gt;first byte&lt;/strong&gt; of the UTF-8 encoding instead;
this caps the number of shard ids at 256,
and at least Latin titles would be sorted
(I did say it's a bad compromise).
There:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;firstbytes&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;normalized&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;map_elements&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;encode&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bin&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;firstbytes&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;shape: (136, 2)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; title    count&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; b&amp;quot;t&amp;quot;     584760&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; b&amp;quot;s&amp;quot;     509065&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; b&amp;quot;a&amp;quot;     317513&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; b&amp;quot;l&amp;quot;     298757&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; …        …&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; b&amp;quot;\xd4&amp;quot;  2&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; b&amp;quot;U&amp;quot;     1&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; b&amp;quot;\xee&amp;quot;  1&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; b&amp;quot;\xf3&amp;quot;  1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;but-some-first-bytes-need-multiple-shards&quot;&gt;But some first bytes need multiple shards&lt;span class=&quot;headerlink&quot;&gt;&amp;nbsp;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-some-first-bytes-need-multiple-shards&quot; title=&quot;permalink&quot;&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;We knew the first byte distribution would be skewed,
but some of them don't even fit on a single shard
(and it gets worse the more shards we need):&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shard_count&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;21&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;firstbytes&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;with_columns&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;... &lt;/span&gt;    &lt;span class=&quot;n&quot;&gt;pl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;col&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'count'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;firstbytes&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shard_count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;... &lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;head&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;()&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;shape: (5, 2)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; title  count&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; b&amp;quot;t&amp;quot;   2.218206&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; b&amp;quot;s&amp;quot;   1.931068&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; b&amp;quot;a&amp;quot;   1.204442&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; b&amp;quot;l&amp;quot;   1.133294&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; b&amp;quot;b&amp;quot;   1.106949&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;We're back to where we started:
how do we sort between shards with the same prefix?&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;We don't – we find &lt;strong&gt;longer prefixes&lt;/strong&gt; that fit in one shard.&lt;/p&gt;
&lt;p&gt;That sounds like the perfect job for a &lt;a class=&quot;external&quot; href=&quot;https://en.wikipedia.org/wiki/Trie&quot;&gt;trie&lt;/a&gt; (aka prefix tree).
This would also allow us to switch back to characters,
and merge small prefixes into ranges
until each range fits one shard.
But that's complicated,
and as often the case,
there must be a better way.&lt;sup class=&quot;footnote-ref&quot; id=&quot;fnref-2&quot;&gt;&lt;a href=&quot;https://death.andgravity.com/albumtitle#fn-2&quot;&gt;2&lt;/a&gt;&lt;/sup&gt;&lt;/p&gt;
&lt;h2 id=&quot;but-tries-and-prefix-ranges-are-complicated&quot;&gt;But tries and prefix ranges are complicated&lt;span class=&quot;headerlink&quot;&gt;&amp;nbsp;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-tries-and-prefix-ranges-are-complicated&quot; title=&quot;permalink&quot;&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;We're looking for contiguous ranges,
each of a certain size.
Tries are good for finding the &lt;em&gt;shortest&lt;/em&gt; prefix,
but we don't really care about prefix length.&lt;/p&gt;
&lt;p&gt;Why not just split the sorted titles into N &lt;strong&gt;equal ranges&lt;/strong&gt; instead?
This takes care of the uneven distribution:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boundaries&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;normalized&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gather_every&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;4&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boundaries&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;shape: (2_103, 2)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; title     count&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; the       161&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; live      23&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; …         …&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; 風吹けは  1&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; 魔法少女  1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;...provided a long enough prefix:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boundaries&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;normalized&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gather_every&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;16&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boundaries&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;col&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'count'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;shape: (3, 2)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; title             count&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; greatest hits     3&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; demo              2&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; the very best of  2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;...almost there:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boundaries&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;normalized&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gather_every&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;2000&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boundaries&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;value_counts&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;sort&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;=&lt;/span&gt;&lt;span class=&quot;kc&quot;&gt;True&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;filter&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;pl&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;col&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'count'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;&amp;gt;&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;shape: (2, 2)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; title          count&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; greatest hits  3&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt; demo           2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This highlights another problem –
if the shard size is too small,
there may be more than a shard's worth of albums with identical titles;
we can fix this by using another, random suffix
(ordering doesn't matter anymore, since they have the same title).&lt;/p&gt;
&lt;p&gt;Thankfully, our shards are huge, so it's not an issue:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shard_size&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;nb&quot;&gt;int&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;math&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ceil&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;nb&quot;&gt;len&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;normalized&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;/&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;shard_count&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;))&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shard_size&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;263619&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;boundaries&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;normalized&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;gather_every&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;shard_size&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;str&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;slice&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;mi&quot;&gt;0&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;20&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;To use this,
save the list of boundaries in code,
and find the index of the biggest boundary smaller than a given album title:&lt;/p&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;bisect&lt;/span&gt;
&lt;span class=&quot;kn&quot;&gt;import&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nn&quot;&gt;unicodedata&lt;/span&gt;

&lt;span class=&quot;n&quot;&gt;ALBUM_TITLE_BOUNDARIES&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;p&quot;&gt;[&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;''&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;  &lt;span class=&quot;c1&quot;&gt;# replaced with the smallest possible string&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'agartha'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'barstow / crazy'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'can you feel it'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'cyan rot'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'dreams take over eve'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'feud semiotics (rb. '&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'grave poetry'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'i live'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'kannaval'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'live in florence'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s2&quot;&gt;&amp;quot;mir ist's gleich / i&amp;quot;&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'notice'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'platforms ep'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'rituals'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'skylten'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'surtr / absorbed'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'the human touch'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'tonttujen jouluyö: '&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'walking away'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
    &lt;span class=&quot;s1&quot;&gt;'голос'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt;
&lt;span class=&quot;p&quot;&gt;]&lt;/span&gt;

&lt;span class=&quot;k&quot;&gt;def&lt;/span&gt;&lt;span class=&quot;w&quot;&gt; &lt;/span&gt;&lt;span class=&quot;nf&quot;&gt;album_shard_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;album_title&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;):&lt;/span&gt;
    &lt;span class=&quot;n&quot;&gt;normalized&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;=&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;unicodedata&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;normalize&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'NFKD'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;album_title&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;lower&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;())&lt;/span&gt;
    &lt;span class=&quot;k&quot;&gt;return&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;bisect&lt;/span&gt;&lt;span class=&quot;o&quot;&gt;.&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;bisect&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;n&quot;&gt;ALBUM_TITLE_BOUNDARIES&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;,&lt;/span&gt; &lt;span class=&quot;n&quot;&gt;normalized&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt; &lt;span class=&quot;o&quot;&gt;-&lt;/span&gt; &lt;span class=&quot;mi&quot;&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;div class=&quot;highlight code-container&quot;&gt;&lt;pre class=&quot;code&quot;&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;album_shard_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'2 Pie Island'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;0&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;album_shard_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'Heavy Migration'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;7&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;album_shard_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'Leaving Home'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;9&lt;/span&gt;
&lt;span class=&quot;gp&quot;&gt;&amp;gt;&amp;gt;&amp;gt; &lt;/span&gt;&lt;span class=&quot;n&quot;&gt;album_shard_id&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;(&lt;/span&gt;&lt;span class=&quot;s1&quot;&gt;'Space Cadet'&lt;/span&gt;&lt;span class=&quot;p&quot;&gt;)&lt;/span&gt;
&lt;span class=&quot;go&quot;&gt;15&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&quot;but-the-prefix-distribution-can-change&quot;&gt;But the prefix distribution can change&lt;span class=&quot;headerlink&quot;&gt;&amp;nbsp;&lt;a href=&quot;https://death.andgravity.com/albumtitle#but-the-prefix-distribution-can-change&quot; title=&quot;permalink&quot;&gt;#&lt;/a&gt;&lt;/span&gt;&lt;/h2&gt;
&lt;p&gt;We were lucky to have data on the prefix distribution,
but that's not always the case,
and even if it is, the distribution can change over time.&lt;/p&gt;
&lt;p&gt;For example, the last of the 21 shards above
starts in the Cyrillic &lt;a class=&quot;external&quot; href=&quot;https://en.wikipedia.org/wiki/Unicode_block#List_of_blocks&quot;&gt;Unicode block&lt;/a&gt;,
which means most existing scripts go into a single shard.
What if we import 1 million Chinese and Japanese albums at some point?&lt;/p&gt;
&lt;p&gt;One way to deal with this is to give more weight to known gaps in the data.
Another is to have more shards from the start to account for unknown gaps –
210 shards instead of 21 sounds pretty reasonable.&lt;/p&gt;
&lt;p&gt;If all else fails,
you can move to a new index with more shards,
but that comes with its own &lt;a class=&quot;internal&quot; href=&quot;https://death.andgravity.com/dynamodb-patterns#more-shards&quot;&gt;complications&lt;/a&gt;,
so it's best to get it roughly right from the start.&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=&quot;text-large&quot;&gt;
&lt;span class=&quot;share-icons&quot;&gt;
&lt;a class=&quot;share-icon pycoders color&quot; href=&quot;https://pycoders.com/submissions&quot; target=&quot;_blank&quot;&gt;PyCoder's Weekly&lt;/a&gt;
&lt;a class=&quot;share-icon hacker-news color&quot; href=&quot;https://news.ycombinator.%63%6f%6d/submitlink?u=https%3A//death.andgravity.com/albumtitle&amp;t=Ordered%20key%20sharding%20in%20DynamoDB&quot;&gt;HN&lt;/a&gt;
&lt;a class=&quot;share-icon bluesky color&quot; href=&quot;https://bsky.%61%70%70/intent/compose?text=Ordered%20key%20sharding%20in%20DynamoDB%20https%3A//death.andgravity.com/albumtitle&quot;&gt;Bluesky&lt;/a&gt;
&lt;!--
&lt;a
    class=&quot;share-icon reddit color&quot;
    href=&quot;https://www.reddit.%63%6f%6d/%73%75%62%6d%69%74?url=https%3A//death.andgravity.com/albumtitle&amp;amp;title=Ordered%20key%20sharding%20in%20DynamoDB&quot;
&gt;Reddit&lt;/a&gt;
--&gt;
&lt;a class=&quot;share-icon linkedin color&quot; href=&quot;https://www.linkedin.%63%6f%6d/sharing/share-offsite/?url=https%3A//death.andgravity.com/albumtitle&quot;&gt;linkedin&lt;/a&gt;
&lt;a class=&quot;share-icon twitter color&quot; href=&quot;https://twitter.%63%6f%6d/%73%68%61%72%65?text=Ordered%20key%20sharding%20in%20DynamoDB&amp;url=https%3A//death.andgravity.com/albumtitle&amp;via=_andgravity&quot;&gt;Twitter&lt;/a&gt;
&lt;/span&gt;
&lt;/span&gt;&lt;/p&gt;

&lt;p&gt;&lt;b&gt;
Want to know when new articles come out?

&lt;a href=&quot;https://death.andgravity.com/albumtitle#embedded-subscribe-form&quot;&gt;Subscribe here&lt;/a&gt;
to get new stuff straight to your inbox!

&lt;/b&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li id=&quot;fn-1&quot;&gt;&lt;p&gt;This is a simplified example;
as the &lt;a class=&quot;external&quot; href=&quot;https://musicbrainz.org/doc/MusicBrainz_Database/Schema&quot;&gt;MusicBrainz database&lt;/a&gt; shows,
the schema for this kind of thing would be way more complicated in practice. &lt;a href=&quot;https://death.andgravity.com/albumtitle#fnref-1&quot; class=&quot;footnote&quot;&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li id=&quot;fn-2&quot;&gt;&lt;p&gt;You're welcome to try, though,
especially if you're preparing for an interview. &lt;a href=&quot;https://death.andgravity.com/albumtitle#fnref-2&quot; class=&quot;footnote&quot;&gt;&lt;sup&gt;[return]&lt;/sup&gt;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;</description>
	<pubDate>Mon, 08 Jun 2026 13:43:32 +0000</pubDate>
</item>
<item>
	<title>Wingware: Wing Python IDE 12 Early Access - June 8, 2026</title>
	<guid>https://wingware.com/news/2026-06-08</guid>
	<link>https://wingware.com/news/2026-06-08</link>
	<description>&lt;p&gt;Wing 12 is now available as an early access release that focuses on AI agent
driven development. Wing 12 introduces deep integration with Claude Code,
including a dedicated &lt;tt class=&quot;literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;Claude&lt;/span&gt; &lt;span class=&quot;pre&quot;&gt;Code&lt;/span&gt;&lt;/tt&gt; tool, a new &lt;tt class=&quot;literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;Tasks&lt;/span&gt;&lt;/tt&gt; tool for
planning, executing, and reviewing AI agent work, and a set of MCP servers
that allow agents to work more efficiently by giving them access to Wing's
source code analysis, unit testing, and debugger features.&lt;/p&gt;
&lt;img src=&quot;https://wingware.com/images/screenshots/wing12-screenshot.png&quot; alt=&quot;Wing 12 Screen Shot&quot; class=&quot;eye-candy-image unfloat-at-900&quot; width=&quot;650px&quot; /&gt;&lt;p&gt;For those using Claude Code, Wing 12 broadens the focus from code-centric
direct development to also include managing multiple concurrent AI agents. Of
course all of Wing's classic IDE features are still available -- the powerful
debugger, deep code analysis and warnings, full-featured editor, project &amp;amp;
package management, and much more.&lt;/p&gt;
&lt;p&gt;Wing 12 also adds true pseudo-terminal support to &lt;tt class=&quot;literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;OS&lt;/span&gt; &lt;span class=&quot;pre&quot;&gt;Commands&lt;/span&gt;&lt;/tt&gt; and &lt;tt class=&quot;literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;Debug&lt;/span&gt;
&lt;span class=&quot;pre&quot;&gt;I/O&lt;/span&gt;&lt;/tt&gt;, reenvisions the &lt;tt class=&quot;literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;OS&lt;/span&gt; &lt;span class=&quot;pre&quot;&gt;Commands&lt;/span&gt;&lt;/tt&gt; tool so that commands can be dragged
to tool or editor splits, reorganizes the &lt;tt class=&quot;literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;Tools&lt;/span&gt;&lt;/tt&gt; menu, adds search and
back/forward navigation to the &lt;tt class=&quot;literal&quot;&gt;&lt;span class=&quot;pre&quot;&gt;Preferences&lt;/span&gt;&lt;/tt&gt; dialog, supports automatic
test discovery, speeds up detection of externally modified files, and makes
many other improvements.&lt;/p&gt;
&lt;p&gt;For more information, see the &lt;a class=&quot;reference&quot; href=&quot;https://wingware.com/wingide/early&quot;&gt;Wingware Early Access Program&lt;/a&gt;. Anyone can participate just by
downloading the release.&lt;/p&gt;
&lt;p&gt;If you have questions, please don't hesitate to contact us at &lt;a class=&quot;reference&quot; href=&quot;mailto:support@wingware.com&quot;&gt;support&amp;#64;wingware.com&lt;/a&gt;.&lt;/p&gt;</description>
	<pubDate>Mon, 08 Jun 2026 01:00:00 +0000</pubDate>
</item>
<item>
	<title>Eli Bendersky: Plugins case study: mdBook preprocessors</title>
	<guid>https://eli.thegreenplace.net/2025/plugins-case-study-mdbook-preprocessors/</guid>
	<link>https://eli.thegreenplace.net/2025/plugins-case-study-mdbook-preprocessors/</link>
	<description>&lt;p&gt;&lt;a class=&quot;reference external&quot; href=&quot;https://rust-lang.github.io/mdBook/index.html&quot;&gt;mdBook&lt;/a&gt; is a tool for easily
creating books out of Markdown files. It's very popular in the Rust ecosystem,
where it's used (among other things) to publish &lt;a class=&quot;reference external&quot; href=&quot;https://doc.rust-lang.org/book/&quot;&gt;the official Rust book&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;mdBook has a simple yet effective plugin mechanism that can be used to modify
the book output in arbitrary ways, using any programming language or tool. This
post describes the mechanism and how it aligns with the
&lt;a class=&quot;reference external&quot; href=&quot;https://eli.thegreenplace.net/2012/08/07/fundamental-concepts-of-plugin-infrastructures&quot;&gt;fundamental concepts of plugin infrastructures&lt;/a&gt;.&lt;/p&gt;
&lt;div class=&quot;section&quot; id=&quot;mdbook-preprocessors&quot;&gt;
&lt;h2&gt;mdBook preprocessors&lt;/h2&gt;
&lt;p&gt;mdBook's architecture is pretty simple: your contents go into a directory tree
of Markdown files. mdBook then renders these into a book, with one file per
chapter. The book's output is HTML by default, but mdBook supports other
outputs like PDF.&lt;/p&gt;
&lt;p&gt;The &lt;a class=&quot;reference external&quot; href=&quot;https://rust-lang.github.io/mdBook/for_developers/preprocessors.html&quot;&gt;preprocessor mechanism&lt;/a&gt;
lets us register an arbitrary program that runs on the book's source after
it's loaded from Markdown files; this program can modify the book's contents in
any way it wishes before it all gets sent to the renderer for generating output.&lt;/p&gt;
&lt;img alt=&quot;Preprocessor flow for mdbook&quot; class=&quot;align-center&quot; src=&quot;https://eli.thegreenplace.net/images/2025/mdbook-preprocessor.png&quot; /&gt;
&lt;p&gt;The official documentation &lt;a class=&quot;reference external&quot; href=&quot;https://rust-lang.github.io/mdBook/for_developers/preprocessors.html#hooking-into-mdbook&quot;&gt;explains this process very well&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;section&quot; id=&quot;sample-plugin&quot;&gt;
&lt;h2&gt;Sample plugin&lt;/h2&gt;
&lt;p&gt;I rewrote &lt;a class=&quot;reference external&quot; href=&quot;https://eli.thegreenplace.net/2012/08/07/fundamental-concepts-of-plugin-infrastructures&quot;&gt;my classical &amp;quot;nacrissist&amp;quot; plugin&lt;/a&gt;
for mdBook; the code is &lt;a class=&quot;reference external&quot; href=&quot;https://github.com/eliben/code-for-blog/tree/main/2025/plugin-mdbook&quot;&gt;available here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In fact, there are two renditions of the same plugin there:&lt;/p&gt;
&lt;ol class=&quot;arabic simple&quot;&gt;
&lt;li&gt;One in Python, to demonstrate how mdBook can invoke preprocessors written in
any programming language.&lt;/li&gt;
&lt;li&gt;One in Rust, to demonstrate how mdBook exposes an application API to plugins
written in Rust (since mdBook is itself written in Rust).&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;
&lt;div class=&quot;section&quot; id=&quot;fundamental-plugin-concepts-in-this-case-study&quot;&gt;
&lt;h2&gt;Fundamental plugin concepts in this case study&lt;/h2&gt;
&lt;p&gt;Let's see how this case study of mdBook preprocessors measures against the
&lt;a class=&quot;reference external&quot; href=&quot;https://eli.thegreenplace.net/2012/08/07/fundamental-concepts-of-plugin-infrastructures&quot;&gt;Fundamental plugin concepts&lt;/a&gt;
that were covered &lt;a class=&quot;reference external&quot; href=&quot;https://eli.thegreenplace.net/tag/plugins&quot;&gt;several times on this blog&lt;/a&gt;.&lt;/p&gt;
&lt;div class=&quot;section&quot; id=&quot;discovery&quot;&gt;
&lt;h3&gt;Discovery&lt;/h3&gt;
&lt;p&gt;Discovery in mdBook is very explicit. For every plugin we want mdBook to use,
it has to be listed in the project's &lt;tt class=&quot;docutils literal&quot;&gt;book.toml&lt;/tt&gt; configuration file. For
example, in &lt;a class=&quot;reference external&quot; href=&quot;https://github.com/eliben/code-for-blog/tree/main/2025/plugin-mdbook&quot;&gt;the code sample for this post&lt;/a&gt;, the Python narcissist plugin
is noted in &lt;tt class=&quot;docutils literal&quot;&gt;book.toml&lt;/tt&gt; as follows:&lt;/p&gt;
&lt;div class=&quot;highlight&quot;&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;[preprocessor.narcissistpy]
command = &amp;quot;python3 ../preprocessor-python-narcissist/narcissist.py&amp;quot;
&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Each preprocessor is a command for &lt;tt class=&quot;docutils literal&quot;&gt;mdBook&lt;/tt&gt; to execute in a sub-process.
Here it uses Python, but it can be anything else that can be validly executed.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;section&quot; id=&quot;registration&quot;&gt;
&lt;h3&gt;Registration&lt;/h3&gt;
&lt;p&gt;For the purpose of registration, &lt;tt class=&quot;docutils literal&quot;&gt;mdBook&lt;/tt&gt; actually invokes the plugin command
&lt;em&gt;twice&lt;/em&gt;. The first time, it passes the arguments &lt;tt class=&quot;docutils literal&quot;&gt;supports &amp;lt;renderer&amp;gt;&lt;/tt&gt; where
&lt;tt class=&quot;docutils literal&quot;&gt;&amp;lt;renderer&amp;gt;&lt;/tt&gt; is the name of the renderer (e.g. &lt;tt class=&quot;docutils literal&quot;&gt;html&lt;/tt&gt;). If the command
returns 0, it means the preprocessor supports this renderer; otherwise, it
doesn't.&lt;/p&gt;
&lt;p&gt;In the second invocation, &lt;tt class=&quot;docutils literal&quot;&gt;mdBook&lt;/tt&gt; passes some metadata plus the entire book
in JSON format to the preprocessor through stdin, and expects the preprocessor
to return the modified book as JSON to stdout (using the same schema).&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;section&quot; id=&quot;hooks&quot;&gt;
&lt;h3&gt;Hooks&lt;/h3&gt;
&lt;p&gt;In terms of hooks, &lt;tt class=&quot;docutils literal&quot;&gt;mdBook&lt;/tt&gt; takes a very coarse-grained approach. The
preprocessor gets the &lt;em&gt;entire book&lt;/em&gt; in a single JSON object (along with a
context object that contains metadata), and is expected to emit the entire
modified book in a single JSON object.
It's up to the preprocessor to figure out which parts of the book to read and
which parts to modify.&lt;/p&gt;
&lt;p&gt;Given that books and other documentation typically have limited sizes, this
is a reasonable design choice. Even tens of MiB of JSON-encoded data are very
quick to pass between sub-processes via stdout and marshal/unmarshal. But we
wouldn't be able to implement Wikipedia using this design.&lt;/p&gt;
&lt;/div&gt;
&lt;div class=&quot;section&quot; id=&quot;exposing-an-application-api-to-plugins&quot;&gt;
&lt;h3&gt;Exposing an application API to plugins&lt;/h3&gt;
&lt;p&gt;This is tricky, given that the preprocessor mechanism is language-agnostic.
Here, &lt;tt class=&quot;docutils literal&quot;&gt;mdBook&lt;/tt&gt; only offers additional utilities to preprocessors implemented
in Rust. These get access to &lt;tt class=&quot;docutils literal&quot;&gt;mdBook&lt;/tt&gt;'s API to unmarshal the JSON
representing the context metadata and book's contents. &lt;tt class=&quot;docutils literal&quot;&gt;mdBook&lt;/tt&gt; offers the
&lt;a class=&quot;reference external&quot; href=&quot;https://docs.rs/mdbook-preprocessor/latest/mdbook_preprocessor/trait.Preprocessor.html&quot;&gt;Preprocessor trait&lt;/a&gt;
Rust preprocessors can implement, which makes it easier to wrangle the book's
contents. See &lt;a class=&quot;reference external&quot; href=&quot;https://github.com/eliben/code-for-blog/tree/main/2025/plugin-mdbook/preprocessor-rust-narcissist&quot;&gt;my Rust version of the narcissist preprocessor&lt;/a&gt;
for a basic example of this.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div class=&quot;section&quot; id=&quot;renderers-backends&quot;&gt;
&lt;h2&gt;Renderers / backends&lt;/h2&gt;
&lt;p&gt;Actually, &lt;tt class=&quot;docutils literal&quot;&gt;mdBook&lt;/tt&gt; has &lt;em&gt;another&lt;/em&gt; plugin mechanism, but it's very similar
conceptually to preprocessors. A &lt;em&gt;renderer&lt;/em&gt; (also called a &lt;em&gt;backend&lt;/em&gt; in some
of &lt;tt class=&quot;docutils literal&quot;&gt;mdBook&lt;/tt&gt;'s own doc pages) takes the same input as a preprocessor, but is
free to do whatever it wants with it. The default renderer emits the HTML
for the book; &lt;a class=&quot;reference external&quot; href=&quot;https://github.com/rust-lang/mdBook/wiki/Third-party-plugins#backends&quot;&gt;other renderers&lt;/a&gt;
can do other things.&lt;/p&gt;
&lt;p&gt;The idea is that the book can go through multiple preprocessors, but at the
end a &lt;em&gt;single&lt;/em&gt; renderer.&lt;/p&gt;
&lt;p&gt;The data a renderer receives is exactly the same as a preprocessor - JSON
encoded book contents. Due to this similarity, there's no real point getting
deeper into renderers in this post.&lt;/p&gt;
&lt;/div&gt;</description>
	<pubDate>Sun, 07 Jun 2026 07:38:58 +0000</pubDate>
</item>
<item>
	<title>Armin Ronacher: Communities of Not</title>
	<guid>https://lucumr.pocoo.org/2026/6/6/communities-of-not/</guid>
	<link>https://lucumr.pocoo.org/2026/6/6/communities-of-not/</link>
	<description>&lt;p&gt;There is a strange thing that happens in communities that gather around
abstinence from something: identity from opposition.  At their best these
communities are not &lt;em&gt;just&lt;/em&gt; negative: childfree spaces can be about autonomy,
choice and acceptance, anti-car spaces about safer streets and transit, and
LLM-skeptical developer spaces about the future of labor, code quality and
slop&lt;sup class=&quot;footnote-ref&quot; id=&quot;fnref-1&quot;&gt;&lt;a href=&quot;https://lucumr.pocoo.org/feed.atom#fn-1&quot;&gt;1&lt;/a&gt;&lt;/sup&gt;.  But the thing being refused often does not go away and instead
becomes the main subject of the community&amp;#8217;s identity.&lt;/p&gt;
&lt;p&gt;That would be fine if it stayed at criticism, maybe even angry criticism, but
more often than not it turns into policing and hatred towards others.  An
influencer without children becomes a parent, an urban bike commuter by choice
buys a Porsche, a respected developer tries LLMs, and the community feels
betrayed because it assumed they were members of the same tribe.  The expulsion
of that person (who never signed up to be a community member) is entirely
imaginary but the punishment that the community unleashes is not: people pile on
and shame them, quote them out of context and turn their weakest moments into
proof that the person was always unserious, a sharlatan or should not be
listened to.&lt;/p&gt;
&lt;p&gt;I do not think the answer is to tell people to stop paying attention.  Cars
shape cities even for people who cycle, children influence politics, workplaces
and taxes even for people who do not have them.  For us developers, LLMs show up
in editors, issue trackers, hiring conversations, management pressure and code
reviews whether we asked for them or not.  Resisting that can be legitimate but
that is no excuse for using one&amp;#8217;s rejection to justify shitty mob behavior.&lt;/p&gt;
&lt;p&gt;I understand the thinking all too well, because I have done versions of this
myself in the past.  It took me a while to become more accepting of other
people&amp;#8217;s worldviews that diverge from mine.  Whatever insecurities we have,
finding a group of others sharing them can be comforting.  The danger is that
being part of a crowd of negativity can easily make us part of collective
harassment.&lt;/p&gt;
&lt;p&gt;I can only encourage you to breathe, slow down, de-escalate when given the
chance, and resist the temptation to always assume the most catastrophic
reading.  Default to being &lt;a href=&quot;https://lucumr.pocoo.org/2026/4/11/the-center-has-a-bias/&quot;&gt;open to new things&lt;/a&gt;.
Being negative towards something, and making that ones identity, is an easy trap
to fall into.&lt;/p&gt;
&lt;div class=&quot;footnotes&quot;&gt;
&lt;ol&gt;
&lt;li id=&quot;fn-1&quot;&gt;
&lt;p&gt;These examples are not meant as equivalents.  The recent
&lt;a href=&quot;https://github.com/RsyncProject/rsync/issues/929&quot;&gt;mob&lt;/a&gt; &lt;a href=&quot;https://mastodon.gamedev.place/@JeremiahFieldhaven/116654345332213390&quot;&gt;against
rsync&lt;/a&gt;
is the LLM version that prompted this post.  I picked the others because I&amp;#8217;m
familiar with those communities and they all show similar cases of personal
choices being interpreted as betrayal.&lt;a href=&quot;https://lucumr.pocoo.org/feed.atom#fnref-1&quot; class=&quot;footnote&quot;&gt;&amp;#8617;&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;/div&gt;</description>
	<pubDate>Sat, 06 Jun 2026 00:00:00 +0000</pubDate>
</item>
<item>
	<title>Will Kahn-Greene: Bleach 6.4.0 releases -- final release</title>
	<guid>https://bluesock.org/~willkg/blog/dev/bleach_6_4_0_final_release.html</guid>
	<link>https://bluesock.org/~willkg/blog/dev/bleach_6_4_0_final_release.html</link>
	<description>&lt;h2&gt;What is it?&lt;/h2&gt;
&lt;p&gt;&lt;a class=&quot;reference external&quot; href=&quot;https://bleach.readthedocs.io/&quot;&gt;Bleach&lt;/a&gt; is a Python library for sanitizing
and linkifying text from untrusted sources for safe usage in HTML.&lt;/p&gt;


&lt;h2&gt;Bleach v6.4.0 released!&lt;/h2&gt;
&lt;p&gt;Bleach 6.4.0 includes two security fixes, a fix to tinycss2 dependency
requirements, and some other things.&lt;/p&gt;
&lt;p&gt;See the changes here:&lt;/p&gt;
&lt;p&gt;&lt;a class=&quot;reference external&quot; href=&quot;https://bleach.readthedocs.io/en/latest/changes.html#version-6-4-0-june-5th-2026&quot;&gt;https://bleach.readthedocs.io/en/latest/changes.html#version-6-4-0-june-5th-2026&lt;/a&gt;&lt;/p&gt;


&lt;h2&gt;Bleach v6.4.0 is the final release&lt;/h2&gt;
&lt;p&gt;I haven't used Bleach on a project in years, but I still had some time to
maintain it. That changed about a year ago when I got re-orged into a new role
and I haven't had time to do any Bleach work since then.&lt;/p&gt;
&lt;p&gt;To recap, Bleach sits on top of
&lt;a class=&quot;reference external&quot; href=&quot;https://github.com/html5lib/html5lib-python&quot;&gt;html5lib&lt;/a&gt; which hasn't
been actively maintained in years. It is dangerous to maintain Bleach in that
context.&lt;/p&gt;
&lt;p&gt;We vendored html5lib so we could make adjustments to the library to keep Bleach
going. This is not a sustainable approach, but it was ok for the short term.&lt;/p&gt;
&lt;p&gt;Over the years, we've talked about other options:&lt;/p&gt;
&lt;ol class=&quot;arabic simple&quot;&gt;
&lt;li&gt;&lt;p&gt;find another library to switch to&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;take over html5lib development&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;fork html5lib and vendor and maintain our fork&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;write a new HTML parser&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;etc&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;None of those are feasible for me.&lt;/p&gt;
&lt;p&gt;Bleach has been a solo-maintained project for a while now. The world is crazy
and it's much harder to build a team of trusted maintainers now than it was (or
at least, it sure feels that way). I don't see any possibility of increasing
the maintenance team or passing it to someone else responsibly.&lt;/p&gt;
&lt;p&gt;Switching contexts from my regular work to Bleach is really hard. Bleach is
complicated, the problem domain is complicated, and there's a lot of nuanced
context. I can't just switch gears, spend 15 minutes on Bleach to do something,
and then switch back to the rest of my day. I periodically get nag messages
about this which are entirely valid, but there's nothing I can do about it.
It doesn't feel great.&lt;/p&gt;
&lt;p&gt;Then in 2025, Emil, a long-time Bleach contributor, built
&lt;a class=&quot;reference external&quot; href=&quot;https://emilstenstrom.github.io/justhtml/&quot;&gt;justhtml&lt;/a&gt; which gives us an easy
migration path off of Bleach. He even took the time to write a
&lt;a class=&quot;reference external&quot; href=&quot;https://emilstenstrom.github.io/justhtml/bleach-migration.html&quot;&gt;migration guide&lt;/a&gt;.&lt;/p&gt;


&lt;h2&gt;Thoughts and statistics&lt;/h2&gt;
&lt;p&gt;In 2019, when I stepped down the first time, I wrote
&lt;a class=&quot;reference external&quot; href=&quot;https://bluesock.org/~willkg/blog/dev/bleach_stepping_down.html&quot;&gt;a post on stepping down&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;In 2023, when I deprecated the project, I wrote
&lt;a class=&quot;reference external&quot; href=&quot;https://bluesock.org/~willkg/blog/dev/bleach_6_0_0_deprecation.html&quot;&gt;a post on Bleach 6.0.0 and deprecation&lt;/a&gt;.&lt;/p&gt;
&lt;ul class=&quot;simple&quot;&gt;
&lt;li&gt;&lt;p&gt;From the first commit on 2010-02-18 to today's final commit on 2026-06-05,
the Bleach project lasted 16 years, 3 months — 5,951 days, or about 16.29
years.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There were 64 releases.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There were roughly 960 commits.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;From 80 roughly contributors&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Top 3:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Will Kahn-Greene: 462&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;James Socol: 182&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Greg Guthe: 133&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Roughly 5,040 lines of Python code excluding the vendored html5lib.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I was maintainer from October 2015 to now--that's a little under 11 years.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It feels weird to end a project that's outlived many of the Mozilla sites and
Python web frameworks it was designed to protect.&lt;/p&gt;


&lt;h2&gt;What happens now?&lt;/h2&gt;
&lt;p&gt;This is the end of the project.&lt;/p&gt;

&lt;a class=&quot;reference external image-reference&quot; href=&quot;https://bluesock.org/~willkg/blog/images/bleach_deprecation.jpg&quot;&gt;
&lt;img alt=&quot;/images/bleach_deprecation.thumbnail.jpg&quot; src=&quot;https://bluesock.org/~willkg/blog/images/bleach_deprecation.thumbnail.jpg&quot; /&gt;
&lt;/a&gt;

&lt;p&gt;Bleach. Last release.&lt;/p&gt;


&lt;p&gt;If you're still using Bleach, I think you have three options:&lt;/p&gt;
&lt;ol class=&quot;arabic simple&quot;&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;End your project.&lt;/strong&gt; Maybe you don't need to be maintaining your thing
anymore? Use Bleach as your reason to exit and do something different with
your time on Earth.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Switch to the sanitizer API.&lt;/strong&gt; Rework your project to use the sanitizer API.&lt;/p&gt;
&lt;ul class=&quot;simple&quot;&gt;
&lt;li&gt;&lt;p&gt;Spec: &lt;a class=&quot;reference external&quot; href=&quot;https://wicg.github.io/sanitizer-api/&quot;&gt;https://wicg.github.io/sanitizer-api/&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Docs: &lt;a class=&quot;reference external&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/Element/setHTML&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/API/Element/setHTML&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Swap Bleach out for justhtml.&lt;/strong&gt; Emil provided a
&lt;a class=&quot;reference external&quot; href=&quot;https://emilstenstrom.github.io/justhtml/bleach-migration.html&quot;&gt;migration guide&lt;/a&gt;
for switching from Bleach to justhtml.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Good luck with whatever option you choose!&lt;/p&gt;


&lt;h2&gt;Thanks!&lt;/h2&gt;
&lt;p&gt;Many thanks to &lt;a class=&quot;reference external&quot; href=&quot;https://github.com/jsocol&quot;&gt;James&lt;/a&gt; who created Bleach and
gave it a set of first principles that guided our choices for 16 years.&lt;/p&gt;
&lt;p&gt;Many thanks to &lt;a class=&quot;reference external&quot; href=&quot;https://github.com/g-k&quot;&gt;Greg&lt;/a&gt; who I worked with on Bleach
for a long while and maintained Bleach for several years. Working with Greg was
always easy and his reviews were thoughtful and spot-on.&lt;/p&gt;
&lt;p&gt;Many thanks to &lt;a class=&quot;reference external&quot; href=&quot;https://github.com/EmilStenstrom&quot;&gt;Emil&lt;/a&gt; who was
a contributor to Bleach for a long while and created
&lt;a class=&quot;reference external&quot; href=&quot;https://emilstenstrom.github.io/justhtml/&quot;&gt;justhtml&lt;/a&gt;
providing Bleach users a migration path.&lt;/p&gt;
&lt;p&gt;Many thanks to &lt;a class=&quot;reference external&quot; href=&quot;https://github.com/jvanasco&quot;&gt;Jonathan&lt;/a&gt; who, over the years,
provided a lot of insight into how best to solve some of Bleach's more
squirrely problems.&lt;/p&gt;
&lt;p&gt;Many thanks to &lt;a class=&quot;reference external&quot; href=&quot;https://github.com/gsnedders&quot;&gt;Sam&lt;/a&gt; who was an indispensible
resource on HTML parsing and sanitizing text in the context of HTML.&lt;/p&gt;
&lt;p&gt;Many thanks to all the users and contributors of Bleach!&lt;/p&gt;


&lt;h2&gt;Where to go for more&lt;/h2&gt;
&lt;p&gt;For more specifics on this release, see here:
&lt;a class=&quot;reference external&quot; href=&quot;https://bleach.readthedocs.io/en/latest/changes.html#version-6-4-0-june-5th-2026&quot;&gt;https://bleach.readthedocs.io/en/latest/changes.html#version-6-4-0-june-5th-2026&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Documentation and quickstart here:
&lt;a class=&quot;reference external&quot; href=&quot;https://bleach.readthedocs.io/en/latest/&quot;&gt;https://bleach.readthedocs.io/en/latest/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Source code and issue tracker here:
&lt;a class=&quot;reference external&quot; href=&quot;https://github.com/mozilla/bleach/&quot;&gt;https://github.com/mozilla/bleach/&lt;/a&gt;&lt;/p&gt;</description>
	<pubDate>Fri, 05 Jun 2026 13:00:00 +0000</pubDate>
</item>
<item>
	<title>Real Python: The Real Python Podcast – Episode #298: Reducing the Size of Python Docker Containers</title>
	<guid>https://realpython.com/podcasts/rpp/298/</guid>
	<link>https://realpython.com/podcasts/rpp/298/</link>
	<description>&lt;p&gt;How can you easily reduce the size of a Python Docker container? What are the exceptions you should catch in your code? Christopher Trudeau is back on the show this week with another batch of PyCoder's Weekly articles and projects.&lt;/p&gt;
        &lt;hr /&gt;
        &lt;p&gt;&lt;em&gt;[ Improve Your Python With 🐍 Python Tricks 💌 – Get a short &amp;amp; sweet Python Trick delivered to your inbox every couple of days. &lt;a href=&quot;https://realpython.com/python-tricks/?utm_source=realpython&amp;utm_medium=rss&amp;utm_campaign=footer&quot;&gt;&amp;gt;&amp;gt; Click here to learn more and see examples&lt;/a&gt; ]&lt;/em&gt;&lt;/p&gt;</description>
	<pubDate>Fri, 05 Jun 2026 12:00:00 +0000</pubDate>
</item>
<item>
	<title>EuroPython Society: EuroPython Society at PyCon US 2026</title>
	<guid>https://www.europython-society.org/europython-society-at-pycon-us-2026/</guid>
	<link>https://www.europython-society.org/europython-society-at-pycon-us-2026/</link>
	<description>&lt;p&gt;This year we were back at PyCon US, and this time in sunny Long Beach, California.&lt;/p&gt;&lt;p&gt;We had a booth again, which has quickly become one of our favourite parts of the trip. It&amp;amp;aposs such a great chance to meet folks from other Python communities, catch up with old friends, and put faces to names we&amp;amp;aposve only seen online. People stopped by to chat about EuroPython, pick up stickers, ask about our grants programme, and share what their own local communities are up to. We loved every minute of it.&lt;/p&gt;&lt;img src=&quot;https://www.europython-society.org/content/images/2026/06/us1.jpg&quot; class=&quot;kg-image&quot; alt=&quot;alt&quot; width=&quot;1069&quot; height=&quot;802&quot; /&gt;&lt;img src=&quot;https://www.europython-society.org/content/images/2026/06/us2.jpg&quot; class=&quot;kg-image&quot; alt=&quot;alt&quot; width=&quot;803&quot; height=&quot;802&quot; /&gt;&lt;p&gt;We also filmed some shorts at the booth, which will be up on our YouTube channel soon! Keep an eye out, there are some lovely conversations in there.&lt;/p&gt;&lt;p&gt;Since EuroPython is celebrating its 25th anniversary this year, we took the chance to talk to community members who have been to many, many EuroPythons over the years. Hearing their stories, the editions they remember most, the friendships that started at one of our conferences, was genuinely moving. It&amp;amp;aposs a good reminder of how much history this community carries with it, and how much of it has been built by people simply showing up year after year.&lt;/p&gt;&lt;p&gt;PyCon US was also where some wonderful people from our community received well-deserved recognition. A huge congratulations to Maria Jose Montreas-Colina, who received an Outstanding PyLady Award for her work with PyLadies and the wider community. Maria is part of our team and helps look after PyLadies and community matters at EuroPython. Congratulations also to Rodrigo Gir&amp;#xE3;o Serr&amp;#xE3;o for receiving the Community Service Award for his contributions to the community. Rodrigo works on our programme and sprints.&lt;/p&gt;&lt;p&gt;Thank you both for everything you do. &amp;#x1F49B;&lt;/p&gt;&lt;img src=&quot;https://www.europython-society.org/content/images/2026/06/us3.jpg&quot; class=&quot;kg-image&quot; alt=&quot;alt&quot; width=&quot;1069&quot; height=&quot;802&quot; /&gt;&lt;img src=&quot;https://www.europython-society.org/content/images/2026/06/us4.jpg&quot; class=&quot;kg-image&quot; alt=&quot;alt&quot; width=&quot;1069&quot; height=&quot;802&quot; /&gt;&lt;p&gt;Long Beach itself was a lovely city. Palm trees, warm weather, the ocean nearby. A very different vibe from the usual conference cities, and a really lovely backdrop for a week of Python.&lt;/p&gt;&lt;p&gt;A big thank you to the PyCon US organisers for having us, and for making space for the wider Python world to come together. And a thank you to everyone who stopped by the booth to say hello, it was a pleasure meeting you.&lt;/p&gt;&lt;p&gt;See you next year, and we hope to see many of you in Krak&amp;#xF3;w for EuroPython 2026!&lt;/p&gt;&lt;img src=&quot;https://www.europython-society.org/content/images/2026/06/us8.jpg&quot; class=&quot;kg-image&quot; alt=&quot;alt&quot; width=&quot;1069&quot; height=&quot;802&quot; /&gt;&lt;img src=&quot;https://www.europython-society.org/content/images/2026/06/us6.jpg&quot; class=&quot;kg-image&quot; alt=&quot;alt&quot; width=&quot;1426&quot; height=&quot;802&quot; /&gt;</description>
	<pubDate>Fri, 05 Jun 2026 09:28:38 +0000</pubDate>
</item>
<item>
	<title>Bob Belderbos: How to Update Multiple Page Elements from One htmx Request</title>
	<guid>https://belderbos.dev/blog/htmx-hx-swap-oob-django/</guid>
	<link>https://belderbos.dev/blog/htmx-hx-swap-oob-django/</link>
	<description>&lt;p&gt;A button submits code, tests run, feedback appears. Standard htmx. But the submissions dropdown stays stale; the new submission is in the database, just not in the dropdown. One request, two elements to update.&lt;/p&gt;
&lt;span id=&quot;continue-reading&quot;&gt;&lt;/span&gt;&lt;h2 id=&quot;the-problem-one-request-two-things-to-update&quot;&gt;The problem: one request, two things to update&lt;/h2&gt;
&lt;p&gt;On our &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https://rustplatform.com/&quot;&gt;Rust platform&lt;/a&gt;, each exercise page has a &quot;Run Tests&quot; button. It posts the editor code to a Django view, which compiles and runs the tests, then swaps a pass/fail panel into a &lt;code&gt;#feedback&lt;/code&gt; div.&lt;/p&gt;
&lt;p&gt;Next to the editor there is a dropdown of your past submissions. Run the tests, and a new submission gets saved server-side. But the dropdown stayed stale until you reloaded the page. The new submission was there in the database, just not in the &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So now I have one request that needs to update two unrelated parts of the page: the feedback panel (the htmx target) and the submissions dropdown (somewhere else entirely in the DOM).&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://belderbos.dev/images/htmx-oob.png&quot; alt=&quot;Rust platform exercise page showing feedback panel and submissions dropdown&quot; /&gt;&lt;/p&gt;
&lt;p&gt;First instinct: write JavaScript to read the response, build a new &lt;code&gt;&amp;lt;option&amp;gt;&lt;/code&gt;, prepend it to the select. That works until you remember the dropdown also enforces a max number of submissions, drops duplicates, and orders newest-first. Replicate that logic in the browser and you now have two sources of truth that drift apart the first time you change the server rule.&lt;/p&gt;
&lt;h2 id=&quot;htmx-out-of-band-swaps&quot;&gt;Htmx out-of-band swaps&lt;/h2&gt;
&lt;p&gt;htmx has a feature for exactly this: &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https://htmx.org/attributes/hx-swap-oob/&quot;&gt;out-of-band swaps&lt;/a&gt;.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The hx-swap-oob attribute allows you to specify that some content in a response should be swapped into the DOM somewhere other than the target, that is &quot;Out of Band&quot;. This allows you to piggyback updates to other element updates on a response.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;htmx swaps any element carrying &lt;code&gt;hx-swap-oob&lt;/code&gt; into its matching target on the page, separately from the main swap. One response, many updates.&lt;/p&gt;
&lt;p&gt;First I pulled the submissions dropdown options into a partial so the page and the view render them identically:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;&amp;lt;!-- _submission_options.html --&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;z-entity z-name z-tag&quot;&gt;option&lt;/span&gt;&lt;span class=&quot;z-entity&quot;&gt; disabled selected&lt;/span&gt;&lt;span&gt;&amp;gt;Submissions / Reset&amp;lt;/&lt;/span&gt;&lt;span class=&quot;z-entity z-name z-tag&quot;&gt;option&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{% for submission in submissions %}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;  &amp;lt;&lt;/span&gt;&lt;span class=&quot;z-entity z-name z-tag&quot;&gt;option&lt;/span&gt;&lt;span class=&quot;z-entity&quot;&gt; value&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;{{ submission.unique_hash }}&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;{{ submission.created_at|date:&amp;quot;Y-m-d H:i&amp;quot; }} {% if submission.ok %}(OK){% else %}(Failed){% endif %}&amp;lt;/&lt;/span&gt;&lt;span class=&quot;z-entity z-name z-tag&quot;&gt;option&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;{% endfor %}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;&amp;lt;&lt;/span&gt;&lt;span class=&quot;z-entity z-name z-tag&quot;&gt;option&lt;/span&gt;&lt;span class=&quot;z-entity&quot;&gt; value&lt;/span&gt;&lt;span&gt;=&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;reset&amp;quot;&lt;/span&gt;&lt;span&gt;&amp;gt;Reset&amp;lt;/&lt;/span&gt;&lt;span class=&quot;z-entity z-name z-tag&quot;&gt;option&lt;/span&gt;&lt;span&gt;&amp;gt;&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The page includes it inside the &lt;code&gt;&amp;lt;select id=&quot;submissions&quot;&amp;gt;&lt;/code&gt;. The view renders the same partial and tags it for an out-of-band swap:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;from&lt;/span&gt;&lt;span&gt; django.template.loader&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; import&lt;/span&gt;&lt;span&gt; render_to_string&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;from&lt;/span&gt;&lt;span&gt; django.utils.html&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; import&lt;/span&gt;&lt;span&gt; escape&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;def&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt; validate&lt;/span&gt;&lt;span class=&quot;z-variable z-parameter z-function&quot;&gt;(request):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;    # ... run the tests, save the submission ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    submissions&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; Submission.objects.filter(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable&quot;&gt;        exercise&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span&gt;exercise,&lt;/span&gt;&lt;span class=&quot;z-variable&quot;&gt; user&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;=&lt;/span&gt;&lt;span&gt;user&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    ).order_by(&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;-created_at&amp;quot;&lt;/span&gt;&lt;span&gt;)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    options&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span&gt; render_to_string(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;        &amp;quot;_submission_options.html&amp;quot;&lt;/span&gt;&lt;span&gt;, {&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;submissions&amp;quot;&lt;/span&gt;&lt;span&gt;: submissions}&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;    return&lt;/span&gt;&lt;span&gt; HttpResponse(&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;        f&lt;/span&gt;&lt;span class=&quot;z-string&quot;&gt;&amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-string&quot;&gt;        &amp;lt;div class=&amp;quot;...&amp;quot;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;{&lt;/span&gt;&lt;span&gt;message&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;z-string&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-string&quot;&gt;        &amp;lt;pre&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;{&lt;/span&gt;&lt;span&gt;escape(output)&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;z-string&quot;&gt;&amp;lt;/pre&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-string&quot;&gt;        &amp;lt;div hx-swap-oob=&amp;quot;innerHTML:#submissions&amp;quot;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;{&lt;/span&gt;&lt;span&gt;options&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;z-string&quot;&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-string&quot;&gt;        &amp;quot;&amp;quot;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The first part of the response swaps into &lt;code&gt;#feedback&lt;/code&gt; as usual. htmx spots the &lt;code&gt;hx-swap-oob&lt;/code&gt; element, pulls it out, and applies it to &lt;code&gt;#submissions&lt;/code&gt; instead. The button HTML only knows about &lt;code&gt;#feedback&lt;/code&gt;. The view decides what else to update. Whoever owns the data controls how it renders.&lt;/p&gt;
&lt;h2 id=&quot;a-second-example-progress-bars-that-update-themselves&quot;&gt;A second example: progress bars that update themselves&lt;/h2&gt;
&lt;p&gt;On the Python platform the same trick drives the learning-path progress widget. Passing an exercise recomputes your progress along every path it belongs to and swaps the bars into a sidebar, from the same request that renders the pass/fail panel:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;if&lt;/span&gt;&lt;span&gt; ok:&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    paths_html&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string&quot;&gt; &amp;quot;&amp;quot;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;    for&lt;/span&gt;&lt;span&gt; path&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; in&lt;/span&gt;&lt;span&gt; bite.bite_paths.prefetch_related(&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;bites&amp;quot;&lt;/span&gt;&lt;span&gt;):&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt;        # ... compute completed / total / pct for this path ...&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;        paths_html&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; +=&lt;/span&gt;&lt;span&gt; render_progress_bar(path, completed, total, pct)&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    extra_html&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; +=&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;        f&lt;/span&gt;&lt;span class=&quot;z-string&quot;&gt;'&amp;lt;div id=&amp;quot;learning-paths-progress&amp;quot; hx-swap-oob=&amp;quot;innerHTML&amp;quot;&amp;gt;&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;{&lt;/span&gt;&lt;span&gt;paths_html&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;}&lt;/span&gt;&lt;span class=&quot;z-string&quot;&gt;&amp;lt;/div&amp;gt;'&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;    )&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The two examples aim at their targets differently. The progress widget uses a bare &lt;code&gt;hx-swap-oob=&quot;innerHTML&quot;&lt;/code&gt;: htmx swaps the fragment into whatever element already shares its &lt;code&gt;id&lt;/code&gt;. The dropdown uses the selector form, &lt;code&gt;hx-swap-oob=&quot;innerHTML:#submissions&quot;&lt;/code&gt;, so the carrier &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt; can target the &lt;code&gt;&amp;lt;select id=&quot;submissions&quot;&amp;gt;&lt;/code&gt; without needing to share its id.&lt;/p&gt;
&lt;p&gt;Use &lt;code&gt;innerHTML&lt;/code&gt; instead of the default &lt;code&gt;hx-swap-oob=&quot;true&quot;&lt;/code&gt;. &lt;code&gt;true&lt;/code&gt; replaces the whole element (its &lt;code&gt;outerHTML&lt;/code&gt;), which for the &lt;code&gt;&amp;lt;select&amp;gt;&lt;/code&gt; throws away the htmx listener attached to it. &lt;code&gt;innerHTML&lt;/code&gt; keeps the element and swaps only its children, so the listener survives and the options refresh underneath it.&lt;/p&gt;
&lt;p&gt;Here's the progress bars before and after passing an exercise. The bars update via out-of-band swap from the same response that renders the pass/fail feedback:&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://belderbos.dev/images/pybites-platform-before-pass.png&quot; alt=&quot;PyBites platform showing progress bars before passing an exercise&quot; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&quot;https://belderbos.dev/images/pybites-platform-after-pass.png&quot; alt=&quot;PyBites platform showing updated progress bars after passing an exercise&quot; /&gt;&lt;/p&gt;
&lt;p&gt;You might spot the submissions dropdown in these shots and wonder why it is not OOB-swapped here too, like on the Rust platform above. It is the boundary of the technique: OOB swap is solid when you replace an element's whole inner content (&lt;code&gt;innerHTML:#submissions&lt;/code&gt;), but inserting a single new &lt;code&gt;&amp;lt;option&amp;gt;&lt;/code&gt; means wrapping it in a &lt;code&gt;&amp;lt;template&amp;gt;&lt;/code&gt; so the browser does not strip it, and htmx 2.x handled that case unreliably. So here the one-option insert runs through a small &lt;code&gt;htmx:afterSwap&lt;/code&gt; listener instead. The view writes the new submission into a hidden div, and the listener reads it and prepends the &lt;code&gt;&amp;lt;option&amp;gt;&lt;/code&gt;:&lt;/p&gt;
&lt;pre class=&quot;giallo z-code&quot;&gt;&lt;code&gt;&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable z-other&quot;&gt;document.&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt;addEventListener&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;htmx:afterSwap&amp;quot;&lt;/span&gt;&lt;span&gt;, ()&lt;/span&gt;&lt;span class=&quot;z-storage z-type&quot;&gt; =&amp;gt;&lt;/span&gt;&lt;span&gt; {&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;  const&lt;/span&gt;&lt;span class=&quot;z-variable z-other z-constant&quot;&gt; newSub&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-variable z-other&quot;&gt; document.&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;new-submission&amp;quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-keyword&quot;&gt;  if&lt;/span&gt;&lt;span&gt; (&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt;!&lt;/span&gt;&lt;span class=&quot;z-variable z-other&quot;&gt;newSub)&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; return&lt;/span&gt;&lt;span&gt;;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;  const&lt;/span&gt;&lt;span class=&quot;z-variable z-other z-constant&quot;&gt; select&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-variable z-other&quot;&gt; document.&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt;getElementById&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;submissions&amp;quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-storage z-type&quot;&gt;  const&lt;/span&gt;&lt;span class=&quot;z-variable z-other z-constant&quot;&gt; opt&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-variable z-other&quot;&gt; document.&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt;createElement&lt;/span&gt;&lt;span&gt;(&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-string z-string&quot;&gt;&amp;quot;option&amp;quot;&lt;/span&gt;&lt;span&gt;);&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable z-other&quot;&gt;  opt.value&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-variable z-other&quot;&gt; newSub.dataset.hash;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable z-other&quot;&gt;  opt.textContent&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-variable z-other&quot;&gt; newSub.dataset.label;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable z-other&quot;&gt;  select.&lt;/span&gt;&lt;span class=&quot;z-entity z-name&quot;&gt;insertBefore&lt;/span&gt;&lt;span class=&quot;z-variable z-other&quot;&gt;(opt, select.children[&lt;/span&gt;&lt;span class=&quot;z-constant&quot;&gt;1&lt;/span&gt;&lt;span&gt;]);&lt;/span&gt;&lt;span class=&quot;z-punctuation z-definition z-comment z-comment&quot;&gt; // after the placeholder&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span class=&quot;z-variable z-other&quot;&gt;  select.value&lt;/span&gt;&lt;span class=&quot;z-keyword&quot;&gt; =&lt;/span&gt;&lt;span class=&quot;z-variable z-other&quot;&gt; opt.value;&lt;/span&gt;&lt;/span&gt;
&lt;span class=&quot;giallo-l&quot;&gt;&lt;span&gt;});&lt;/span&gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;One honest cost: this adds a query. After saving, the view re-fetches the submissions to render the partial. The alternative is re-implementing state changes in pure JavaScript, creating behavior in two places. With out-of-band swaps you drive the logic from the view, all in one place. One more query, but less code and a more maintainable solution.&lt;/p&gt;
&lt;p&gt;Whoever fetches the data should render it. Keep the query and the template together.&lt;/p&gt;
&lt;p&gt;For more on hypermedia-driven applications, see this great book: &lt;a rel=&quot;noopener external&quot; target=&quot;_blank&quot; href=&quot;https://hypermedia.systems&quot;&gt;Hypermedia Systems&lt;/a&gt;.&lt;/p&gt;</description>
	<pubDate>Fri, 05 Jun 2026 00:00:00 +0000</pubDate>
</item>

</channel>
</rss>
