<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Jean-Luc Martel</title>
    <description>The latest articles on DEV Community by Jean-Luc Martel (@jlmartel).</description>
    <link>https://dev.to/jlmartel</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4061410%2Fc407a2f0-c9db-4796-bcf2-43f744536ee6.jpg</url>
      <title>DEV Community: Jean-Luc Martel</title>
      <link>https://dev.to/jlmartel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jlmartel"/>
    <language>en</language>
    <item>
      <title>Python: while...else and Other Crimes Against Intuition</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Mon, 02 Nov 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/jlmartel/python-whileelse-and-other-crimes-against-intuition-33pd</link>
      <guid>https://dev.to/jlmartel/python-whileelse-and-other-crimes-against-intuition-33pd</guid>
      <description>&lt;p&gt;We've already explored some of PHP's more questionable life choices and JavaScript's remarkable ability to make perfectly valid code look like a practical joke.&lt;/p&gt;

&lt;p&gt;Now it's Python's turn.&lt;/p&gt;

&lt;p&gt;Python is particularly interesting because it has become the language of choice for data science, machine learning, and AI worlds.&lt;/p&gt;

&lt;p&gt;And understandably so.&lt;/p&gt;

&lt;p&gt;It's readable. It's expressive. Its ecosystem is extraordinary. You can go from a CSV file to a predictive model in 5 lines of code.&lt;/p&gt;

&lt;p&gt;But Python has a few surprises of its own.&lt;/p&gt;

&lt;p&gt;And when you're working with data, some of those surprises can produce results that are &lt;strong&gt;plausible, reproducible, and completely wrong&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Which is arguably worse than code that simply crashes.&lt;/p&gt;

&lt;p&gt;Here are five of my favorites.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. One Change to a Matrix Changes Every Row
&lt;/h2&gt;

&lt;p&gt;Let's start with something familiar to anyone who works with tabular data.&lt;/p&gt;

&lt;p&gt;You need a 3×3 matrix initialized to zero.&lt;/p&gt;

&lt;p&gt;Easy:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;matrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[[0, 0, 0],
 [0, 0, 0],
 [0, 0, 0]]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Perfect.&lt;/p&gt;

&lt;p&gt;Now let's change one value.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;99&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'd expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[[99, 0, 0],
 [ 0, 0, 0],
 [ 0, 0, 0]]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[[99, 0, 0],
 [99, 0, 0],
 [99, 0, 0]]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait. What?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You modified one cell, and an entire column changed.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The explanation is simple, but rather unpleasant.&lt;/p&gt;

&lt;p&gt;When Python evaluates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It doesn't create three independent inner lists.&lt;/p&gt;

&lt;p&gt;It creates one list and repeats the &lt;em&gt;reference&lt;/em&gt; to that list three times.&lt;/p&gt;

&lt;p&gt;All three rows are actually the same object.&lt;/p&gt;

&lt;p&gt;You can prove it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="n"&gt;matrix&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;matrix&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now each row is independent.&lt;/p&gt;

&lt;p&gt;To be clear, this is a native Python list problem, not how NumPy array construction works. But it's exactly the kind of mistake that can creep into preprocessing code, custom numerical algorithms, or quick exploratory scripts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; Repeating a list doesn't necessarily create independent copies. Sometimes it just gives the same object several different places to hide.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. Your Function Remembers Things You Never Asked It to Remember
&lt;/h2&gt;

&lt;p&gt;Suppose you're collecting results from an experiment.&lt;/p&gt;

&lt;p&gt;You write a little helper:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[]):&lt;/span&gt;
    &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks perfectly reasonable.&lt;/p&gt;

&lt;p&gt;Now you run two experiments:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;record_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Experiment A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nf"&gt;record_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Experiment B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;['Experiment A']
['Experiment A', 'Experiment B']
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Maybe that's what you wanted.&lt;/p&gt;

&lt;p&gt;But what if the second experiment was supposed to have its own history?&lt;/p&gt;

&lt;p&gt;Too bad.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Python has been maintaining a shared list behind your back.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The culprit is the default argument:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In Python, default argument values are evaluated when the function is &lt;em&gt;defined&lt;/em&gt;, not every time the function is called.&lt;/p&gt;

&lt;p&gt;That list is created once and reused across calls that omit the argument.&lt;/p&gt;

&lt;p&gt;Which means your convenient little helper has accidentally acquired state.&lt;/p&gt;

&lt;p&gt;In a data science workflow, that could mean results from one experiment leaking into another.&lt;/p&gt;

&lt;p&gt;The solution is the familiar &lt;code&gt;None&lt;/code&gt; pattern:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;record_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;history&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="n"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;history&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every call without an explicit history gets a fresh list.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; Python's default arguments aren't necessarily defaults in the sense you expect. Mutable defaults can become accidental storage devices.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;True&lt;/code&gt;, &lt;code&gt;1&lt;/code&gt;, and &lt;code&gt;1.0&lt;/code&gt; Are the Same Dictionary Key
&lt;/h2&gt;

&lt;p&gt;Here's an especially entertaining one for anyone working with categorical data.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;labels&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;boolean&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;integer&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;float&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;labels&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three keys, three types.&lt;/p&gt;

&lt;p&gt;You might reasonably expect three entries.&lt;/p&gt;

&lt;p&gt;Instead:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{True: 'float'}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One entry.&lt;/p&gt;

&lt;p&gt;The last value wins.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because Python considers all three keys equal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="bp"&gt;True&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;      &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;       &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;    &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And their hashes are equal, too.&lt;/p&gt;

&lt;p&gt;Even more interesting:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="nf"&gt;isinstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# True
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Yes, &lt;code&gt;bool&lt;/code&gt; is actually a subclass of &lt;code&gt;int&lt;/code&gt; in Python.&lt;/p&gt;

&lt;p&gt;So dictionaries treat these values as equivalent keys.&lt;/p&gt;

&lt;p&gt;The first key object remains, but its value gets overwritten twice.&lt;/p&gt;

&lt;p&gt;This can matter when you're mixing boolean indicators with numeric category identifiers.&lt;/p&gt;

&lt;p&gt;If your categories need to distinguish between a boolean &lt;code&gt;True&lt;/code&gt; and the integer &lt;code&gt;1&lt;/code&gt;, a plain dictionary won't do it automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; In Python, equality and identity are different concepts, and dictionaries care about equality and hashing, not whether two keys have different types.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Python Has a &lt;code&gt;while...else&lt;/code&gt;. Because Apparently &lt;code&gt;if&lt;/code&gt; Wasn't Enough.
&lt;/h2&gt;

&lt;p&gt;Most programmers are familiar with &lt;code&gt;if...else&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Some languages have &lt;code&gt;try...else&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Python went full "hold my beer" and decided that loops should have an &lt;code&gt;else&lt;/code&gt; too.&lt;/p&gt;

&lt;p&gt;Yes, really.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;This will never run&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;But this will!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;But this will!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Okay, that's a little unusual, but not completely insane.&lt;/p&gt;

&lt;p&gt;Now try this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Loop finished!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;0
1
2
Loop finished!
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait. What?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;while&lt;/code&gt; condition was "true", the loop executed, and somehow the &lt;code&gt;else&lt;/code&gt; block still ran.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Welcome to Python.&lt;/p&gt;

&lt;p&gt;Here's the secret: a loop's &lt;code&gt;else&lt;/code&gt; clause doesn't mean what you'd normally expect from the word "else."&lt;/p&gt;

&lt;p&gt;It actually means something closer to &lt;strong&gt;"the loop completed without encountering a &lt;code&gt;break&lt;/code&gt;."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

&lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
&lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Finished!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This time, the &lt;code&gt;else&lt;/code&gt; block doesn't execute.&lt;/p&gt;

&lt;p&gt;Because the loop ended with &lt;code&gt;break&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Which means the &lt;code&gt;else&lt;/code&gt; runs when the loop terminates normally, including when it never runs at all, but not when a &lt;code&gt;break&lt;/code&gt; interrupts it.&lt;/p&gt;

&lt;p&gt;To be fair, there's a legitimate use case here. When searching for something in a loop, you can use &lt;code&gt;break&lt;/code&gt; when you find it and &lt;code&gt;else&lt;/code&gt; when the search finishes without finding anything.&lt;/p&gt;

&lt;p&gt;But you have to admit that &lt;code&gt;else&lt;/code&gt; is a rather creative choice of keyword.&lt;/p&gt;

&lt;p&gt;Python's own documentation describes the behavior by comparing loop &lt;code&gt;else&lt;/code&gt; clauses to the &lt;code&gt;else&lt;/code&gt; in a &lt;code&gt;try&lt;/code&gt; statement.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; In Python, &lt;code&gt;else&lt;/code&gt; doesn't always mean "otherwise." Sometimes it means "congratulations, nobody broke out of the loop."&lt;/p&gt;

&lt;h2&gt;
  
  
  5. Python Objects Can Share a Secret You Never Intended Them to Share
&lt;/h2&gt;

&lt;p&gt;Let's get into object-oriented programming.&lt;/p&gt;

&lt;p&gt;Suppose you're tracking machine learning experiments. Each experiment has a name and a collection of metrics.&lt;/p&gt;

&lt;p&gt;You write a simple class:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Experiment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks reasonable enough.&lt;/p&gt;

&lt;p&gt;Now let's create two independent experiments.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;experiment_a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Model A&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;experiment_b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Experiment&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Model B&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;experiment_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experiment_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experiment_b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[0.95]
[0.95]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Excuse me?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;You added a metric to Model A, and Model B somehow knows about it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The two experiments are completely separate instances.&lt;/p&gt;

&lt;p&gt;Different names. Different objects.&lt;/p&gt;

&lt;p&gt;But apparently, they have a shared memory.&lt;/p&gt;

&lt;p&gt;Here's what's happening.&lt;/p&gt;

&lt;p&gt;In Python, attributes declared directly inside a class body are &lt;em&gt;class attributes&lt;/em&gt;, not instance attributes.&lt;/p&gt;

&lt;p&gt;So this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Experiment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Creates one list shared by every instance of &lt;code&gt;Experiment&lt;/code&gt;, unless an instance overrides the attribute.&lt;/p&gt;

&lt;p&gt;When Python evaluates:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It looks for &lt;code&gt;metrics&lt;/code&gt; on the instance.&lt;/p&gt;

&lt;p&gt;Doesn't find it.&lt;/p&gt;

&lt;p&gt;Looks on the class.&lt;/p&gt;

&lt;p&gt;Finds the shared list.&lt;/p&gt;

&lt;p&gt;And modifies it.&lt;/p&gt;

&lt;p&gt;Now here's where things get even more interesting.&lt;/p&gt;

&lt;p&gt;Suppose you do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;experiment_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.99&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experiment_a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# [0.99]
&lt;/span&gt;&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;experiment_b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# [0.95]
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Suddenly, the experiments are independent!&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because assigning to &lt;code&gt;experiment_a.metrics&lt;/code&gt; creates an instance attribute that shadows the class attribute.&lt;/p&gt;

&lt;p&gt;But calling &lt;code&gt;.append()&lt;/code&gt; on the original list modifies the shared class attribute.&lt;/p&gt;

&lt;p&gt;So two operations that look like they're manipulating the same property can behave very differently.&lt;/p&gt;

&lt;p&gt;The correct implementation is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;Experiment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;add_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;metrics&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;metric&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now every experiment gets its own metrics list.&lt;/p&gt;

&lt;p&gt;And this isn't limited to experimental metrics. The same problem can appear in cached results, configuration objects, event handlers, and any class that accidentally shares mutable state between instances.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; In Python, where you declare an attribute matters. Put mutable per-instance state in &lt;code&gt;__init__&lt;/code&gt;, unless you genuinely want every object to share it.&lt;/p&gt;

&lt;p&gt;Because nothing inspires confidence in your machine learning results quite like discovering that two supposedly independent experiments have been sharing their data all along.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Python Throws an Exception. Your Data Changes Anyway.
&lt;/h2&gt;

&lt;p&gt;I saved my favorite for last.&lt;/p&gt;

&lt;p&gt;Because this one challenges a fairly reasonable assumption:&lt;/p&gt;

&lt;p&gt;If an operation fails, surely it didn't complete?&lt;/p&gt;

&lt;p&gt;Meet Python.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],)&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="nb"&gt;TypeError&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Operation failed!&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="nf"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Operation failed!
([1, 2, 3],)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's review what just happened.&lt;/p&gt;

&lt;p&gt;Python threw an exception.&lt;/p&gt;

&lt;p&gt;We caught it.&lt;/p&gt;

&lt;p&gt;And the data changed anyway.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The operation failed successfully.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's why.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;data&lt;/code&gt; is a tuple containing a list.&lt;/p&gt;

&lt;p&gt;Tuples are immutable, meaning you can't replace their elements.&lt;/p&gt;

&lt;p&gt;But the objects &lt;em&gt;inside&lt;/em&gt; a tuple can still be mutable.&lt;/p&gt;

&lt;p&gt;When Python executes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things happen.&lt;/p&gt;

&lt;p&gt;First, the list is modified in place. The value &lt;code&gt;3&lt;/code&gt; is appended.&lt;/p&gt;

&lt;p&gt;Then Python attempts to assign the result back into the tuple.&lt;/p&gt;

&lt;p&gt;That second step fails because tuples don't support item assignment.&lt;/p&gt;

&lt;p&gt;But the first step already happened.&lt;/p&gt;

&lt;p&gt;And Python doesn't automatically roll it back.&lt;/p&gt;

&lt;p&gt;So you get an exception AND modified data.&lt;/p&gt;

&lt;p&gt;To avoid this particular problem, you can mutate the contained list explicitly when that's what you intend:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or use immutable contents if you need the entire structure to remain unchanged.&lt;/p&gt;

&lt;p&gt;The truly wonderful detail is that this behavior is documented in the official Python FAQ.&lt;/p&gt;

&lt;p&gt;It's not a bug.&lt;/p&gt;

&lt;p&gt;It's a consequence of Python's object model and augmented assignment semantics.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; Exceptions do not imply atomicity. A failed operation can leave behind side effects, and those side effects can matter enormously in data-processing pipelines.&lt;/p&gt;

&lt;h2&gt;
  
  
  Beautiful Is Better Than Ugly. Usually.
&lt;/h2&gt;

&lt;p&gt;Python's famous Zen includes principles like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Beautiful is better than ugly.&lt;/li&gt;
&lt;li&gt;Explicit is better than implicit.&lt;/li&gt;
&lt;li&gt;Simple is better than complex.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And Python generally does an admirable job of living up to them.&lt;/p&gt;

&lt;p&gt;But as we've seen, simple syntax doesn't always imply simple behavior.&lt;/p&gt;

&lt;p&gt;The most dangerous programming errors aren't necessarily the ones that crash.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;They're the ones that quietly give you the wrong answer.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;And in data science, that wrong answer might come with a beautiful chart and a very convincing confidence interval.&lt;/p&gt;




&lt;p&gt;This is part of an ongoing series exploring the stranger corners of familiar programming languages. We've already visited PHP and JavaScript. C# and Perl are still waiting.&lt;/p&gt;

&lt;p&gt;Perl, naturally, is preparing something involving regex.&lt;/p&gt;

</description>
      <category>python</category>
      <category>programming</category>
      <category>machinelearning</category>
      <category>discuss</category>
    </item>
    <item>
      <title>JavaScript: The Language That Gaslights Its Developers</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Fri, 30 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/jlmartel/javascript-the-language-that-gaslights-its-developers-1m08</link>
      <guid>https://dev.to/jlmartel/javascript-the-language-that-gaslights-its-developers-1m08</guid>
      <description>&lt;p&gt;After writing about PHP's greatest hits in the Weird Language Hall of Fame, it felt only fair to turn to JavaScript.&lt;/p&gt;

&lt;p&gt;Because if PHP is the language of historical baggage, JavaScript is the language of &lt;strong&gt;"this seems impossible, and yet it is fully specified behavior."&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;JavaScript has a special talent for producing results that look like bugs, feel like bugs, and absolutely are not bugs.&lt;/p&gt;

&lt;p&gt;Most of the time, the culprit is type coercion, implicit conversion, or one of the language's many "technically reasonable, emotionally devastating" rules.&lt;/p&gt;

&lt;p&gt;So here are a few of my favorite examples.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. An Empty Array Is Truthy... But Also Equal to &lt;code&gt;false&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;Let's begin with a classic.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nc"&gt;Boolean&lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt;       &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;       &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;      &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the kind of thing that makes developers stare at the screen and wonder why they picked computing science instead of accounting like their mom advised.&lt;/p&gt;

&lt;p&gt;An empty array is truthy.&lt;/p&gt;

&lt;p&gt;So far, so normal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;([])&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Yep, truthy&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That runs.&lt;/p&gt;

&lt;p&gt;But then this also happens:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;   &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because JavaScript doesn't use the same rules for boolean contexts and loose equality.&lt;/p&gt;

&lt;p&gt;In a boolean context, arrays are simply objects, and objects are truthy.&lt;/p&gt;

&lt;p&gt;But with &lt;code&gt;==&lt;/code&gt;, JavaScript starts converting values. The empty array becomes an empty string, which then becomes the number &lt;code&gt;0&lt;/code&gt;. Meanwhile, &lt;code&gt;false&lt;/code&gt; also becomes &lt;code&gt;0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So JavaScript ends up comparing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which is, unfortunately, true.&lt;/p&gt;

&lt;p&gt;And if you really want to ruin your afternoon:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[]&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;   &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because &lt;code&gt;![]&lt;/code&gt; becomes &lt;code&gt;false&lt;/code&gt;, and now you're back in the previous example.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; truthiness and loose equality are not the same thing. If you're comparing values in JavaScript, &lt;code&gt;===&lt;/code&gt; should be your default.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;typeof null&lt;/code&gt; Is &lt;code&gt;"object"&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;This one has survived for decades.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;         &lt;span class="c1"&gt;// "object"&lt;/span&gt;
&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="kc"&gt;undefined&lt;/span&gt;    &lt;span class="c1"&gt;// "undefined"&lt;/span&gt;
&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;           &lt;span class="c1"&gt;// "object"&lt;/span&gt;
&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="kc"&gt;NaN&lt;/span&gt;          &lt;span class="c1"&gt;// "number"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Let's focus on the weirdest part:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;   &lt;span class="c1"&gt;// "object"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Except &lt;code&gt;null&lt;/code&gt; is not an object.&lt;/p&gt;

&lt;p&gt;This is a long-standing historical bug in JavaScript that became untouchable because too much code depended on it.&lt;/p&gt;

&lt;p&gt;So now it just lives there, like haunted wallpaper.&lt;/p&gt;

&lt;p&gt;And since JavaScript enjoys layering confusion on top of confusion:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;null&lt;/code&gt; is not an object, but &lt;code&gt;typeof null&lt;/code&gt; says it is.&lt;/li&gt;
&lt;li&gt;Arrays are objects, so &lt;code&gt;typeof []&lt;/code&gt; is also &lt;code&gt;"object"&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;NaN&lt;/code&gt; means "Not a Number," but &lt;code&gt;typeof NaN&lt;/code&gt; is &lt;code&gt;"number"&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can almost hear the language whispering, &lt;em&gt;"If you can't handle me on my worst days, you don't deserve me."&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; &lt;code&gt;typeof&lt;/code&gt; is useful, but not always precise. If you need to distinguish arrays, nulls, and objects, use more specific checks like &lt;code&gt;Array.isArray()&lt;/code&gt; and explicit &lt;code&gt;value === null&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. &lt;code&gt;map(parseInt)&lt;/code&gt; Produces Nonsense
&lt;/h2&gt;

&lt;p&gt;This is one of the best examples in the entire language because every piece of it looks reasonable.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What would you expect?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What do you actually get?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;NaN&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Naturally.&lt;/p&gt;

&lt;p&gt;Here's what's happening.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;map()&lt;/code&gt; doesn't just pass each array element to the callback. It passes three arguments:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;the current value
&lt;/li&gt;
&lt;li&gt;the current index
&lt;/li&gt;
&lt;li&gt;the whole array&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;So this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;effectively becomes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// 10&lt;/span&gt;
&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// NaN&lt;/span&gt;
&lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;// 2&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And why is that a problem?&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;parseInt()&lt;/code&gt; takes a second argument: the radix.&lt;/p&gt;

&lt;p&gt;So JavaScript interprets the array index as the number base.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;"10"&lt;/code&gt; in base 0 becomes 10&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"10"&lt;/code&gt; in base 1 is invalid, so &lt;code&gt;NaN&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;"10"&lt;/code&gt; in base 2 becomes 2&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is one of those perfect JavaScript moments where two completely legitimate APIs collide and create chaos.&lt;/p&gt;

&lt;p&gt;The fix is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;or:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;10&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; Don't hand a function to &lt;code&gt;map()&lt;/code&gt; unless you know exactly what arguments it expects.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. &lt;code&gt;sort()&lt;/code&gt; Sorts Numbers Like Strings
&lt;/h2&gt;

&lt;p&gt;This one has caused real bugs in real applications.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you probably expect:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What JavaScript gives you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Because by default, &lt;code&gt;sort()&lt;/code&gt; compares elements as strings.&lt;/p&gt;

&lt;p&gt;So JavaScript is effectively sorting these:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;code&gt;"1"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"2"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"10"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"20"&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;"3"&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;And in string comparison, &lt;code&gt;"10"&lt;/code&gt; comes before &lt;code&gt;"2"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;This makes perfect sense if you're sorting words.&lt;/p&gt;

&lt;p&gt;It makes much less sense if you're sorting numbers in a programming language and expected numeric order to be the default.&lt;/p&gt;

&lt;p&gt;The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nf"&gt;sort&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now you get what you wanted:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; JavaScript's default behaviors are often optimized for generality, not for your expectations.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, Is JavaScript Bad?
&lt;/h2&gt;

&lt;p&gt;Not really.&lt;/p&gt;

&lt;p&gt;Like PHP, JavaScript is often mocked for behavior that is actually consistent with its own rules.&lt;/p&gt;

&lt;p&gt;The problem is that many of those rules are invisible until you trip over them.&lt;/p&gt;

&lt;p&gt;An empty array can be truthy while also loosely equal to &lt;code&gt;false&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;typeof null&lt;/code&gt; can lie to your face.&lt;/p&gt;

&lt;p&gt;A perfectly innocent-looking &lt;code&gt;map(parseInt)&lt;/code&gt; can produce results that seem like a prank.&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;sort()&lt;/code&gt; can rearrange numbers alphabetically, because apparently that was a reasonable default choice at some point in human history.&lt;/p&gt;

&lt;p&gt;But that's also what makes JavaScript fascinating.&lt;/p&gt;

&lt;p&gt;Its weirdness isn't random. It's the product of a language that was created to validate forms in web pages and became something people use to build microservices and large-scale data streaming pipelines, and evolved while the entire internet was standing on top of it.&lt;/p&gt;

&lt;p&gt;Which is kind of impressive.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This is part of a series on programming languages that occasionally make developers question the nature of reality.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;PHP already had its turn. Python, C#, and Perl are waiting patiently in the wings.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;And Perl, as always, remains less a programming language than a dare.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>programming</category>
      <category>webdev</category>
      <category>discuss</category>
    </item>
    <item>
      <title>PHP: 30 Years of Making Developers Say "Wait, What?" (Part 2: It Gets Worse)</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Wed, 28 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/jlmartel/php-30-years-of-making-developers-say-wait-what-part-2-it-gets-worse-1jk8</link>
      <guid>https://dev.to/jlmartel/php-30-years-of-making-developers-say-wait-what-part-2-it-gets-worse-1jk8</guid>
      <description>&lt;p&gt;Apparently, three examples of PHP weirdness weren't enough.&lt;/p&gt;

&lt;p&gt;In the original post, we explored how PHP considers &lt;code&gt;"0"&lt;/code&gt; to be false, how array keys can silently overwrite one another, and how an innocent-looking &lt;code&gt;foreach&lt;/code&gt; loop can unexpectedly modify your data.&lt;/p&gt;

&lt;p&gt;Those were fun.&lt;/p&gt;

&lt;p&gt;But PHP has been around for three decades, and you don't accumulate that much history without developing a few interesting personality traits.&lt;/p&gt;

&lt;p&gt;So here are four more examples of PHP doing exactly what it was designed to do, even when that seems deeply unreasonable.&lt;/p&gt;

&lt;h2&gt;
  
  
  4. Filtering an Array Can Turn It Into a JSON Object
&lt;/h2&gt;

&lt;p&gt;Let's say you're building an API.&lt;/p&gt;

&lt;p&gt;You have an array of fruit. You want to remove one item and return the rest as JSON.&lt;/p&gt;

&lt;p&gt;Simple enough.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$items&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'apple'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'banana'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'cherry'&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nv"&gt;$filtered&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nv"&gt;$items&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="k"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nv"&gt;$item&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="s1"&gt;'banana'&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filtered&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What do you expect?&lt;/p&gt;

&lt;p&gt;Probably this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"cherry"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What you actually get:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nl"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"2"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"cherry"&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait. What?&lt;/p&gt;

&lt;p&gt;You started with an array. You filtered an array. You encoded an array.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;And somehow, you got a JSON object.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Here's why.&lt;/p&gt;

&lt;p&gt;PHP's &lt;code&gt;array_filter()&lt;/code&gt; preserves the original keys.&lt;/p&gt;

&lt;p&gt;After filtering, the array looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'apple'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'cherry'&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When &lt;code&gt;json_encode()&lt;/code&gt; encounters a PHP array whose keys aren't consecutive integers starting at zero, it serializes it as a JSON object rather than a JSON array.&lt;/p&gt;

&lt;p&gt;Your perfectly reasonable filtering operation has silently changed the shape of your API response.&lt;/p&gt;

&lt;p&gt;Imagine the delight of the frontend developer expecting an array.&lt;/p&gt;

&lt;p&gt;The fix:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="nb"&gt;json_encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;array_values&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$filtered&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Which gives you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"apple"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s2"&gt;"cherry"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; If you're filtering arrays for JSON responses, remember that &lt;code&gt;array_filter()&lt;/code&gt; preserves keys. Use &lt;code&gt;array_values()&lt;/code&gt; when you need a proper JSON array.&lt;/p&gt;

&lt;h2&gt;
  
  
  5. A Variable Can Exist and Still Not Be Set
&lt;/h2&gt;

&lt;p&gt;Let's talk about &lt;code&gt;isset()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The name seems reasonably self-explanatory.&lt;/p&gt;

&lt;p&gt;Surely it tells you whether something exists?&lt;/p&gt;

&lt;p&gt;Well, not exactly.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$config&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'timeout'&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'timeout'&lt;/span&gt;&lt;span class="p"&gt;]));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bool(false)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the key is right there!&lt;/p&gt;

&lt;p&gt;You can see it. I can see it. PHP can presumably see it.&lt;/p&gt;

&lt;p&gt;And yet &lt;code&gt;isset()&lt;/code&gt; returns false.&lt;/p&gt;

&lt;p&gt;That's because &lt;code&gt;isset()&lt;/code&gt; doesn't actually mean "does this variable or key exist?"&lt;/p&gt;

&lt;p&gt;It means something closer to &lt;strong&gt;"does this exist AND contain a value other than null?"&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So we have two functions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;isset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'timeout'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;                &lt;span class="c1"&gt;// false&lt;/span&gt;

&lt;span class="nb"&gt;array_key_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'timeout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same array. Same key. Different answers.&lt;/p&gt;

&lt;p&gt;And there's a practical consequence.&lt;/p&gt;

&lt;p&gt;Suppose &lt;code&gt;null&lt;/code&gt; means "disable the timeout," while a missing key means "use the default timeout."&lt;/p&gt;

&lt;p&gt;You might write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'timeout'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the null coalescing operator also falls back when the value is &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So &lt;code&gt;$timeout&lt;/code&gt; becomes &lt;code&gt;30&lt;/code&gt;, even though the configuration explicitly contained &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If those two states matter, you need to distinguish them:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$timeout&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;array_key_exists&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'timeout'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="o"&gt;?&lt;/span&gt; &lt;span class="nv"&gt;$config&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'timeout'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="o"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now an explicitly configured &lt;code&gt;null&lt;/code&gt; stays &lt;code&gt;null&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; In PHP, "missing" and "present but null" are different states, but &lt;code&gt;isset()&lt;/code&gt; treats them the same. Sometimes that's convenient. Sometimes it's a bug waiting for the right configuration file.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Two Different Strings Can Be Equal Because They're Both Zero
&lt;/h2&gt;

&lt;p&gt;Here's a fun one.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"0e123"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"0e999"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;What would you expect?&lt;/p&gt;

&lt;p&gt;They're clearly different strings.&lt;/p&gt;

&lt;p&gt;Surely the answer is false.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;bool(true)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Of course it is.&lt;/p&gt;

&lt;p&gt;The explanation involves PHP's numeric-string handling.&lt;/p&gt;

&lt;p&gt;Both strings are valid representations of numbers in scientific notation.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;0e123&lt;/code&gt; means zero multiplied by ten to the power of 123.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;0e999&lt;/code&gt; means zero multiplied by ten to the power of 999.&lt;/p&gt;

&lt;p&gt;Both evaluate numerically to zero.&lt;/p&gt;

&lt;p&gt;And because PHP's loose comparison operator (&lt;code&gt;==&lt;/code&gt;) recognizes both strings as numeric, it compares their numeric values.&lt;/p&gt;

&lt;p&gt;So:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s2"&gt;"0e123"&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"0e999"&lt;/span&gt;    &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="s2"&gt;"0e123"&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;"0e999"&lt;/span&gt;   &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This particular behavior has a wonderful nickname: &lt;strong&gt;magic hashes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Certain hash outputs happen to consist entirely of a zero, followed by &lt;code&gt;e&lt;/code&gt;, followed by digits.&lt;/p&gt;

&lt;p&gt;When two such hashes are compared using loose equality, PHP can interpret them as equal numeric values even though their string contents differ.&lt;/p&gt;

&lt;p&gt;Historically, this has contributed to security vulnerabilities in applications that compared hash strings incorrectly.&lt;/p&gt;

&lt;p&gt;And no, switching from PHP 7 to PHP 8 doesn't eliminate this particular issue. Both operands here are valid numeric strings, so the loose comparison still considers them equal.&lt;/p&gt;

&lt;p&gt;The fix is straightforward:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="s2"&gt;"0e123"&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="s2"&gt;"0e999"&lt;/span&gt;  &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For security-sensitive hash comparisons, use the appropriate cryptographic comparison functions, such as &lt;code&gt;hash_equals()&lt;/code&gt; when comparing authentication-related strings, or &lt;code&gt;password_verify()&lt;/code&gt; for password hashes.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; Loose equality isn't merely a convenience feature. When values originate from external input, it can become a security problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  7. &lt;code&gt;switch&lt;/code&gt; and &lt;code&gt;match&lt;/code&gt; Disagree About Reality
&lt;/h2&gt;

&lt;p&gt;PHP 8 introduced &lt;code&gt;match&lt;/code&gt;, a more modern alternative to &lt;code&gt;switch&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;And naturally, they don't quite agree on what constitutes a match.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;switch&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"Integer ten"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

    &lt;span class="k"&gt;case&lt;/span&gt; &lt;span class="s2"&gt;"10"&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"String ten"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;break&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Integer ten
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The value is a string.&lt;/p&gt;

&lt;p&gt;The first case is an integer.&lt;/p&gt;

&lt;p&gt;But PHP's &lt;code&gt;switch&lt;/code&gt; uses loose comparisons, so &lt;code&gt;"10"&lt;/code&gt; matches &lt;code&gt;10&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The first matching case wins.&lt;/p&gt;

&lt;p&gt;Now let's rewrite that using &lt;code&gt;match&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"10"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="k"&gt;match&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$value&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="mi"&gt;10&lt;/span&gt;   &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"Integer ten"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"10"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"String ten"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Output:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;String ten
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Same input. Same alternatives. Different result.&lt;/p&gt;

&lt;p&gt;Why?&lt;/p&gt;

&lt;p&gt;Because &lt;code&gt;match&lt;/code&gt; uses strict comparisons (&lt;code&gt;===&lt;/code&gt;), while &lt;code&gt;switch&lt;/code&gt; uses loose comparisons (&lt;code&gt;==&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;And &lt;code&gt;match&lt;/code&gt; has some other differences, including no fall-through and a requirement to handle unmatched values or provide a default.&lt;/p&gt;

&lt;p&gt;To be fair, &lt;code&gt;match&lt;/code&gt; is arguably the better-designed construct.&lt;/p&gt;

&lt;p&gt;But imagine converting an old &lt;code&gt;switch&lt;/code&gt; statement to &lt;code&gt;match&lt;/code&gt; during a refactoring session, assuming that you're just modernizing the syntax.&lt;/p&gt;

&lt;p&gt;You could change application behavior without changing any of the values being compared.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; When replacing &lt;code&gt;switch&lt;/code&gt; with &lt;code&gt;match&lt;/code&gt;, you're not merely changing syntax. You're changing comparison semantics.&lt;/p&gt;

&lt;h2&gt;
  
  
  PHP Is Consistent. That's Not Always Comforting.
&lt;/h2&gt;

&lt;p&gt;Here's the interesting thing about all seven examples we've now explored across these two articles.&lt;/p&gt;

&lt;p&gt;None of them are random.&lt;/p&gt;

&lt;p&gt;PHP has rules governing truthiness, type coercion, array keys, references, JSON serialization, and comparisons.&lt;/p&gt;

&lt;p&gt;The interpreter follows those rules consistently.&lt;/p&gt;

&lt;p&gt;The problem is that those rules don't always line up with what developers intuitively expect.&lt;/p&gt;

&lt;p&gt;And sometimes two individually reasonable rules interact to produce something spectacularly unreasonable.&lt;/p&gt;

&lt;p&gt;That's what makes PHP such a fascinating language to revisit.&lt;/p&gt;

&lt;p&gt;It has evolved a lot since the 1990s. Modern PHP is faster, more expressive, and substantially better engineered than the language many developers remember.&lt;/p&gt;

&lt;p&gt;But it still has a few surprises hiding behind perfectly ordinary syntax.&lt;/p&gt;

&lt;p&gt;And after thirty years, I suspect it's earned the right to be a little eccentric.&lt;/p&gt;

</description>
      <category>php</category>
      <category>programming</category>
      <category>webdev</category>
      <category>discuss</category>
    </item>
    <item>
      <title>PHP: 30 Years of Making Developers Say "Wait, What?"</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Sun, 25 Oct 2026 01:57:59 +0000</pubDate>
      <link>https://dev.to/jlmartel/php-30-years-of-making-developers-say-wait-what-3flc</link>
      <guid>https://dev.to/jlmartel/php-30-years-of-making-developers-say-wait-what-3flc</guid>
      <description>&lt;p&gt;With PHP suddenly back in the spotlight, I figured this was a good time to revisit one of the web's most successful, most criticized, and occasionally most bewildering programming languages.&lt;/p&gt;

&lt;p&gt;PHP has been powering websites for decades. It has evolved a lot, particularly through PHP 7 and 8, but it still carries some fascinating quirks.&lt;/p&gt;

&lt;p&gt;And I'm not talking about obscure syntax or features nobody uses. I'm talking about perfectly ordinary code that does something completely unexpected.&lt;/p&gt;

&lt;p&gt;Here are three of my favorites.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. The String "0" Is False. The String "00" Is True.
&lt;/h2&gt;

&lt;p&gt;Let's start with something simple.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;      &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s2"&gt;"00"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;     &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s2"&gt;"false"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;  &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;       &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Isn't that beautiful?&lt;/p&gt;

&lt;p&gt;The string &lt;code&gt;"0"&lt;/code&gt; evaluates to false, but &lt;code&gt;"00"&lt;/code&gt; evaluates to true.&lt;/p&gt;

&lt;p&gt;Even the literal string &lt;code&gt;"false"&lt;/code&gt; evaluates to true.&lt;/p&gt;

&lt;p&gt;PHP considers an empty string and the specific string &lt;code&gt;"0"&lt;/code&gt; to be falsy. Other nonempty strings, including &lt;code&gt;"00"&lt;/code&gt;, are truthy.&lt;/p&gt;

&lt;p&gt;Now consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$password&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nv"&gt;$password&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;echo&lt;/span&gt; &lt;span class="s2"&gt;"No password provided!"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Except a password &lt;strong&gt;was&lt;/strong&gt; provided. PHP just decided it didn't count.&lt;/p&gt;

&lt;p&gt;And if you're thinking that &lt;code&gt;empty()&lt;/code&gt; might save you:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"0"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;    &lt;span class="c1"&gt;// true&lt;/span&gt;
&lt;span class="k"&gt;empty&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"00"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// false&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nope.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; Don't use truthiness to determine whether a string contains meaningful input. Be explicit about what you're checking.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. PHP Arrays Can Silently Overwrite Your Data
&lt;/h2&gt;

&lt;p&gt;PHP arrays aren't really conventional arrays. They're ordered maps that support both integer and string keys.&lt;/p&gt;

&lt;p&gt;Which sounds perfectly reasonable until you do this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s2"&gt;"8"&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"eight"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mi"&gt;8&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"EIGHT"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"08"&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"zero eight"&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You might expect three entries.&lt;/p&gt;

&lt;p&gt;You get two.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Array
(
    [8] =&amp;gt; EIGHT
    [08] =&amp;gt; zero eight
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;PHP automatically converts the string key &lt;code&gt;"8"&lt;/code&gt; to the integer &lt;code&gt;8&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So the second entry overwrites the first.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;"08"&lt;/code&gt; stays a string because it isn't a valid decimal integer representation for this conversion.&lt;/p&gt;

&lt;p&gt;And it gets better:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="mi"&gt;1&lt;/span&gt;    &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"integer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s2"&gt;"1"&lt;/span&gt;  &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"string"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s2"&gt;"boolean"&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Result:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Array
(
    [1] =&amp;gt; boolean
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three apparently different keys. One surviving value.&lt;/p&gt;

&lt;p&gt;The first two collide because &lt;code&gt;"1"&lt;/code&gt; becomes integer &lt;code&gt;1&lt;/code&gt;. The third collides because &lt;code&gt;true&lt;/code&gt; also becomes integer &lt;code&gt;1&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; PHP array keys undergo implicit type conversion. If your application depends on distinguishing keys by type, you're in for an unpleasant surprise.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. A &lt;code&gt;foreach&lt;/code&gt; Loop Can Quietly Corrupt Your Array
&lt;/h2&gt;

&lt;p&gt;This is my favorite, because it's the kind of bug that can survive a code review.&lt;/p&gt;

&lt;p&gt;Consider:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nv"&gt;$numbers&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$numbers&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Do some processing&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$numbers&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Do some more processing&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nb"&gt;print_r&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$numbers&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Nothing particularly exotic here.&lt;/p&gt;

&lt;p&gt;The first loop iterates by reference. The second iterates normally.&lt;/p&gt;

&lt;p&gt;What does the final array contain?&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Array
(
    [0] =&amp;gt; 1
    [1] =&amp;gt; 2
    [2] =&amp;gt; 2
)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wait. What happened to the 3?&lt;/p&gt;

&lt;p&gt;The culprit is that innocent-looking ampersand in the first loop:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$numbers&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When that loop finishes, &lt;code&gt;$number&lt;/code&gt; remains a reference to the &lt;strong&gt;last element of the array&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The reference doesn't magically disappear when the loop ends.&lt;/p&gt;

&lt;p&gt;Then the second loop reuses &lt;code&gt;$number&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Each iteration assigns another array value to a variable that's &lt;em&gt;still referencing the last element&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;By the time the second loop finishes, the last element has been overwritten with the second-to-last value.&lt;/p&gt;

&lt;p&gt;The truly delightful part?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This behavior is explicitly documented in the PHP manual.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The fix is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="k"&gt;foreach&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$numbers&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// Do some processing&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;unset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$number&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// Break the reference&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;But the fact that you need to remember to do this is what makes the behavior so interesting.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The lesson:&lt;/strong&gt; References in PHP can outlive the control structures that introduced them. Reusing a variable after a by-reference loop can have side effects that are surprisingly difficult to spot.&lt;/p&gt;

&lt;h2&gt;
  
  
  So, Is PHP Broken?
&lt;/h2&gt;

&lt;p&gt;Not really.&lt;/p&gt;

&lt;p&gt;All three behaviors follow documented rules. They're not random, and they're not bugs in the PHP interpreter.&lt;/p&gt;

&lt;p&gt;But they illustrate something interesting about programming languages.&lt;/p&gt;

&lt;p&gt;A language can be internally consistent while still producing results that surprise the people using it.&lt;/p&gt;

&lt;p&gt;PHP has spent decades balancing developer convenience, backward compatibility, and language evolution. Sometimes those priorities have produced genuinely useful features. Sometimes they've produced behavior that makes you stare at your screen for a few minutes.&lt;/p&gt;

&lt;p&gt;And that's part of what makes exploring programming languages so entertaining.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;This was fun so I think I'll make it a series looking at the stranger corners of familiar languages.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>php</category>
      <category>programming</category>
      <category>webdev</category>
      <category>discuss</category>
    </item>
    <item>
      <title>Migrating AKS workloads from AAD Pod Identity to Entra Workload ID</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Thu, 22 Oct 2026 02:49:22 +0000</pubDate>
      <link>https://dev.to/jlmartel/migrating-aks-workloads-from-aad-pod-identity-to-entra-workload-id-3bk8</link>
      <guid>https://dev.to/jlmartel/migrating-aks-workloads-from-aad-pod-identity-to-entra-workload-id-3bk8</guid>
      <description>&lt;p&gt;You're looking at an AKS cluster for a client. &lt;/p&gt;

&lt;p&gt;Somewhere in it, a pod is authenticating to Azure using AAD Pod Identity. It relies on AzureIdentity and AzureIdentityBinding custom resources. It depends on the Node Managed Identity (NMI) pods intercepting metadata requests on port 2579. &lt;/p&gt;

&lt;p&gt;It is deprecated. Microsoft pulled the plug a year ago. The NMI intercept is a tax on the cluster's CPU and a liability in the threat model. &lt;/p&gt;

&lt;p&gt;You need to move it to Entra Workload ID. It uses OIDC federation. It is cleaner. But cleaner does not mean easy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Inventory the plumbing
&lt;/h2&gt;

&lt;p&gt;Do not just generate new manifests and hope for the best. &lt;/p&gt;

&lt;p&gt;Inventory what you actually have. Every &lt;code&gt;AzureIdentityBinding&lt;/code&gt; is a dependency. Every &lt;code&gt;AzureIdentity&lt;/code&gt; is a managed identity mapped to a specific set of pod labels. &lt;/p&gt;

&lt;p&gt;Map each workload to its intended federated Kubernetes Service Account. If you do not know exactly what the old identity does, you will not know what permissions the new one needs. &lt;/p&gt;

&lt;p&gt;Treat the old bindings as a map of your actual access patterns, not just a list of resources to delete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Validate the exact strings
&lt;/h2&gt;

&lt;p&gt;Entra Workload ID does not guess. It matches strings. &lt;/p&gt;

&lt;p&gt;When you create the federated identity credential in Entra ID, the issuer is your AKS OIDC endpoint. The subject must be exactly &lt;code&gt;system:serviceaccount:&amp;lt;namespace&amp;gt;:&amp;lt;name&amp;gt;&lt;/code&gt;. &lt;/p&gt;

&lt;p&gt;Miss a colon. Add a trailing space. Use the wrong namespace. &lt;strong&gt;It will not give you a helpful error&lt;/strong&gt;. It will just give you a 401 Unauthorized.&lt;/p&gt;

&lt;p&gt;The pod also requires the label &lt;code&gt;azure.workload.identity/use: "true"&lt;/code&gt;. Without it, the Entra workload identity mutating webhook ignores the pod. No environment variables get injected. No service account token gets projected. The pod starts, but it cannot authenticate.&lt;/p&gt;

&lt;p&gt;Test the token exchange before you move production traffic. &lt;/p&gt;

&lt;p&gt;Exec into a debug pod. Request the token from the projected volume. Decode it. Check the aud (audience) and sub (subject) claims. Verify the exact string matches what you configured in Entra ID. &lt;/p&gt;

&lt;h2&gt;
  
  
  Stage the migration
&lt;/h2&gt;

&lt;p&gt;Do not do a cutover. Do a &lt;strong&gt;migration&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;Deploy the new service account and the federated identity credential. Update the workload definition to use the new service account and apply the required pod label. &lt;/p&gt;

&lt;p&gt;Watch the logs. Verify the token exchange succeeds. &lt;/p&gt;

&lt;p&gt;Once the new identity is serving traffic and passing health checks, kill the old &lt;code&gt;AzureIdentityBinding&lt;/code&gt; and &lt;code&gt;AzureIdentity&lt;/code&gt; resources. &lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Do not&lt;/strong&gt; leave the old plumbing in the cluster "just in case." Deprecated resources left behind become permanent attack surface. If you do not need it, delete it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prove it works
&lt;/h2&gt;

&lt;p&gt;You are not done when the pod starts. You are done when you prove it can do its job.&lt;/p&gt;

&lt;p&gt;Check the Azure Activity Log. Look for the API calls. &lt;/p&gt;

&lt;p&gt;See the new managed identity making the requests. See the old one go silent. &lt;/p&gt;

&lt;p&gt;That is the only evidence that matters. The new identity reaches the required services with the intended permissions, and the old plumbing is gone.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>azure</category>
    </item>
    <item>
      <title>The Green Padlock That Almost Cost Me a Contract</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Mon, 19 Oct 2026 20:15:12 +0000</pubDate>
      <link>https://dev.to/jlmartel/the-green-padlock-that-almost-cost-me-a-contract-dgo</link>
      <guid>https://dev.to/jlmartel/the-green-padlock-that-almost-cost-me-a-contract-dgo</guid>
      <description>&lt;p&gt;Encryption is supposed to answer one question for the person sitting at the browser: &lt;em&gt;am I really talking to who I think I'm talking to?&lt;/em&gt; On a corporate network running TLS inspection, the honest answer is often "not necessarily" — and most of the people relying on that network have no idea. &lt;/p&gt;

&lt;p&gt;There are serious risks if the setup isn't done properly, but raising that flag can lead to uncomfortable discussions. I found that out the hard way, when demonstrating the problem nearly got my contract terminated.&lt;/p&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The client ran a TLS inspection appliance on their internal network — the kind that decrypts outbound HTTPS so it can be scanned for malware and data loss before being re-encrypted and sent on. These devices are common and, in principle, defensible: most malware command-and-control and data exfiltration now hides inside TLS, so without inspection, a lot of security tooling is blind.&lt;/p&gt;

&lt;p&gt;The way these appliances work is by man-in-the-middling every HTTPS connection. The appliance holds its own certificate authority. When you visit a site, it generates a certificate for that site on the fly, signs it with its own CA, and presents it to your browser. For this to work without warnings, that CA's root certificate is installed in the trust store of every managed machine — usually pushed automatically through group policy or device management.&lt;/p&gt;

&lt;p&gt;Once that root is trusted, the browser stops complaining. The certificates the appliance generates look completely valid. No warning. No broken padlock. Nothing to tell the user their "encrypted" session is being read in clear text in the middle.&lt;/p&gt;

&lt;p&gt;On top of that, the certificate issuance on this network was far more permissive than it should have been. The practical effect was that the internal CA could produce a trusted certificate for essentially any domain name — including public ones like google.com.&lt;/p&gt;

&lt;h2&gt;
  
  
  The problem I wanted to show
&lt;/h2&gt;

&lt;p&gt;The thing I wanted the security team to see is subtle, and it's the part people consistently underestimate: &lt;strong&gt;once a root is trusted, the browser's special protections for high-value sites quietly stop applying.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Browsers do a lot to protect domains like google.com:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Certificate pinning&lt;/strong&gt; ties those domains to a specific set of public CAs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Certificate Transparency&lt;/strong&gt; requires publicly trusted certificates to be logged in public, auditable logs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HSTS&lt;/strong&gt; forces connections to use HTTPS.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Every one of these makes an exception for locally installed roots. Pinning is skipped when the chain ends at an admin-installed CA. CT isn't required for private roots. HSTS is satisfied by any cert the browser considers valid. These exceptions exist &lt;em&gt;on purpose&lt;/em&gt; — they're what stops a legitimate inspection appliance from breaking Google for the whole company. But they also mean that anyone who can issue from the trusted internal CA inherits all of that stand-aside treatment.&lt;/p&gt;

&lt;p&gt;In other words: the protections you'd assume would catch an impersonated Google simply don't fire inside that environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  The demonstration
&lt;/h2&gt;

&lt;p&gt;To make this concrete rather than theoretical, I built a tiny proof of concept on my own machine:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I created a web page that said, in plain text, &lt;strong&gt;"I'm a fake Google page."&lt;/strong&gt; No login form, no attempt to harvest anything — the content existed only to make the point unmistakable.&lt;/li&gt;
&lt;li&gt;I added an entry to my own machine's hosts file pointing google.com at the IP serving that page.&lt;/li&gt;
&lt;li&gt;I generated a certificate for google.com from the environment's trusted CA.&lt;/li&gt;
&lt;li&gt;I loaded google.com in the browser.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The page loaded as google.com, with a green padlock, no warnings, and a certificate the browser reported as valid.&lt;/p&gt;

&lt;p&gt;That was the whole point. The browser was not malfunctioning. It was doing exactly what the trust configuration told it to do. Every safeguard that should have flagged an impersonated Google had been designed to stand aside for a locally trusted root, and that's exactly what happened.&lt;/p&gt;

&lt;p&gt;The hosts file was the deliberately contained part. It only affected my own machine — nobody else could reach that page. In a real attack, the hosts file stands in for an attacker's network position: a poisoned entry on the internal DNS resolver, ARP spoofing on a flat LAN, a rogue DHCP server, or a tampered proxy configuration. On a lot of internal networks, getting into that position is a lower bar than people like to assume. The demo swapped that step out for a one-line, single-machine change so there was no collateral effect at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  The reaction
&lt;/h2&gt;

&lt;p&gt;I sent the example to the security team as part of a technical debate about the risks of their setup. Their response was not to engage with the finding. It was to try to have my contract terminated, on the grounds that I had been "phishing."&lt;/p&gt;

&lt;p&gt;I had not shared the fake page with anyone. There was no victim, no credential harvesting, no email, no deception of a single other person. The entire thing lived on one machine — mine — and demonstrated a property of &lt;em&gt;their&lt;/em&gt; infrastructure. Calling it phishing was a category error, and I think a revealing one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the reaction matters more than the bug
&lt;/h2&gt;

&lt;p&gt;Phishing is the use of deception to trick a person into giving something up. A proof of concept that impersonates a site to no one but yourself, to demonstrate a flaw in a system you were engaged to assess, is the opposite of that: it's disclosure. The deception, such as it was, was aimed at a browser I controlled, to reveal something true about the environment.&lt;/p&gt;

&lt;p&gt;What the reaction demonstrated is a failure mode I've seen more than once: when a finding is uncomfortable, it's easier to attack the method than to sit with the conclusion. The conclusion here was genuinely uncomfortable. It wasn't "the appliance has a bug." It was "the way trust is configured on this network means the guarantees your users assume they have — that a green padlock on google.com means they're talking to Google — don't hold, and an insider with issuance rights can impersonate anything to anyone, invisibly."&lt;/p&gt;

&lt;p&gt;That's not a comfortable thing to hear about a system you own and defend. But shooting the person who showed it to you doesn't make it less true. It just guarantees the next person won't tell you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What should have happened
&lt;/h2&gt;

&lt;p&gt;The appropriate response to that demo was a conversation about controls, not contracts:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Keep the interception CA separate from the internal issuing CA&lt;/strong&gt;, so a compromise of one doesn't hand over the other.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Apply name constraints&lt;/strong&gt; to internal roots so they can only sign the domains they're supposed to.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lock certificate profiles&lt;/strong&gt; so requested names have to match the requester's identity, instead of allowing arbitrary domains.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restrict and audit issuance&lt;/strong&gt; — know who can request certificates and log everything that gets signed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Protect the CA key properly&lt;/strong&gt;, ideally in an HSM, because whoever holds it can impersonate anything to every managed device.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Be honest with users&lt;/strong&gt; that inspection is happening, and keep sensitive categories like banking and health out of scope.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that is exotic. It's the standard hygiene for running TLS inspection without turning it into a single, silent point of total compromise.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;The green padlock is a promise, and TLS inspection quietly rewrites the terms of that promise for everyone on the network. That can be done responsibly. But the first step is being willing to look at what your own configuration actually allows — especially when someone hands you an uncomfortable demonstration of it on a single contained machine, and asks you to think about it.&lt;/p&gt;

&lt;p&gt;Attacking the messenger is a choice. It's just not a security control.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>ssl</category>
      <category>dns</category>
      <category>browser</category>
    </item>
    <item>
      <title>BigQuery BI Engine deserves a benchmark before you dismiss Looker Studio as slow</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Sun, 18 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/jlmartel/bigquery-bi-engine-deserves-a-benchmark-before-you-dismiss-looker-studio-as-slow-1go5</link>
      <guid>https://dev.to/jlmartel/bigquery-bi-engine-deserves-a-benchmark-before-you-dismiss-looker-studio-as-slow-1go5</guid>
      <description>&lt;p&gt;Someone is complaining that Looker Studio is slow.&lt;/p&gt;

&lt;p&gt;A dashboard takes forty seconds to load. &lt;/p&gt;

&lt;p&gt;The immediate reaction is to blame the tool. &lt;/p&gt;

&lt;p&gt;"Looker is too slow."&lt;br&gt;
"We need to migrate."&lt;br&gt;
"We need a custom frontend."&lt;/p&gt;

&lt;p&gt;Look at the query logs.&lt;/p&gt;

&lt;p&gt;The dashboard is running a full table scan on a 500GB fact table every time a user changes a date filter.&lt;/p&gt;

&lt;p&gt;Looker Studio isn't the bottleneck. The query path is.&lt;/p&gt;

&lt;p&gt;Blaming the visualization layer for a warehouse execution problem is like blaming the paint for a crumbling foundation.&lt;/p&gt;

&lt;p&gt;Before you throw money at BigQuery BI Engine, isolate the variables.&lt;/p&gt;

&lt;p&gt;Performance hypotheses usually mash three distinct things together.&lt;/p&gt;

&lt;p&gt;First, connector caching. Is the dashboard just failing to cache the extract?&lt;/p&gt;

&lt;p&gt;Second, query shape. Are you running a distinct count on a high-cardinality string?&lt;/p&gt;

&lt;p&gt;Third, BI Engine support. Does the query use supported data types and operations?&lt;/p&gt;

&lt;p&gt;BI Engine can't fix a query it doesn't support. &lt;/p&gt;

&lt;p&gt;If your query falls back to the main BigQuery engine, the BI Engine reservation sits idle while you pay for it.&lt;/p&gt;

&lt;p&gt;Don't guess. Benchmark.&lt;/p&gt;

&lt;p&gt;Take the five most-used, slowest queries from the dashboard.&lt;/p&gt;

&lt;p&gt;Run them without BI Engine. Record the latency. Record the bytes billed.&lt;/p&gt;

&lt;p&gt;Turn on a BI Engine reservation. Run the exact same queries. Record the latency. Record the bytes billed.&lt;/p&gt;

&lt;p&gt;Calculate the cost of the reservation against the savings in bytes billed and the value of the latency reduction.&lt;/p&gt;

&lt;p&gt;If the query isn't supported, latency won't drop. &lt;/p&gt;

&lt;p&gt;If the query is just badly shaped, latency might drop, but the cost won't justify the reservation.&lt;/p&gt;

&lt;p&gt;A reservation is a blunt instrument. It solves a specific class of problem and ignores everything else.&lt;/p&gt;

&lt;p&gt;Before you provision one, ask yourself: which exact metric will prove the bottleneck is in the compute layer, and not in the query design?&lt;/p&gt;

&lt;p&gt;If you can't answer that, you haven't fixed a performance problem.&lt;/p&gt;

&lt;p&gt;You've just bought a faster way to run a bad query.&lt;/p&gt;

</description>
      <category>googlecloud</category>
      <category>bigdata</category>
      <category>data</category>
    </item>
    <item>
      <title>Azure subscription Owner is an access-delegation privilege</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Tue, 13 Oct 2026 07:00:00 +0000</pubDate>
      <link>https://dev.to/jlmartel/azure-subscription-owner-is-an-access-delegation-privilege-4nid</link>
      <guid>https://dev.to/jlmartel/azure-subscription-owner-is-an-access-delegation-privilege-4nid</guid>
      <description>&lt;p&gt;You hand someone the Owner role on an Azure subscription. &lt;/p&gt;

&lt;p&gt;You think you're giving them the keys to manage resources. VMs, databases, networking. You think you've made them a builder. &lt;/p&gt;

&lt;p&gt;You look at the role name. "Owner." It sounds like they just own the stuff inside the box. &lt;/p&gt;

&lt;p&gt;They don't. They own the box. And they own the locks.&lt;/p&gt;

&lt;p&gt;It’s not tenant-wide administration. A subscription Owner isn't a Global Administrator. They can't rewrite the rules for the entire Azure tenant. They can't even remove protected deny assignments if someone locked a resource down properly.&lt;/p&gt;

&lt;p&gt;But they don't need to. &lt;/p&gt;

&lt;p&gt;Because within the boundary of that subscription, Owner is an access-delegation privilege. &lt;/p&gt;

&lt;p&gt;They can assign powerful data-access roles. They can hand out the keys to the data plane. They can change legacy Key Vault access policies. They can grant someone else the right to read the secrets, even if they themselves were never explicitly granted that data access. &lt;/p&gt;

&lt;p&gt;The control plane becomes a backdoor to the data plane.&lt;/p&gt;

&lt;p&gt;We treat subscription management and data access as two different conversations. &lt;/p&gt;

&lt;p&gt;They aren't. &lt;/p&gt;

&lt;p&gt;If you want to protect your data, you need to look at who holds the Owner role. Review who can assign those powerful data-access roles. Review who can quietly flip a legacy Key Vault access policy to let themselves in.&lt;/p&gt;

&lt;p&gt;When that Owner grant was approved, what was the justification? "Needs to manage the dev environment." "Needs to deploy the infrastructure."&lt;/p&gt;

&lt;p&gt;But was the grant approved with those delegation powers in view? &lt;/p&gt;

&lt;p&gt;If not, you haven't granted resource administration. &lt;/p&gt;

&lt;p&gt;You have granted data access. And you didn't even realize it.&lt;/p&gt;

</description>
      <category>azure</category>
      <category>iam</category>
    </item>
    <item>
      <title>The data engineering reality of GNNs in production</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Thu, 08 Oct 2026 20:32:16 +0000</pubDate>
      <link>https://dev.to/jlmartel/the-data-engineering-reality-of-gnns-in-production-1n0j</link>
      <guid>https://dev.to/jlmartel/the-data-engineering-reality-of-gnns-in-production-1n0j</guid>
      <description>&lt;p&gt;Your offline GNN looks perfect. The AUC is high. The precision is great.&lt;/p&gt;

&lt;p&gt;You ship it to production to catch fraud or optimize a supply chain. &lt;/p&gt;

&lt;p&gt;Then the metrics tank.&lt;/p&gt;

&lt;p&gt;The model isn't broken. The data is just too old. &lt;/p&gt;

&lt;p&gt;It is making real-time decisions using neighborhoods and embeddings from yesterday, or last week. In fraud, a node's connections change in milliseconds. In supply chain, they change in hours. &lt;/p&gt;

&lt;p&gt;The model is evaluating a ghost.&lt;/p&gt;

&lt;p&gt;GNNs don't just need features. They need fresh ones. They need neighborhoods that reflect the current state of the world.&lt;/p&gt;

&lt;p&gt;This means your graph storage and feature-update pipelines cannot be an afterthought. They must be engineered to the exact freshness the use case demands. &lt;/p&gt;

&lt;p&gt;A batch job running at midnight is fine for predicting customer churn. It is catastrophic for blocking a fraudulent transaction at checkout. &lt;/p&gt;

&lt;p&gt;Data engineering must run on the same clock as the business decision.&lt;/p&gt;

&lt;p&gt;Don't trust your architecture diagram. Trust a trace.&lt;/p&gt;

&lt;p&gt;Pick one production decision and trace it backward. Start at the served model input. &lt;/p&gt;

&lt;p&gt;Look at the embedding it consumed. Look at the neighborhood it aggregated. &lt;/p&gt;

&lt;p&gt;Trace those back to the graph storage layer. Trace that back to the feature-update pipeline. Trace that back to the source system change. &lt;/p&gt;

&lt;p&gt;If a user changes their email, or a supplier misses a shipment, how long does it take for that reality to reach the model? &lt;/p&gt;

&lt;p&gt;If you can't measure the lag, you can't trust the prediction.&lt;/p&gt;

&lt;p&gt;Offline evaluation assumes the model has access to the exact state of the graph at the moment of the event. Production rarely offers that luxury.&lt;/p&gt;

</description>
      <category>dataengineering</category>
      <category>machinelearning</category>
      <category>mlops</category>
      <category>datascience</category>
    </item>
    <item>
      <title>A zero-disruption PodDisruptionBudget can block an AKS upgrade</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Thu, 08 Oct 2026 04:06:50 +0000</pubDate>
      <link>https://dev.to/jlmartel/a-zero-disruption-poddisruptionbudget-can-block-an-aks-upgrade-18h5</link>
      <guid>https://dev.to/jlmartel/a-zero-disruption-poddisruptionbudget-can-block-an-aks-upgrade-18h5</guid>
      <description>&lt;p&gt;Your AKS cluster upgrade is stuck.&lt;/p&gt;

&lt;p&gt;The control plane is waiting for a node to drain. The node is waiting for a pod to terminate. The pod is refusing to move.&lt;/p&gt;

&lt;p&gt;Look at the logs. The workload has a PodDisruptionBudget that allows exactly zero disruptions.&lt;/p&gt;

&lt;p&gt;It is doing exactly what it was told to do. Protect the application at all costs. Even if it means halting the cluster upgrade.&lt;/p&gt;

&lt;p&gt;To upgrade a node, AKS has to drain it. It cordons the node and evicts the pods.&lt;/p&gt;

&lt;p&gt;But the eviction process respects disruption budgets.&lt;/p&gt;

&lt;p&gt;If your deployment has three replicas, and your budget dictates that three must remain available, the eviction fails. The node stays tainted. The upgrade stalls.&lt;/p&gt;

&lt;p&gt;You didn't just configure high availability. You configured a deadlock.&lt;/p&gt;

&lt;p&gt;Replica headroom isn't just a best practice for scaling. It is the physical space required for the control plane to do maintenance.&lt;/p&gt;

&lt;p&gt;Before your next maintenance window, look at your math.&lt;/p&gt;

&lt;p&gt;Check your &lt;code&gt;minAvailable&lt;/code&gt; or &lt;code&gt;maxUnavailable&lt;/code&gt; settings against the actual number of running replicas. &lt;/p&gt;

&lt;p&gt;If you use &lt;code&gt;minAvailable&lt;/code&gt; with a percentage, remember that Kubernetes rounds up. A setting of 100% means zero disruptions allowed. A setting of 90% with three replicas rounds up to three.&lt;/p&gt;

&lt;p&gt;You have to leave room for the scheduler to breathe. Configure these values deliberately, not just to pass a compliance check.&lt;/p&gt;

&lt;p&gt;High availability means surviving a failure. It shouldn't mean blocking your own operations team.&lt;/p&gt;

&lt;p&gt;Your availability policy guarantees the app survives a zone failure. But has anyone tested whether it survives routine maintenance?&lt;/p&gt;

&lt;p&gt;If your zero-disruption configuration blocks your upgrade pipeline, you haven't built a resilient system.&lt;/p&gt;

&lt;p&gt;You have built a hostage situation.&lt;/p&gt;

</description>
      <category>kubernetes</category>
      <category>aws</category>
      <category>cloud</category>
      <category>devops</category>
    </item>
    <item>
      <title>I rebuilt four dead systems with AI. The failures all landed in the same place.</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Thu, 24 Sep 2026 16:55:00 +0000</pubDate>
      <link>https://dev.to/jlmartel/i-rebuilt-four-dead-systems-with-ai-the-failures-all-landed-in-the-same-place-38pa</link>
      <guid>https://dev.to/jlmartel/i-rebuilt-four-dead-systems-with-ai-the-failures-all-landed-in-the-same-place-38pa</guid>
      <description>&lt;p&gt;Four reconstructions, four kinds of missing documentation, and the defects landed in the same place every time.&lt;/p&gt;

&lt;p&gt;In HAL/S it was the characters the scanner destroyed. In the Nautilus simulator it was the parameters Verne never wrote down. In Wikicarta it was the desktop window no headless harness can open. In the compressor it was the part of the input space the corpus never sampled.&lt;/p&gt;

&lt;p&gt;Different projects, different languages, different failure symptoms. One shape: &lt;strong&gt;the defects clustered wherever no automated check could reach, and in every case the code looked fine.&lt;/strong&gt; Tests green, types satisfied, output plausible.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was actually being varied
&lt;/h2&gt;

&lt;p&gt;These weren't four projects that happened to be about old software. They were one experiment with a controlled variable: &lt;em&gt;how degraded is the source of truth the model works from, and can it be caught being wrong?&lt;/em&gt;&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;arm&lt;/th&gt;
&lt;th&gt;source of truth&lt;/th&gt;
&lt;th&gt;oracle&lt;/th&gt;
&lt;th&gt;outcome&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/singular-state/space-shuttle-sdk" rel="noopener noreferrer"&gt;HAL/S&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;degraded formal spec + preserved code&lt;/td&gt;
&lt;td&gt;strong, independent&lt;/td&gt;
&lt;td&gt;faithful reconstruction; 12/12 preserved programs run&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/singular-state/nautilus-sim" rel="noopener noreferrer"&gt;Nautilus&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;a novel&lt;/td&gt;
&lt;td&gt;none — one was built&lt;/td&gt;
&lt;td&gt;5 findings; the legend fails on every axis&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/singular-state/wikicarta" rel="noopener noreferrer"&gt;Wikicarta&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;memory of a product&lt;/td&gt;
&lt;td&gt;none — human judgement&lt;/td&gt;
&lt;td&gt;shippable app; every defect at the runtime seam&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;a href="https://github.com/singular-state/compressor" rel="noopener noreferrer"&gt;compressor&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;behaviour only, key sealed&lt;/td&gt;
&lt;td&gt;strong, opened after freeze&lt;/td&gt;
&lt;td&gt;decoder 100%, encoder 8.3%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The intuition going in was that performance would degrade smoothly as the source got worse. It didn't. Something else predicted the results, and it cut across all four.&lt;/p&gt;

&lt;h2&gt;
  
  
  Determined beats hard
&lt;/h2&gt;

&lt;p&gt;The reconstruction quality tracked whether &lt;strong&gt;the available evidence determines the answer&lt;/strong&gt; — not how difficult the task was.&lt;/p&gt;

&lt;p&gt;The compressor makes this measurable, because both halves ran under identical conditions. The decoder recovered the bitstream format at 100%, because a format is one-way determined: if you want to decode the stream, exactly one answer works. The encoder scored 8.3% on the same format, because its heuristics — match-finder strategy, tie-breaking, greedy versus lazy — are &lt;em&gt;choices&lt;/em&gt; that leave no trace in the decompressed bytes. Same model family, same oracle, same week. A 12× gap, entirely explained by whether the evidence pinned the answer.&lt;/p&gt;

&lt;p&gt;The other three say the same thing in their own terms:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HAL/S.&lt;/strong&gt; The 485 grammar productions were determined by the page images, and the reconstruction got them. The characters the scan physically destroyed were determined by nothing, and no amount of model capability recovers them. That limit is information-theoretic. Better OCR narrows it and never closes it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nautilus.&lt;/strong&gt; The physics is determined; the equations produce the same crush depth for anyone. The parameters Verne omitted are determined by nothing, so submerged endurance ranges across 6 to 211 hours — a 35× spread — and battery capacity across 100×. Those spreads aren't noise around a finding. They &lt;em&gt;are&lt;/em&gt; the finding.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wikicarta.&lt;/strong&gt; Parsing, backoff, cache naming and database ordering are determined by their inputs, and compiled correct on the first or second attempt, consistently. Whether the category wheel feels like Encarta is determined by nothing you can write down, and stayed a human call from start to finish.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The practical version: before handing a legacy task to a model, ask whether the evidence you have actually pins the answer. If it does, expect the model to find it and build a cheap mechanical check to confirm. If it doesn't, expect a confident, plausible, unmarked invention — and note that the model will not tell you which mode it's in, because from the inside they feel identical.&lt;/p&gt;

&lt;h2&gt;
  
  
  A strong oracle is not the protection you think
&lt;/h2&gt;

&lt;p&gt;The compressor was built to demonstrate that a strong oracle masks confabulation. It mostly refuted that.&lt;/p&gt;

&lt;p&gt;The reconstruction's central error — assigning Huffman code lengths by symbol index rather than in heap-extraction order — failed the byte-match test loudly. The oracle caught it. What the oracle &lt;em&gt;did&lt;/em&gt; hide were two mechanisms the corpus never triggered: a block-splitting path that needs inputs larger than 8 KiB to fire, and a match-finder chain cap that needs input more self-similar than anything tested.&lt;/p&gt;

&lt;p&gt;So the masking risk is not a property of the oracle's strength. &lt;strong&gt;It's a property of the input space you sampled.&lt;/strong&gt; A strong checker over a narrow corpus hides precisely the mechanisms your corpus never exercises, and hides them silently, because everything it can see is green.&lt;/p&gt;

&lt;p&gt;Strengthening the checker does nothing for this. Widening the corpus does.&lt;/p&gt;

&lt;p&gt;That generalises past compression. It's the same failure as a test suite with excellent coverage of the paths anyone thought to write a test for.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the human actually contributed
&lt;/h2&gt;

&lt;p&gt;I expected to spend supervision effort reviewing code. Across all four projects, almost none of it went there. The load-bearing human contributions were:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;HAL/S&lt;/strong&gt; — deciding &lt;em&gt;not&lt;/em&gt; to vote between OCR engines, and then reading 485 grammar productions against page images by eye.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Nautilus&lt;/strong&gt; — signing the invariants and making them append-only, so the model could patch the physics but was structurally unable to weaken the checks that judge it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wikicarta&lt;/strong&gt; — being the runtime. Clicking the button in the real window, on the real WebKit build, where the blocking dialog freeze and the cross-realm &lt;code&gt;instanceof&lt;/code&gt; failure lived.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;compressor&lt;/strong&gt; — holding the seal. Not reading the answer key until the reconstruction was frozen at a tag.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Two categories, and neither is code review. One is &lt;strong&gt;supplying what no code can check&lt;/strong&gt; — an eye on a page image, a hand on a mouse. The other is &lt;strong&gt;designing the checks that are possible&lt;/strong&gt; and then putting them structurally out of the model's reach.&lt;/p&gt;

&lt;p&gt;The OCR decision is the sharpest of the four, because it's a judgement call that separates a good result from a worthless one that looks identical. Running three OCR engines and taking the majority is the obvious move and it is wrong: the errors are correlated, so a vote launders error rather than cancelling it, and no fusion can recover a character that no engine produced. Survivable in prose, fatal in a grammar where the metacharacters are the content. Take the vote and you get a clean, plausible, silently corrupt grammar — and every downstream artifact inherits it.&lt;/p&gt;

&lt;p&gt;The Nautilus invariant signing is the most reusable. Twelve invariants live in version control with a manifest checksum only a human re-signs. When the fuzzer finds a violation, the repair workflow requires patching the core and forbids touching the invariant. An agent optimising against a check it can also edit is not being checked.&lt;/p&gt;

&lt;h2&gt;
  
  
  Build the oracle before the artifact
&lt;/h2&gt;

&lt;p&gt;Ranked by how much verification apparatus existed before the real work started, the four projects come out in the same order as how much I trust their conclusions.&lt;/p&gt;

&lt;p&gt;Nautilus had determinism, golden-stream replay and malformed-input fuzzing green in its check harness &lt;strong&gt;before a single equation was written&lt;/strong&gt;, plus a frozen protocol and a rule that every spec parameter traces to either a sourced claim or a documented gap range. Its numbers are the most defensible in the series. The compressor's seal, cold-recall protocol, freeze tag and grading harness are most of that repository; the reconstruction is the small part. HAL/S inherited an independent interpreter, which is a luxury and which is why it is the easiest arm.&lt;/p&gt;

&lt;p&gt;Wikicarta had none of this available, and it shows — its conclusions are qualitative, argued from patterns across features rather than measured. They're still the most immediately useful findings in the series, and they're the least provable. Both things are true because of the same missing oracle.&lt;/p&gt;

&lt;p&gt;There's a specific trap worth naming. A verification harness that finds nothing is indistinguishable from a broken one. Nautilus's 24-hour soak ran 879,643 ticks and found zero invariant violations, which is only meaningful because the harness was first validated against a deliberately poisoned invariant — which it caught, minimised from seven commands to two, and packaged. Test the test.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three results that went the wrong way
&lt;/h2&gt;

&lt;p&gt;A series that only reports confirmations is a demo reel. These four produced three clean refutations, and they were the most informative outputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The compressor predicted the classic overfitting curve — strong on trained data, weak on held-out. It came out inverted, 8% trained against 71% held-out, because the split tracked &lt;em&gt;difficulty&lt;/em&gt; rather than seen-versus-unseen.&lt;/li&gt;
&lt;li&gt;It also predicted the oracle would mask confabulation. The oracle caught the main error; a narrow corpus hid the rest.&lt;/li&gt;
&lt;li&gt;Nautilus's fuzzer was built to find physics exploits. It found none. The only finding in the whole loop was a flaw in the experiment design — the genetic algorithm had been optimising survival rather than performance, because the controller pinned its own objectives.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each of those changed the conclusion. None would have surfaced from reasoning about the problem.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this doesn't reach
&lt;/h2&gt;

&lt;p&gt;Four projects, one operator, no control group. There is no human-only reconstruction to compare against, so nothing here says AI was faster or better than the alternative — only what it did and did not manage. The projects were chosen partly for being interesting, which is a selection effect. Model versions moved during the work. Three of the four are Rust, which is a small language sample.&lt;/p&gt;

&lt;p&gt;The one number I'd most like and don't have is a like-for-like human baseline on the same corpus. That would need someone to transcribe 485 productions and reconstruct an encoder without a model, which is a real cost for a comparison nobody has funded yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd tell someone starting
&lt;/h2&gt;

&lt;p&gt;Handing legacy code to a model works, and it works well enough that the interesting question is where to spend your own attention. Four projects say:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Not on the code.&lt;/strong&gt; The logic was right nearly every time. Spend it at the host, runtime and OS boundary, where every real defect in the desktop project lived and where no harness reached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On knowing which mode you're in.&lt;/strong&gt; Where the evidence determines the answer, expect success and build the cheap mechanical check — production numbering, reference integrity, a verbatim grounding gate. Where it doesn't, expect confident invention, and force it into the open: a divergence log, a gap inventory with plausible ranges, an ambiguity recorded rather than resolved inline. Every one of these projects has a file whose entire job is holding the things that were not derivable, and those files are what make the rest credible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On the checks the model can't edit.&lt;/strong&gt; Sign them, hash them, make weakening them require a human.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;On widening the input space rather than tightening the oracle.&lt;/strong&gt; The gaps that survive strong testing are exactly the ones your corpus never triggers.&lt;/p&gt;

&lt;p&gt;All four repositories are public, with the divergence logs, gap inventories, transcription artifacts and grading harnesses included — those are the parts worth reading, more than the reconstructions themselves.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;&lt;a href="https://github.com/singular-state/space-shuttle-sdk" rel="noopener noreferrer"&gt;space-shuttle-sdk&lt;/a&gt; · &lt;a href="https://github.com/singular-state/nautilus-sim" rel="noopener noreferrer"&gt;nautilus-sim&lt;/a&gt; · &lt;a href="https://github.com/singular-state/wikicarta" rel="noopener noreferrer"&gt;wikicarta&lt;/a&gt; · &lt;a href="https://github.com/singular-state/compressor" rel="noopener noreferrer"&gt;compressor&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>architecture</category>
      <category>legacycode</category>
      <category>testing</category>
    </item>
    <item>
      <title>The Decoder Scored 100%. The Encoder Scored 8.3%.</title>
      <dc:creator>Jean-Luc Martel</dc:creator>
      <pubDate>Tue, 22 Sep 2026 13:08:00 +0000</pubDate>
      <link>https://dev.to/jlmartel/the-decoder-scored-100-the-encoder-scored-83-2i49</link>
      <guid>https://dev.to/jlmartel/the-decoder-scored-100-the-encoder-scored-83-2i49</guid>
      <description>&lt;p&gt;This is the last of four experiments in AI-assisted reconstruction of legacy systems. The first three degraded the source of truth — a scanned spec, then a novel, then a memory of a product. This one removes it completely. &lt;/p&gt;

&lt;p&gt;The target is LHA's &lt;code&gt;-lh5-&lt;/code&gt; method — Haruyasu Yoshizaki's 1988 archiver, LZSS with an 8 KB window plus static Huffman.&lt;/p&gt;

&lt;p&gt;In the experiment, there is no spec and no source, only behaviour, plus an oracle that will answer any question you ask it about outputs. It is the only configuration where the two questions that actually matter become measurable rather than assertable:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Behavioural match is not understanding.&lt;/strong&gt; You can pass every test and hold a wrong model of the mechanism, with untriggered modes waiting in the part of the input space you never sampled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A strong oracle masks confabulation.&lt;/strong&gt; The model fits observed outputs while inventing a plausible-but-wrong internal mechanism, and because the tests keep going green, the error is rewarded rather than exposed.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Measuring those requires reconstructing blind against an answer key you are not allowed to look at. Everything else in the design follows from that one constraint.&lt;/p&gt;

&lt;h2&gt;
  
  
  Choosing something the model hasn't memorised
&lt;/h2&gt;

&lt;p&gt;WinRAR was the instinct and is the wrong specimen. RAR and DEFLATE are heavily memorised, so you'd measure recall rather than reconstruction. RAR5 is far too large. Its decompressor is documented while its compressor has no public ground truth, which is exactly the half you need to grade. And it is live commercial software rather than abandonware.&lt;/p&gt;

&lt;p&gt;LHA satisfies four properties that a lot of candidates fail:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;The original runs&lt;/strong&gt;, so the oracle is unlimited — compress or decompress anything on demand.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The algorithm is public&lt;/strong&gt;, so a real answer key exists and the confabulation can be &lt;em&gt;graded&lt;/em&gt; rather than merely suspected. A never-solved black box demonstrates that the pitfall exists but can never put a number on it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's bounded&lt;/strong&gt; — a few hundred lines, reconstructable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The encoder heuristics are underdetermined by the output.&lt;/strong&gt; Match-finder strategy, minimum match length, tie-breaking, greedy versus lazy matching, block splitting are all encoder-side &lt;em&gt;choices&lt;/em&gt; that leave no trace in the decompressed bytes. That is the confabulation sweet spot.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The general LZSS-plus-Huffman scaffold is in every model's training data. LHA's specific choices are not, and those specifics are what gets measured.&lt;/p&gt;

&lt;h2&gt;
  
  
  The controls
&lt;/h2&gt;

&lt;p&gt;Three, and they're the reason the result is a measurement instead of an anecdote.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The seal.&lt;/strong&gt; The original source sits in the repo, git-ignored, hashed. At unseal, &lt;code&gt;sha256sum -c&lt;/code&gt; verified 107 of 107 files intact, and the manifest's own digest matched the value pinned in the sealing document. Reading it before the reconstruction was frozen is the single act that would have destroyed the experiment, so it was made mechanically detectable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The freeze.&lt;/strong&gt; The reconstruction was tagged and committed &lt;em&gt;before&lt;/em&gt; unsealing, so no artifact could be quietly edited to fit the answer once the answer was visible.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The cold-recall file.&lt;/strong&gt; Before any reconstruction began, a model wrote down everything it already believed about LHA, committed. Without it there is no way to distinguish "derived from behaviour" from "remembered from training," and the whole exercise collapses into a memory test.&lt;/p&gt;

&lt;p&gt;Three models did the work, deliberately split: Gemini 3.1 Pro reconstructed the decoder, Codex/GPT-5 the encoder and the cold-recall baseline, Claude ran the design thread.&lt;/p&gt;

&lt;h2&gt;
  
  
  The decoder: 100%, and it doesn't count for much
&lt;/h2&gt;

&lt;p&gt;Nineteen of nineteen round-trips, exact. The format was fully recovered.&lt;/p&gt;

&lt;p&gt;The sharpest single result lives here. LHA's generic macro defines &lt;code&gt;PBIT 5&lt;/code&gt;, so anyone reading the source would guess a 5-bit position-table field width. The &lt;code&gt;-lh5-&lt;/code&gt; method actually uses &lt;code&gt;pbit = 4&lt;/code&gt; with &lt;code&gt;np = 14&lt;/code&gt;. The reconstruction recovered the method-specific 4 purely from round-trip behaviour, against a constant that would have misled a source-reader.&lt;/p&gt;

&lt;p&gt;But checking that win against the cold-recall file deflates most of the rest. Nearly every format fact the decoder "recovered" was already in the model's committed prior: the 8 KB window, the threshold of 3 and max match of 256, the roughly 510-symbol alphabet, the 19-entry precode with its 0/1/2 zero-run encoding, the 16-bit block count, the position-slot decode. The 100% credits &lt;strong&gt;prior knowledge&lt;/strong&gt;, not derivation.&lt;/p&gt;

&lt;p&gt;Which is unsurprising, because the format is one-way determined. If you want to decode the bitstream, there is exactly one answer, and both memory and derivation converge on it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The encoder: 8.3%, and it counts for everything
&lt;/h2&gt;

&lt;p&gt;Byte-match against real &lt;code&gt;lha&lt;/code&gt;, on a fresh-seed corpus the reconstruction had never seen:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;input class&lt;/th&gt;
&lt;th&gt;trained on?&lt;/th&gt;
&lt;th&gt;byte-match&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;text&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;0/3 (0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;source&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;0/3 (0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;structured&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;0/3 (0%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;repetitive&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;1/3 (33%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;random&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;2/2 (100%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;edges&lt;/td&gt;
&lt;td&gt;no&lt;/td&gt;
&lt;td&gt;3/5 (60%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;trained total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;1/12 (8.3%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;held-out total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;5/7 (71.4%)&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;A perfect decoder coexisting with an 8% encoder is &lt;em&gt;match ≠ understanding&lt;/em&gt; in one line. The format was recoverable. The encoder's &lt;strong&gt;choices&lt;/strong&gt; were not.&lt;/p&gt;

&lt;h2&gt;
  
  
  The hypothesis was wrong, in a useful direction
&lt;/h2&gt;

&lt;p&gt;The plan predicted the classic overfitting shape: high scores on trained classes, dropping on held-out ones. The data inverted it. Held-out scored 71%, trained scored 8%.&lt;/p&gt;

&lt;p&gt;The split tracks &lt;strong&gt;difficulty, not seen versus unseen&lt;/strong&gt;. The trained classes are exactly the ones that exercise real &lt;code&gt;-lh5-&lt;/code&gt; compression; the held-out set is dominated by incompressible and trivial inputs that fall through to stored mode or hit trivial paths. Random data scores 100% because no compression heuristics ever run.&lt;/p&gt;

&lt;p&gt;So the honest statement isn't "an 8% to 71% generalisation gap." It's this: &lt;strong&gt;byte-identity is reached only where the encoder heuristics never fire. Wherever genuine compression happens, the reconstruction diverges.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;The rates were identical on the trained-seed and fresh-seed corpora, so the divergence is systematic rather than instance overfitting.&lt;/p&gt;

&lt;h2&gt;
  
  
  The confabulation catalogue
&lt;/h2&gt;

&lt;p&gt;Every inferred encoder decision, graded against the now-visible source:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;decision&lt;/th&gt;
&lt;th&gt;inferred&lt;/th&gt;
&lt;th&gt;actual&lt;/th&gt;
&lt;th&gt;verdict&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;longest-match search&lt;/td&gt;
&lt;td&gt;yes&lt;/td&gt;
&lt;td&gt;matches&lt;/td&gt;
&lt;td&gt;correct&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tie-break among equal lengths&lt;/td&gt;
&lt;td&gt;nearest offset&lt;/td&gt;
&lt;td&gt;chain walked recent→old, updates only on strictly longer&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;derived correct&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;greedy vs lazy&lt;/td&gt;
&lt;td&gt;one-step lazy&lt;/td&gt;
&lt;td&gt;emits literal if next match is strictly longer&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;derived correct&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;block splitting&lt;/td&gt;
&lt;td&gt;"one block; unknown for larger"&lt;/td&gt;
&lt;td&gt;flush at 32 KB buffer threshold&lt;/td&gt;
&lt;td&gt;never triggered — correctly scoped&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;match-finder chain cap&lt;/td&gt;
&lt;td&gt;not modelled&lt;/td&gt;
&lt;td&gt;a hard limit makes the finder &lt;strong&gt;non-exhaustive&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;wrong, and it passed anyway&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Huffman code-length assignment&lt;/td&gt;
&lt;td&gt;canonical, tie order unmatched&lt;/td&gt;
&lt;td&gt;lengths assigned in &lt;strong&gt;heap-extraction order&lt;/strong&gt;
&lt;/td&gt;
&lt;td&gt;visible divergence&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two of those are clean wins. Nearest-offset tie-breaking and one-step lazy matching are both marked as unknown in the cold-recall file — "do not know whether &lt;code&gt;lha -lh5-&lt;/code&gt; does this," "not a specific memory." Derived from black-box behaviour, with documented prior ignorance.&lt;/p&gt;

&lt;p&gt;There's a caveat that inverts on inspection. Codex wrote both the recall file and the encoder, which normally weakens an independence claim. Here it strengthens it: a model cannot conceal prior knowledge from its own committed statement of ignorance. Deriving both heuristics after recording that it didn't know them is the cleanest evidence in the run.&lt;/p&gt;

&lt;p&gt;The failure is concentrated in one place. LHA builds a frequency heap and assigns code lengths in the order symbols leave the heap, governed by the exact comparison semantics of its sift-down — not by symbol value. Textbook canonical Huffman breaks ties by symbol index, which produces different lengths for equal-frequency symbols, which cascades through the entire bitstream. The reconstruction localised the problem correctly and even tried a heap hypothesis, but never replicated the precise sift order.&lt;/p&gt;

&lt;h2&gt;
  
  
  The second hypothesis was also wrong, and that's the finding
&lt;/h2&gt;

&lt;p&gt;The project was built to show that a strong oracle masks confabulation. It mostly didn't.&lt;/p&gt;

&lt;p&gt;The core error — the Huffman ordering — is a &lt;strong&gt;visible&lt;/strong&gt; divergence. It fails the byte test loudly. The oracle caught it, exactly as an oracle should. Only two gaps were genuinely hidden: the never-triggered block split, and the unmodelled chain cap, which is latent because the corpus never produced input self-similar enough to expose it.&lt;/p&gt;

&lt;p&gt;Both are artifacts of a corpus that tops out at 8 KiB.&lt;/p&gt;

&lt;p&gt;So the masking risk isn't a property of the oracle's strength. &lt;strong&gt;It's a property of the input space you sampled.&lt;/strong&gt; A strong oracle over a narrow corpus hides exactly the mechanisms your corpus never triggers, and it hides them silently, because everything it &lt;em&gt;can&lt;/em&gt; see is green. Making the oracle stronger doesn't help. Making the corpus wider does.&lt;/p&gt;

&lt;p&gt;That reframing is more useful than the result I expected to get, and I would not have got it by reasoning about it. It needed the sealed envelope.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it cost to know any of this
&lt;/h2&gt;

&lt;p&gt;Final tally across the encoder decisions: six recalled-correct, five derived-correct, one derived-wrong-but-passed, two never-triggered, one visible divergence.&lt;/p&gt;

&lt;p&gt;The infrastructure to produce those thirteen data points — the pinned oracle, the deterministic corpus generator, the seal and its manifest, the cold-recall protocol, the freeze tag, the grading harness — is most of the repository. The reconstruction itself is the small part.&lt;/p&gt;

&lt;p&gt;That ratio is the practical lesson. If you want to know whether an AI-assisted reconstruction actually understood the system, rather than whether it passed, you have to build the apparatus that can tell you, and you have to build it before you start. Afterwards, every artifact is contaminated by the answer.&lt;/p&gt;

&lt;p&gt;One thing remains open. A fully independent contamination estimate needs a repeat where the recall file and the encoder come from different model families, closing the theoretical shared-prior channel that the current caveat only argues around.&lt;/p&gt;

&lt;p&gt;Next, the wrap-up: what four projects across four degrees of source degradation say about where the current generation of models actually fails, and why the failures kept landing in the same place.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Code: &lt;a href="https://github.com/singular-state/compressor" rel="noopener noreferrer"&gt;https://github.com/singular-state/compressor&lt;/a&gt;. LHA was written by Haruyasu Yoshizaki in 1988; the original source is used here only as a private grading key, held out during reconstruction and consulted once. The reconstruction is clean-room, so the measured gap between "passes the oracle" and "matches the source" is legitimate.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>computerscience</category>
      <category>machinelearning</category>
      <category>softwareengineering</category>
    </item>
  </channel>
</rss>
