This is a valid RSS feed.
This feed is valid, but interoperability with the widest range of feed readers could be improved by implementing the following recommendations.
Children’s Hospital.</p><p>Now I definitely know where it is:</p><div ...
line 510, column 0: (5 occurrences) [help]
them.</p><h1 id="h_how_to_use_ai">How to use AI<a class="headerlink" aria-la ...
<?xml version="1.0" encoding="UTF-8"?>
<?xml-stylesheet type="text/xsl" href="https://nedbatchelder.com/rssfull2html.xslt" media="screen" ?>
<rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns="http://purl.org/rss/1.0/">
<channel rdf:about="https://nedbatchelder.com/blog">
<title>Ned Batchelder's blog</title>
<link>https://nedbatchelder.com/blog</link>
<description>Ned Batchelder's personal blog.</description>
<dc:language>en-US</dc:language>
<image rdf:resource="https://nedbatchelder.com/pix/rss-banner.gif"/>
<items>
<rdf:Seq>
<rdf:li resource="https://nedbatchelder.com/blog/202506/math_factoid_of_the_day_63.html"/><rdf:li resource="https://nedbatchelder.com/blog/202506/digital_equipment_corporation_no_more.html"/><rdf:li resource="https://nedbatchelder.com/blog/202505/pycon_summer_camp.html"/><rdf:li resource="https://nedbatchelder.com/blog/202505/filtering_github_actions_by_changed_files.html"/><rdf:li resource="https://nedbatchelder.com/blog/202504/regex_affordances.html"/><rdf:li resource="https://nedbatchelder.com/blog/202504/find_the_bear.html"/><rdf:li resource="https://nedbatchelder.com/blog/202504/nedflix.html"/><rdf:li resource="https://nedbatchelder.com/blog/202503/human_sorting_improved.html"/><rdf:li resource="https://nedbatchelder.com/blog/202503/horseless_intelligence.html"/><rdf:li resource="https://nedbatchelder.com/blog/202503/faster_branch_coverage_measurement.html"/>
</rdf:Seq>
</items>
</channel>
<image rdf:about="https://nedbatchelder.com/pix/rss-banner.gif">
<title>Ned Batchelder's blog</title>
<link>https://nedbatchelder.com/blog</link>
<url>https://nedbatchelder.com/pix/rss-banner.gif</url>
</image>
<item rdf:about="https://nedbatchelder.com/blog/202506/math_factoid_of_the_day_63.html">
<title>Math factoid of the day: 63</title>
<link>https://nedbatchelder.com/blog/202506/math_factoid_of_the_day_63.html</link>
<dc:date>2025-06-16T00:00:00-04:00</dc:date>
<dc:creator>Ned Batchelder</dc:creator>
<description><![CDATA[<p>63 is a <a rel="external noopener" href="https://en.wikipedia.org/wiki/Centered_octahedral_number">centered octahedral number</a>. That means if you
build an approximation of an octahedron with cubes, one size of octahedron will
have 63 cubes.</p><p>In the late 1700’s <a rel="external noopener" href="https://en.wikipedia.org/wiki/Ren%C3%A9_Just_Ha%C3%BCy">René Just Haüy</a> developed a theory
about how crystals formed: successive layers of fundamental primitives in
orderly arrangements. One of those arrangements was stacking cubes together to
make an octahedron.</p><p>Start with one cube:</p><div class="figurep"><img src="https://nedbatchelder.com/code/diagrams/hauy/0.svg" alt="Just one lonely cube"></div><p>Add six more cubes around it, one on each face. Now we have seven:</p><div class="figurep"><img src="https://nedbatchelder.com/code/diagrams/hauy/1.svg" alt="Seven cubes as a crude octahedron"></div><p>Add another layer, adding a cube to touch each visible cube, making 25:</p><div class="figurep"><img src="https://nedbatchelder.com/code/diagrams/hauy/2.svg" alt="25 cubes arranged like an octahedron five cubes wide"></div><p>One more layer and we have a total of 63:</p><div class="figurep"><img src="https://nedbatchelder.com/code/diagrams/hauy/3.svg" alt="63 cubes arranged like an octahedron seven cubes wide"></div><p>The remaining numbers in <a href="https://oeis.org/A001845" rel="external noopener">the sequence</a>
less than 10,000 are 129, 231, 377, 575, 833, 1159, 1561, 2047, 2625, 3303,
4089, 4991, 6017, 7175, 8473, 9919.</p><p>63 also shows up in the <a rel="external noopener" href="https://en.wikipedia.org/wiki/Delannoy_number">Delannoy numbers</a>: the
number of ways to traverse a grid from the lower left corner to upper right
using only steps north, east, or northeast. Here are the 63 ways of moving on a
3<span class="times">×</span>3 grid:</p><div class="figurep"><img src="https://nedbatchelder.com/code/diagrams/delannoy3.svg" alt="63 different ways to traverse a 3x3 grid"></div><p>(Diagram from <a href="https://en.wikipedia.org/wiki/File:Delannoy3x3.svg" rel="external noopener">Wikipedia</a>)</p><p>In fact, the number of cubes in a Haüy octahedron with N layers is the same
as the number of Delannoy steps on a 3<span class="times">×</span>N grid!</p><p>Since the two ideas are both geometric and fairly simple, I would love to
find a geometric explanation for the correspondence. The octahedron is
three-dimensional, and the Delannoy grids have that tantalizing 3 in them. It
seems like there should be a way to convert Haüy coordinates to Delannoy
coordinates to show how they relate. But I haven’t found one...</p><p class="bulletsep">• • •</p><p>Colophon: I made the octahedron diagrams by asking Claude to write a
<a href="https://nedbatchelder.com/code/diagrams/hauy/hauy_oct.py">Python program</a> to do it.
It wasn’t a fast process because it took pushing and prodding to get the
diagrams to come out the way I liked. But Claude was very competent, and I
could think about the results rather than about projections or color spaces. I
could dip into it for 10 minutes at a time over a number of days without having
to somehow reconstruct a mental context.</p><p>This kind of casual hobby programming is perfect for AI assistance. I don’t
need the code to be perfect or even good, I just want the diagrams to be nice.
I don’t have the focus time to learn how to write the program, so I can leave it
to an imperfect assistant.</p>
]]></description>
</item>
<item rdf:about="https://nedbatchelder.com/blog/202506/digital_equipment_corporation_no_more.html">
<title>Digital Equipment Corporation no more</title>
<link>https://nedbatchelder.com/blog/202506/digital_equipment_corporation_no_more.html</link>
<dc:date>2025-06-09T09:43:53-04:00</dc:date>
<dc:creator>Ned Batchelder</dc:creator>
<description><![CDATA[<p>Today is the 39-year anniversary of my first day working for
<a rel="external noopener" href="https://en.wikipedia.org/wiki/Digital_Equipment_Corporation">Digital Equipment Corporation</a>. It was my first real job in
the tech world, two years out of college. <a href="https://nedbatchelder.com/blog/200606/digital_equipment_corporation.html">I wrote
about it 19 years ago</a>, but it’s on my mind again.</p><p>More and more, I find that people have never heard of Digital (as we called
it) or DEC (as they preferred we didn’t call it but everyone did). It’s
something I’ve had to get used to. I try to relate a story from that time, and
I find that even experienced engineers with deep knowledge of technologies don’t
know of the company.</p><p>I mention this not in a crabby “kids these days” kind of way. It does
surprise me, but I’m taking it as a learning opportunity. If there’s a lesson
to learn, it is this:</p><blockquote><div><p>This too shall pass.</p></div></blockquote><p>I am now working for Netflix, and one of the great things about it is that
everyone has heard of Netflix. I can mention my job to anyone and they are
impressed in some way. Techies know it as one of the FAANG companies, and
“civilians” know it for the entertainment it produces and delivers.</p><p>When I joined Digital in 1986, at least among tech people, it was similar.
Everyone knew about Digital and what they had done: the creation of the
minicomputer, the genesis of Unix and C, the ubiquitous VT100. Many foundations
of the software world flowed directly and famously from Digital.</p><p>These days Digital isn’t quite yet a footnote to history, but it is more and
more unknown even among the most tech-involved. And the tech world carries
on!</p><p>My small team at Netflix has a number of young engineers, less than two years
out of college, and even an intern still in college. I’m sure they felt
incredibly excited to join a company as well-known and influential as Netflix.
In 39 years when they tell a story from the early days of their career will they
start with, “Have you heard of Netflix?” and have to adjust to the blank stares
they get in return?</p><p>This too shall pass.</p>
]]></description>
</item>
<item rdf:about="https://nedbatchelder.com/blog/202505/pycon_summer_camp.html">
<title>PyCon summer camp</title>
<link>https://nedbatchelder.com/blog/202505/pycon_summer_camp.html</link>
<dc:date>2025-05-15T07:05:20-04:00</dc:date>
<dc:creator>Ned Batchelder</dc:creator>
<description><![CDATA[<p>I’m headed to PyCon today, and I’m reminded about how it feels like summer
camp, in mostly good ways, but also in a tricky way.</p><p>You take some time off from your “real” life, you go somewhere else, you hang
out with old friends and meet some new friends. You do different things than in
your real life, some are playful, some take real work. These are all good ways
it’s like summer camp.</p><p>Here’s the tricky thing to watch out for: like summer camp, you can make
connections to people or projects that are intense and feel like they could last
forever. You make friends at summer camp, or even have semi-romantic crushes on
people. You promise to stay in touch, you think it’s the “real thing.” When you
get home, you write an email or two, maybe a phone call, but it fades away. The
excitement of the summer is overtaken by your autumnal real life again.</p><p>PyCon can be the same way, either with people or projects. Not a romance,
but the exciting feeling that you want to keep doing the project you started at
PyCon, or be a member of some community you hung out with for those days. You
want to keep talking about that exciting thing with that person. These are
great feelings, but it’s easy to emotionally over-commit to those efforts and
then have it fade away once PyCon is over.</p><p>How do you know what projects are just crushes, and which are permanent
relationships? Maybe it doesn’t matter, and we should just get excited about
things.</p><p>I know I started at least one effort last year that I thought would be done
in a few months, but has since stalled. Now I am headed back to PyCon. Will I
become attached to yet more things this time? Is that bad? Should I temper my
enthusiasm, or is it fine to light a few fires and accept that some will peter
out?</p>
]]></description>
</item>
<item rdf:about="https://nedbatchelder.com/blog/202505/filtering_github_actions_by_changed_files.html">
<title>Filtering GitHub actions by changed files</title>
<link>https://nedbatchelder.com/blog/202505/filtering_github_actions_by_changed_files.html</link>
<dc:date>2025-05-04T07:00:06-04:00</dc:date>
<dc:creator>Ned Batchelder</dc:creator>
<description><![CDATA[<p>Coverage.py has a large test suite that runs in many environments, which can
take a while. But some changes don’t require running the test suite at all.
I’ve changed the actions to detect when they need to run based on what files
have changed, but there were some twists and turns along the way.</p><p>The <a rel="external noopener" href="https://github.com/dorny/paths-filter">dorny/paths-filter</a> action can check which files
have changed for pull requests or branches. I <a rel="external noopener" href="https://github.com/nedbat/coveragepy/commit/62891c81d2c7f175ff14f0d492b5695f507780e8#diff-2dcad576de77b3d5156ec1976a4f88236339c48a6000beda4885b0fe8e3ba006">added it to
my tests action</a> like this:</p><blockquote class="code"><pre class="yaml"><div class="source"><span class="nt">jobs</span><span class="p">:</span>
<br>
<br><span class="w">  </span><span class="nt">changed</span><span class="p">:</span>
<br><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s">"Check</span><span class="nv"> </span><span class="s">what</span><span class="nv"> </span><span class="s">files</span><span class="nv"> </span><span class="s">changed"</span>
<br><span class="w">    </span><span class="nt">outputs</span><span class="p">:</span>
<br><span class="w">      </span><span class="nt">python</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">${{ steps.filter.outputs.python }}</span>
<br><span class="w">    </span><span class="nt">steps</span><span class="p">:</span>
<br><span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s">"Check</span><span class="nv"> </span><span class="s">out</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">repo"</span>
<br><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">actions/checkout</span>
<br>
<br><span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s">"Examine</span><span class="nv"> </span><span class="s">changed</span><span class="nv"> </span><span class="s">files"</span>
<br><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">dorny/paths-filter</span>
<br><span class="w">        </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">filter</span>
<br><span class="w">        </span><span class="nt">with</span><span class="p">:</span>
<br><span class="w">          </span><span class="nt">filters</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">|</span>
<br><span class="w">            </span><span class="no">python:</span>
<br><span class="w">              </span><span class="no">- "**.py"</span>
<br>
<br><span class="w">  </span><span class="nt">tests</span><span class="p">:</span>
<br><span class="w">    </span><span class="c1"># Don't run tests if the branch name includes "-notests".</span>
<br><span class="w">    </span><span class="c1"># Only run tests if Python files changed.</span>
<br><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">changed</span>
<br><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">${{ !contains(github.ref, '-notests') && needs.changed.outputs.python == 'true' }}</span>
<br></div>
</pre></blockquote><p>The “changed” jobs checks what files have changed, then the “tests” job
examines its output to decide whether to run at all.</p><p>It’s a little awkward having an output for the “changed” job as an
intermediary, but this did what I wanted: if any .py file changed, run the
tests, otherwise don’t run them. I left in an old condition: if the branch name
includes “-notests”, then don’t run the tests.</p><p>This worked, but I realized I needed to run the tests on other conditions
also. What if no Python file changed, but the GitHub action file itself had
changed? So I <a rel="external noopener" href="https://github.com/nedbat/coveragepy/commit/9cb9bdc6847115c1a357ec1cb95c697e49a434ae#diff-2dcad576de77b3d5156ec1976a4f88236339c48a6000beda4885b0fe8e3ba006">added that as a condition</a>. The
if-expression was getting long, so I made it a multi-line string:</p><blockquote class="code"><pre class="yaml"><div class="source"><span class="nt">jobs</span><span class="p">:</span>
<br>
<br><span class="w">  </span><span class="nt">changed</span><span class="p">:</span>
<br><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s">"Check</span><span class="nv"> </span><span class="s">what</span><span class="nv"> </span><span class="s">files</span><span class="nv"> </span><span class="s">changed"</span>
<br><span class="w">    </span><span class="nt">outputs</span><span class="p">:</span>
<br><span class="w">      </span><span class="nt">python</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">${{ steps.filter.outputs.python }}</span>
<br><span class="w">      </span><span class="nt">workflow</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">${{ steps.filter.outputs.workflow }}</span>
<br><span class="w">    </span><span class="nt">steps</span><span class="p">:</span>
<br><span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s">"Check</span><span class="nv"> </span><span class="s">out</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">repo"</span>
<br><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">actions/checkout</span>
<br>
<br><span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s">"Examine</span><span class="nv"> </span><span class="s">changed</span><span class="nv"> </span><span class="s">files"</span>
<br><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">dorny/paths-filter</span>
<br><span class="w">        </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">filter</span>
<br><span class="w">        </span><span class="nt">with</span><span class="p">:</span>
<br><span class="w">          </span><span class="nt">filters</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">|</span>
<br><span class="w">            </span><span class="no">python:</span>
<br><span class="w">              </span><span class="no">- "**.py"</span>
<br><span class="w">            </span><span class="no">workflow:</span>
<br><span class="w">              </span><span class="no">- ".github/workflows/testsuite.yml"</span>
<br>
<br><span class="w">  </span><span class="nt">tests</span><span class="p">:</span>
<br><span class="w">    </span><span class="c1"># Don't run tests if the branch name includes "-notests".</span>
<br><span class="w">    </span><span class="c1"># Only run tests if Python files or this workflow changed.</span>
<br><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">changed</span>
<br><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">|</span>
<br><span class="w">      </span><span class="no">${{</span>
<br><span class="w">        </span><span class="no">!contains(github.ref, '-notests')</span>
<br><span class="w">        </span><span class="no">&& (</span>
<br><span class="w">          </span><span class="no">needs.changed.outputs.python == 'true'</span>
<br><span class="w">          </span><span class="no">|| needs.changed.outputs.workflow == 'true'</span>
<br><span class="w">        </span><span class="no">)</span>
<br><span class="w">      </span><span class="no">}}</span>
<br></div>
</pre></blockquote><p>This seemed to work, but it has a bug that I will get to in a bit.</p><p>Thinking about it more, I realized there are other files that could affect
the test results: requirements files, test output files, and the tox.ini.
Rather than add them as three more conditions, I <a rel="external noopener" href="https://github.com/nedbat/coveragepy/commit/18f2240406d18417eac3497df6b2d184c719a7cc#diff-2dcad576de77b3d5156ec1976a4f88236339c48a6000beda4885b0fe8e3ba006">combined
them all into one</a>:</p><blockquote class="code"><pre class="yaml"><div class="source"><span class="nt">jobs</span><span class="p">:</span>
<br>
<br><span class="w">  </span><span class="nt">changed</span><span class="p">:</span>
<br><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s">"Check</span><span class="nv"> </span><span class="s">what</span><span class="nv"> </span><span class="s">files</span><span class="nv"> </span><span class="s">changed"</span>
<br><span class="w">    </span><span class="nt">outputs</span><span class="p">:</span>
<br><span class="w">      </span><span class="nt">run_tests</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">${{ steps.filter.outputs.run_tests }}</span>
<br><span class="w">    </span><span class="nt">steps</span><span class="p">:</span>
<br><span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s">"Check</span><span class="nv"> </span><span class="s">out</span><span class="nv"> </span><span class="s">the</span><span class="nv"> </span><span class="s">repo"</span>
<br><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">actions/checkout</span>
<br>
<br><span class="w">      </span><span class="p p-Indicator">-</span><span class="w"> </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s">"Examine</span><span class="nv"> </span><span class="s">changed</span><span class="nv"> </span><span class="s">files"</span>
<br><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">dorny/paths-filter</span>
<br><span class="w">        </span><span class="nt">id</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">filter</span>
<br><span class="w">        </span><span class="nt">with</span><span class="p">:</span>
<br><span class="w">          </span><span class="nt">filters</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">|</span>
<br><span class="w">            </span><span class="no">run_tests:</span>
<br><span class="w">              </span><span class="no">- "**.py"</span>
<br><span class="w">              </span><span class="no">- ".github/workflows/testsuite.yml"</span>
<br><span class="w">              </span><span class="no">- "tox.ini"</span>
<br><span class="w">              </span><span class="no">- "requirements/*.pip"</span>
<br><span class="w">              </span><span class="no">- "tests/gold/**"</span>
<br>
<br><span class="w">  </span><span class="nt">tests</span><span class="p">:</span>
<br><span class="w">    </span><span class="c1"># Don't run tests if the branch name includes "-notests".</span>
<br><span class="w">    </span><span class="c1"># Only run tests if files that affect tests have changed.</span>
<br><span class="w">    </span><span class="nt">needs</span><span class="p">:</span><span class="w"> </span><span class="l l-Scalar l-Scalar-Plain">changed</span>
<br><span class="w">    </span><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">|</span>
<br><span class="w">      </span><span class="no">${{</span>
<br><span class="w">        </span><span class="no">needs.changed.outputs.run_tests == 'true'</span>
<br><span class="w">        </span><span class="no">&& !contains(github.ref, '-notests')</span>
<br><span class="w">      </span><span class="no">}}</span>
<br></div>
</pre></blockquote><p>BTW: these commits also update the quality checks workflow which has other
kinds of mix-and-match conditions to deal with that you might be interested
in.</p><p>All seemed good! Then I made a commit that only changed my Makefile, and the
tests ran! Why!? The Makefile isn’t one of the checked files. The paths-filter
action helpfully includes debug output that showed that only the Makefile was
considered changed, and that the “run_test” output was false.</p><p>I took a guess that GitHub actions don’t like expressions with newlines in
them. Using the trusty <a href="https://yaml-multiline.info/" rel="external noopener">YAML multi-line
string cheat sheet</a>, I tried changing from the literal block style (with a
pipe) to the folded style (with a greater-than):</p><blockquote class="code"><pre class="yaml"><div class="source"><span class="nt">if</span><span class="p">:</span><span class="w"> </span><span class="p p-Indicator">></span>
<br><span class="w">  </span><span class="no">${{</span>
<br><span class="w">    </span><span class="no">needs.changed.outputs.run_tests == 'true'</span>
<br><span class="w">    </span><span class="no">&& !contains(github.ref, '-notests')</span>
<br><span class="w">  </span><span class="no">}}</span>
<br></div>
</pre></blockquote><p>The literal form includes all newlines, the folded style turns newlines into
spaces. To check that I had it right, I tried parsing the YAML files: to my
surprise, both forms included all the newlines, there was no difference at all.
It turns out that YAML “helpfully” notices changes in indentation, and includes
newlines for indented lines. My expression is nicely indented, so it has
newlines no matter what syntax I use.</p><p>The <a rel="external noopener" href="https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/evaluate-expressions-in-workflows-and-actions">GitHub actions docs</a> don’t mention it, but it
seems that newlines do break expression evaluation. Sigh. My expressions are
not as long now as they had gotten during this exploration, so I
<a rel="external noopener" href="https://github.com/nedbat/coveragepy/commit/902ef7a111a0fb5b48828f4502b01db1e9e002aa#diff-2dcad576de77b3d5156ec1976a4f88236339c48a6000beda4885b0fe8e3ba006">changed them all back to one line</a>, and now it all works
as I wanted.</p><p>There are some other things I’d like to tweak: when the tests are skipped,
the final status is “success”, but I’m wondering if there’s a way to make it
“skipped”. I’m also torn about whether every change to master should run all
the workflows or if they should also filter based on the changed files.
Currently they are filtered.</p><p>Continuous integration and GitHub workflows are great, but they always seem
to involve this kind of fiddling in environments that are difficult to debug.
Maybe I’ve saved you some grief.</p>
]]></description>
</item>
<item rdf:about="https://nedbatchelder.com/blog/202504/regex_affordances.html">
<title>Regex affordances</title>
<link>https://nedbatchelder.com/blog/202504/regex_affordances.html</link>
<dc:date>2025-04-18T11:19:46-04:00</dc:date>
<dc:creator>Ned Batchelder</dc:creator>
<description><![CDATA[<p>Python regexes have a number of features that bring new power to text
manipulation. I’m not talking about fancy matching features like negative
look-behinds, but ways you can construct and use regexes. As a demonstration,
I’ll show you some real code from a real project.</p><p>Coverage.py will expand environment variables in values read from its
configuration files. It does this with a function called
<code>substitute_variables</code>:</p><blockquote class="code"><pre class="python"><div class="source"><span class="k">def</span><span class="w"> </span><span class="nf">substitute_variables</span><span class="p">(</span>
<br>    <span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span>
<br>    <span class="n">variables</span><span class="p">:</span> <span class="nb">dict</span><span class="p">[</span><span class="nb">str</span><span class="p">,</span> <span class="nb">str</span><span class="p">],</span>
<br><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
<br><span class="w">    </span><span class="sd">"""</span>
<br><span class="sd">    Substitute ``${VAR}`` variables in `text`.</span>
<br>
<br><span class="sd">    Variables in the text can take a number of</span>
<br><span class="sd">    shell-inspired forms::</span>
<br>
<br><span class="sd">        $VAR</span>
<br><span class="sd">        ${VAR}</span>
<br><span class="sd">        ${VAR?}         strict: an error if no VAR.</span>
<br><span class="sd">        ${VAR-miss}     defaulted: "miss" if no VAR.</span>
<br><span class="sd">        $$              just a dollar sign.</span>
<br>
<br><span class="sd">    `variables` is a dictionary of variable values.</span>
<br>
<br><span class="sd">    Returns the resulting text with values substituted.</span>
<br>
<br><span class="sd">    """</span>
<br></div>
</pre></blockquote><p>Call it with a string and a dictionary, and it makes the substitutions:</p><blockquote class="code"><pre class="python"><div class="source"><span class="o">>>></span> <span class="n">substitute_variables</span><span class="p">(</span>
<br><span class="o">...</span>     <span class="n">text</span><span class="o">=</span><span class="s2">"Look: $FOO ${BAR-default} $$"</span><span class="p">,</span>
<br><span class="o">...</span>     <span class="n">variables</span><span class="o">=</span><span class="p">{</span><span class="s1">'FOO'</span><span class="p">:</span> <span class="s1">'Xyzzy'</span><span class="p">},</span>
<br><span class="o">...</span> <span class="p">)</span>
<br>
<br><span class="s1">'Look: Xyzzy default $'</span>
<br></div>
</pre></blockquote><p>We use a regex to pick apart the text:</p><blockquote class="code"><pre class="python"><div class="source"><span class="n">dollar_pattern</span> <span class="o">=</span> <span class="sa">r</span><span class="s2">"""(?x)   # Verbose regex syntax</span>
<br><span class="s2">    \$                      # A dollar sign,</span>
<br><span class="s2">    (?:                     # then</span>
<br><span class="s2">        (?P<dollar> \$ ) |      # a dollar sign, or</span>
<br><span class="s2">        (?P<word1> \w+ ) |      # a plain word, or</span>
<br><span class="s2">        \{                      # a {-wrapped</span>
<br><span class="s2">            (?P<word2> \w+ )        # word,</span>
<br><span class="s2">            (?:                         # either</span>
<br><span class="s2">                (?P<strict> \? ) |      # strict or</span>
<br><span class="s2">                -(?P<defval> [^}]* )    # defaulted</span>
<br><span class="s2">            )?                      # maybe</span>
<br><span class="s2">        }</span>
<br><span class="s2">    )</span>
<br><span class="s2">    """</span>
<br></div>
</pre></blockquote><p>This isn’t a super-fancy regex: it doesn’t use advanced pattern matching.
But there are some useful regex features at work here:</p><ul>
<li>The <code>(?x)</code> flag at the beginning turns on “verbose” regex syntax. In
this mode, all white space is ignored so the regex can be multi-line and we can
indent to help see the structure, and comments are allowed at the ends of
lines.</li>
<li>Named groups like <code>(?P<word1> … )</code> are used to capture parts of
the text that we can retrieve later by name.</li>
<li>There are also two groups used to get the precedence of operators right, but
we don’t want to capture those values separately, so I use the non-capturing
group syntax for them: <code>(?: … )</code>. In this code, we only ever access groups
by name, so I could have left them as regular capturing groups, but I think it’s
clearer to indicate up-front that we won’t be using them.</li>
</ul><p>The verbose syntax in particular makes it easier to understand the regex.
Compare to what it would look like in one line:</p><blockquote class="code"><pre class="python"><div class="source"><span class="sa">r</span><span class="s2">"\$(?:(?P<dollar>\$)|(?P<word1>\w+)|\{(?P<word2>\w+)(?:(?P<strict>\?)|-(?P<defval>[^}]*))?})"</span>
<br></div>
</pre></blockquote><p>Once we have the regex, we can use <code>re.sub()</code> to replace the variables
with their values:</p><blockquote class="code"><pre class="python"><div class="source"><span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="n">dollar_pattern</span><span class="p">,</span> <span class="n">dollar_replace</span><span class="p">,</span> <span class="n">text</span><span class="p">)</span>
<br></div>
</pre></blockquote><p>But we’re going to use another power feature of Python regexes:
<code>dollar_replace</code> here isn’t a string, it’s a function! Each fragment the
regex matches will be passed as a match object to our <code>dollar_replace</code>
function. It returns a string which re.sub() uses as the replacement in the
text:</p><blockquote class="code"><pre class="python"><div class="source"><span class="k">def</span><span class="w"> </span><span class="nf">dollar_replace</span><span class="p">(</span><span class="n">match</span><span class="p">:</span> <span class="n">re</span><span class="o">.</span><span class="n">Match</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-></span> <span class="nb">str</span><span class="p">:</span>
<br><span class="w">    </span><span class="sd">"""Called for each $replacement."""</span>
<br>    <span class="c1"># Get the one group that matched.</span>
<br>    <span class="n">groups</span> <span class="o">=</span> <span class="n">match</span><span class="o">.</span><span class="n">group</span><span class="p">(</span><span class="s1">'dollar'</span><span class="p">,</span> <span class="s1">'word1'</span><span class="p">,</span> <span class="s1">'word2'</span><span class="p">)</span>
<br>    <span class="n">word</span> <span class="o">=</span> <span class="nb">next</span><span class="p">(</span><span class="n">g</span> <span class="k">for</span> <span class="n">g</span> <span class="ow">in</span> <span class="n">groups</span> <span class="k">if</span> <span class="n">g</span><span class="p">)</span>
<br>
<br>    <span class="k">if</span> <span class="n">word</span> <span class="o">==</span> <span class="s2">"$"</span><span class="p">:</span>
<br>        <span class="k">return</span> <span class="s2">"$"</span>
<br>    <span class="k">elif</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">variables</span><span class="p">:</span>
<br>        <span class="k">return</span> <span class="n">variables</span><span class="p">[</span><span class="n">word</span><span class="p">]</span>
<br>    <span class="k">elif</span> <span class="n">match</span><span class="p">[</span><span class="s2">"strict"</span><span class="p">]:</span>
<br>        <span class="n">msg</span> <span class="o">=</span> <span class="sa">f</span><span class="s2">"Variable </span><span class="si">{</span><span class="n">word</span><span class="si">}</span><span class="s2"> is undefined: </span><span class="si">{</span><span class="n">text</span><span class="si">!r}</span><span class="s2">"</span>
<br>        <span class="k">raise</span> <span class="ne">NameError</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>
<br>    <span class="k">else</span><span class="p">:</span>
<br>        <span class="k">return</span> <span class="n">match</span><span class="p">[</span><span class="s2">"defval"</span><span class="p">]</span>
<br></div>
</pre></blockquote><p>First we use <code>match.group()</code>. Called with a number of names, it returns
a tuple of what those named groups matched. They could be the matched text, or
None if the group didn’t match anything.</p><p>The way our regex is written only one of those three groups will match, so
the tuple will have one string and two None’s. To get the matched string, we
use <code>next()</code> to find it. If the built-in <code>any()</code> returned the first
true thing it found this code could be simpler, but it doesn’t so we have to do
it this way.</p><p>Now we can check the value to decide on the replacement:</p><ul>
<li>If the match was a dollar sign, we return a dollar sign.</li>
<li>If the word is one of our defined variables, we return the value of the
variable.</li>
<li>Since the word isn’t a defined variable, we check if the “strict” marker was
found, and if so, raise an exception.</li>
<li>Otherwise we return the default value provided.</li>
</ul><p>The final piece of the implementation is to use <code>re.sub()</code> and return
the result:</p><blockquote class="code"><pre class="python"><div class="source"><span class="k">return</span> <span class="n">re</span><span class="o">.</span><span class="n">sub</span><span class="p">(</span><span class="n">dollar_pattern</span><span class="p">,</span> <span class="n">dollar_replace</span><span class="p">,</span> <span class="n">text</span><span class="p">)</span>
<br></div>
</pre></blockquote><p>Regexes are often criticized for being too opaque and esoteric. But done
right, they can be very powerful and don’t have to be a burden. What we’ve done
here is used simple pattern matching paired with useful API features to
compactly write a useful transformation.</p><p>BTW, if you are interested, the <a rel="external noopener" href="https://github.com/nedbat/coveragepy/blob/master/coverage/misc.py#L228-L276">real code is in
coverage.py</a>.</p>
]]></description>
</item>
<item rdf:about="https://nedbatchelder.com/blog/202504/find_the_bear.html">
<title>Find the bear</title>
<link>https://nedbatchelder.com/blog/202504/find_the_bear.html</link>
<dc:date>2025-04-06T13:46:19-04:00</dc:date>
<dc:creator>Ned Batchelder</dc:creator>
<description><![CDATA[<p>A parenting story from almost 30 years ago.</p><p>My wife told me about something her dad did when she was young: in the car,
knowing they were approaching an exit on the highway, he’d say to himself, but
loud enough for his daughters in the back to hear, “If only I could find exit
10...” The girls would look out the window and soon spot the very sign he
needed! “There it is Dad, we found it!” I liked it, it was clever and
sweet.</p><p>When my son <a rel="external noopener" href="https://www.imdb.com/name/nm6026174/">Max</a> was six or so, we were headed into
Boston to visit the big FAO Schwarz toy store that used to be on Boylston St.
They had a large bronze statue of a teddy bear on the corner in front of the
store. It must have been 10 or 12 feet tall. I wanted to try my father-in-law’s
technique with it.</p><p>Max had always been observant, competent and confident. The kind of kid who
could quickly tell you if a piece was missing from a Lego kit. I figured he’d
be the perfect target for this.</p><p>We got off the T (the subway if you aren’t from Boston), and had to walk a
bit. When we were a half block from the store, I could clearly see the bear
up ahead. I said, “If only I could find that bear statue...”</p><p>Max responded, “Oh Dad, I knew you didn’t know where it was!”</p><p class="bulletsep">• • •</p><p>The store closed in 2004 and the bear was removed. I thought it was gone for
good. But on a walk a few weeks ago, I happened upon it outside the Tufts
Children’s Hospital.</p><p>Now I definitely know where it is:</p><div class="figurep"><figure><picture><source type="image/webp" srcset="https://nedbatchelder.com/iv/webp/pix/fao-schwarz-bear.jpg.webp"><img src="https://nedbatchelder.com/pix/fao-schwarz-bear.jpg" alt="The bronze bear statue" width="550" height="760"></picture></figure></div>
]]></description>
</item>
<item rdf:about="https://nedbatchelder.com/blog/202504/nedflix.html">
<title>Nedflix</title>
<link>https://nedbatchelder.com/blog/202504/nedflix.html</link>
<dc:date>2025-04-03T18:19:39-04:00</dc:date>
<dc:creator>Ned Batchelder</dc:creator>
<description><![CDATA[<p>Well, Anthropic and I were not a good fit, though
<a href="https://nedbatchelder.com/blog/202407/anthropic.html">as predicted</a> it was an experience. I’ve
started a new job on the Python language team at Netflix. It feels like a much
better match in a number of ways.</p>
]]></description>
</item>
<item rdf:about="https://nedbatchelder.com/blog/202503/human_sorting_improved.html">
<title>Human sorting improved</title>
<link>https://nedbatchelder.com/blog/202503/human_sorting_improved.html</link>
<dc:date>2025-03-29T12:59:35-04:00</dc:date>
<dc:creator>Ned Batchelder</dc:creator>
<description><![CDATA[<p>When sorting strings, you’d often like the order to make sense to a person.
That means numbers need to be treated numerically even if they are in a larger
string.</p><p>For example, sorting Python versions with the default sort() would give
you:</p><blockquote class="code"><pre>Python 3.10<br>Python 3.11<br>Python 3.9<br></pre></blockquote><p>when you want it to be:</p><blockquote class="code"><pre>Python 3.9<br>Python 3.10<br>Python 3.11<br></pre></blockquote><p>I wrote about this long ago (<a href="https://nedbatchelder.com/blog/200712/human_sorting.html">Human sorting</a>), but have
continued to tweak the code and needed to <a rel="external noopener" href="https://github.com/nedbat/watchgha/commit/cfcd48ac3f24f5b76aa02caa695af13e37f38bcf">add it to a
project</a> recently. Here’s the latest:</p><blockquote class="code"><pre class="python"><div class="source"><span class="kn">import</span><span class="w"> </span><span class="nn">re</span>
<br>
<br><span class="k">def</span><span class="w"> </span><span class="nf">human_key</span><span class="p">(</span><span class="n">s</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="nb">tuple</span><span class="p">[</span><span class="nb">list</span><span class="p">[</span><span class="nb">str</span> <span class="o">|</span> <span class="nb">int</span><span class="p">],</span> <span class="nb">str</span><span class="p">]:</span>
<br><span class="w">    </span><span class="sd">"""Turn a string into a sortable value that works how humans expect.</span>
<br>
<br><span class="sd">    "z23A" -> (["z", 23, "a"], "z23A")</span>
<br>
<br><span class="sd">    The original string is appended as a last value to ensure the</span>
<br><span class="sd">    key is unique enough so that "x1y" and "x001y" can be distinguished.</span>
<br>
<br><span class="sd">    """</span>
<br>    <span class="k">def</span><span class="w"> </span><span class="nf">try_int</span><span class="p">(</span><span class="n">s</span><span class="p">:</span> <span class="nb">str</span><span class="p">)</span> <span class="o">-></span> <span class="nb">str</span> <span class="o">|</span> <span class="nb">int</span><span class="p">:</span>
<br><span class="w">        </span><span class="sd">"""If `s` is a number, return an int, else `s` unchanged."""</span>
<br>        <span class="k">try</span><span class="p">:</span>
<br>            <span class="k">return</span> <span class="nb">int</span><span class="p">(</span><span class="n">s</span><span class="p">)</span>
<br>        <span class="k">except</span> <span class="ne">ValueError</span><span class="p">:</span>
<br>            <span class="k">return</span> <span class="n">s</span>
<br>
<br>    <span class="k">return</span> <span class="p">([</span><span class="n">try_int</span><span class="p">(</span><span class="n">c</span><span class="p">)</span> <span class="k">for</span> <span class="n">c</span> <span class="ow">in</span> <span class="n">re</span><span class="o">.</span><span class="n">split</span><span class="p">(</span><span class="sa">r</span><span class="s2">"(\d+)"</span><span class="p">,</span> <span class="n">s</span><span class="o">.</span><span class="n">casefold</span><span class="p">())],</span> <span class="n">s</span><span class="p">)</span>
<br>
<br><span class="k">def</span><span class="w"> </span><span class="nf">human_sort</span><span class="p">(</span><span class="n">strings</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">])</span> <span class="o">-></span> <span class="kc">None</span><span class="p">:</span>
<br><span class="w">    </span><span class="sd">"""Sort a list of strings how humans expect."""</span>
<br>    <span class="n">strings</span><span class="o">.</span><span class="n">sort</span><span class="p">(</span><span class="n">key</span><span class="o">=</span><span class="n">human_key</span><span class="p">)</span>
<br></div>
</pre></blockquote><p>The central idea here is to turn a string like <code>"Python 3.9"</code> into the
key <code>["Python ", 3, ".", 9]</code> so that numeric components will be sorted by
their numeric value. The re.split() function gives us interleaved words and
numbers, and try_int() turns the numbers into actual numbers, giving us sortable
key lists.</p><p>There are two improvements from the original:</p><ul>
<li>The sort is made case-insensitive by using casefold() to lower-case the
string.</li>
<li>The key returned is now a two-element tuple: the first element is the list
of intermixed strings and integers that gives us the ordering we want. The
second element is the original string unchanged to ensure that unique strings
will always result in distinct keys. Without it, <code>"x1y"</code> and
<code>"x001Y"</code> would both produce the same key. This solves a
<a rel="external noopener" href="https://github.com/nedbat/coveragepy/issues/1709">problem that actually happened</a> when sorting the items of
a dictionary.
<blockquote class="code"><pre class="python"><div class="source"><span class="c1"># Without the tuple: different strings, same key!!</span>
<br><span class="n">human_key</span><span class="p">(</span><span class="s2">"x1y"</span><span class="p">)</span> <span class="o">-></span> <span class="p">[</span><span class="s2">"x"</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">"y"</span><span class="p">]</span>
<br><span class="n">human_key</span><span class="p">(</span><span class="s2">"x001Y"</span><span class="p">)</span> <span class="o">-></span> <span class="p">[</span><span class="s2">"x"</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">"y"</span><span class="p">]</span>
<br>
<br><span class="c1"># With the tuple: different strings, different keys.</span>
<br><span class="n">human_key</span><span class="p">(</span><span class="s2">"x1y"</span><span class="p">)</span> <span class="o">-></span> <span class="p">([</span><span class="s2">"x"</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">"y"</span><span class="p">],</span> <span class="s2">"x1y"</span><span class="p">)</span>
<br><span class="n">human_key</span><span class="p">(</span><span class="s2">"x001Y"</span><span class="p">)</span> <span class="o">-></span> <span class="p">([</span><span class="s2">"x"</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="s2">"y"</span><span class="p">],</span> <span class="s2">"x001Y"</span><span class="p">)</span>
<br></div>
</pre></blockquote>
</li>
</ul><p>If you are interested, there are many different ways to split the string into
the word/number mix. The <a rel="external noopener" href="https://nedbatchelder.com/blog/200712/human_sorting.html#comments">comments on the old post</a>
have many alternatives, and there are certainly more.</p><p>This still makes some assumptions about what is wanted, and doesn’t cover all
possible options (floats? negative/positive? full file paths?). For those, you
probably want the full-featured <a rel="external noopener" href="https://pypi.org/project/natsort/">natsort</a> (natural sort)
package.</p>
]]></description>
</item>
<item rdf:about="https://nedbatchelder.com/blog/202503/horseless_intelligence.html">
<title>Horseless intelligence</title>
<link>https://nedbatchelder.com/blog/202503/horseless_intelligence.html</link>
<dc:date>2025-03-17T13:56:03-04:00</dc:date>
<dc:creator>Ned Batchelder</dc:creator>
<description><![CDATA[<p>AI is everywhere these days, and everyone has opinions and thoughts. These
are some of mine.</p><p>Full disclosure: for a time I worked for Anthropic, the makers of
<a href="https://claude.ai" rel="external noopener">Claude.ai</a>. I no longer do, and nothing in
this post (or elsewhere on this site) is their opinion or is proprietary to
them.</p><h1 id="h_how_to_use_ai">How to use AI<a class="headerlink" aria-label="Link to this header" href="#h_how_to_use_ai"></a></h1><p>My advice about using AI is simple: use AI as an assistant, not an expert,
and use it judiciously. Some people will object, “but AI can be wrong!” Yes,
and so can the internet in general, but no one now recommends avoiding online
resources because they can be wrong. They recommend taking it all with a grain
of salt and being careful. That’s what you should do with AI help as well.</p><p>We are all learning how to use AI well. Prompt engineering is a new
discipline. It surprises me that large language models (LLMs) give better
answers if you include phrases like “think step-by-step” or “check your answer
before you reply” in your prompt, but they do improve the result. LLMs are not
search engines, but like search engines, you have to approach them as unique
tools that will do better if you know how to ask the right questions.</p><p>If you approach AI thinking that it will hallucinate and be wrong, and then
discard it as soon as it does, you are falling victim to
<a rel="external noopener" href="https://en.wikipedia.org/wiki/Confirmation_bias">confirmation bias</a>. Yes, AI will be wrong sometimes. That
doesn’t mean it is useless. It means you have to use it carefully.</p><p>I’ve used AI to help me write code when I didn’t know how to get started
because it needed more research than I could afford at the moment. The AI
didn’t produce finished code, but it got me going in the right direction, and
iterating with it got me to working code.</p><p>One thing it seemed to do well was to write more tests given a few examples
to start from. Your workflow probably has steps where AI can help you. It’s
not a magic bullet, it’s a tool that you have to learn how to use.</p><h1 id="h_the_future_of_coding">The future of coding<a class="headerlink" aria-label="Link to this header" href="#h_the_future_of_coding"></a></h1><p>In beginner-coding spaces like <a rel="external noopener" href="https://discord.gg/python">Python Discord</a>,
anxious learners ask if there is any point in learning to code, since won’t AI
take all the jobs soon anyway?</p><p><a rel="external noopener" href="https://simonwillison.net/">Simon Willison</a> seems to be our best guide to the
head-spinning pace of AI development these days (if you can keep up with the
head-spinning pace of his blog!) I like what he <a rel="external noopener" href="https://news.ycombinator.com/item?id=43166157">said
recently</a> about how AI will affect new programmers:</p><blockquote><div>
<p>There has never been a better time to learn to code — the
learning curve is being shaved down by these new LLM-based tools, and the
amount of value people with programming literacy can produce is going up by
an order of magnitude.</p>
<p>People who know both coding and LLMs will be a whole lot more attractive
to hire to build software than people who just know LLMs for many years to
come.</p>
</div></blockquote><p>Simon has also emphasized in his writing what I have found: AI lets me write
code that I wouldn’t have undertaken without its help. It doesn’t produce the
finished code, but it’s a helpful pair-programming assistant.</p><h1 id="h_can_llms_think">Can LLMs think?<a class="headerlink" aria-label="Link to this header" href="#h_can_llms_think"></a></h1><p>Another objection I see often: “but LLMs can’t think, they just predict the
next word!” I’m not sure we have a consensus understanding of what “think” means
in this context. Airplanes don’t fly in the same way that birds do.
Automobiles don’t run in the same way that horses do. The important thing is
that they accomplish many of the same tasks.</p><p>OK, so AI doesn’t think the same way that people do. I’m fine with that.
What’s important to me is that it can do some work for me, work that could also
be done by people thinking. Cars (“horseless carriages”) do work that used to
be done by horses running. No one now complains that cars work differently than
horses.</p><p>If “just predict the next word” is an accurate description of what LLMs are
doing, it’s a demonstration of how surprisingly powerful predicting the next
word can be.</p><h1 id="h_harms">Harms<a class="headerlink" aria-label="Link to this header" href="#h_harms"></a></h1><p>I am concerned about the harms that AI can cause. Some people and
organizations are focused on Asimov-style harms (will society collapse, will
millions die?) and I am glad they are. But I’m more concerned with
Dickens-style harms: people losing jobs not because AI can do their work, but
because people in charge will think AI can do other people’s work. Harms due to
people misunderstanding what AI does and doesn’t do well and misusing it.</p><p>I don’t see easy solutions to these problems. To go back to the car analogy:
we’ve been a car society for about 120 years. For most of that time we’ve been
leaning more and more towards cars. We are still trying to find the right
balance, the right way to reduce the harm they cause while keeping the benefits
they give us.</p><p>AI will be similar. The technology is not going to go away. We will not turn
our back on it and put it back into the bottle. We’ll continue to work on
improving how it works and how we work with it. There will be good and bad.
The balance will depend on how well we collectively use it and educate each
other, and how well we pay attention to what is happening.</p><h1 id="h_future">Future<a class="headerlink" aria-label="Link to this header" href="#h_future"></a></h1><p>The pro-AI hype in the industry now is at a fever pitch, it’s completely
overblown. But the anti-AI crowd also seems to be railing against it without a
clear understanding of the current capabilities or the useful approaches.</p><p>I’m going to be using AI more, and learning where it works well and where it
doesn’t.</p>
]]></description>
</item>
<item rdf:about="https://nedbatchelder.com/blog/202503/faster_branch_coverage_measurement.html">
<title>Faster branch coverage measurement</title>
<link>https://nedbatchelder.com/blog/202503/faster_branch_coverage_measurement.html</link>
<dc:date>2025-03-09T16:07:02-04:00</dc:date>
<dc:creator>Ned Batchelder</dc:creator>
<description><![CDATA[<p>After nearly two years, I think this is finally ready: coverage.py can use
<a rel="external noopener" href="https://docs.python.org/3/library/sys.monitoring.html">sys.monitoring</a> to more efficiently measure branch
coverage.</p><p>I would love for people to try it, but it’s a little involved at the
moment:</p><ul>
<li>You need to have your own build of Python from the main branch on GitHub,
because the CPython side of the work landed after 3.14 alpha 5. Alpha 6 is
supposed to arrive within a week, so that will make it easier.</li>
<li>I haven’t released a version of coverage.py to PyPI with this code yet, so
you also need to install coverage from GitHub:</li>
<blockquote class="code"><pre class="shell"><div class="source">%<span class="w"> </span>python3<span class="w"> </span>-m<span class="w"> </span>pip<span class="w"> </span>install<span class="w"> </span><span>git+https://github.com/nedbat/coveragepy</span>
<br></div>
</pre></blockquote>
</ul><p>Once you have both of those things, set the environment variable
<code>COVERAGE_CORE=sysmon</code> and run coverage as you usually do. If all goes
well, it should be faster. Please let me know!</p><p>Feedback is welcome in <a rel="external noopener" href="https://github.com/nedbat/coveragepy/issues">GitHub issues</a> or in the
<a rel="external noopener" href="https://discord.gg/RBw567RyWc">#coverage-py channel</a> in the Python Discord server.</p><p>This has been a long journey, starting when <a rel="external noopener" href="https://discuss.python.org/t/pep-669-low-impact-monitoring-for-cpython/13018/33">I first
commented on PEP 669</a> that underpins this work. Mark Shannon and I have had
many back and forths about the behavior of sys.monitoring, finally landing on
something that would work for us both.</p><p>For the curious: traditionally coverage.py relied on
<a rel="external noopener" href="https://docs.python.org/3/library/sys.html#sys.settrace">sys.settrace</a>. Python calls my recording function for
every line of Python executed. It’s simple and effective, but inefficient.
After I’ve been told a line was executed once, I don’t need to be told again,
but settrace keeps calling my function. The new
<a rel="external noopener" href="https://docs.python.org/3/library/sys.monitoring.html">sys.monitoring</a> that arrived in Python 3.12 lets me
disable an event once it’s fired, so after the first ping there’s no overhead to
running that same code multiple times.</p><p>It took a while to iron out the event behavior that lets us measure branches
as well as lines, but Python 3.14.0 after alpha 5 has it, so we’re finally able
to announce coverage.py support for people to try out.</p>
]]></description>
</item>
</rdf:RDF>
If you would like to create a banner that links to this page (i.e. this validation result), do the following:
Download the "valid RSS" banner.
Upload the image to your own server. (This step is important. Please do not link directly to the image on this server.)
Add this HTML to your page (change the image src
attribute if necessary):
If you would like to create a text link instead, here is the URL you can use:
http://www.feedvalidator.org/check.cgi?url=https%3A//www.nedbatchelder.com/blog/rss.xml