<?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: Annamyrat Hallyyev</title>
    <description>The latest articles on DEV Community by Annamyrat Hallyyev (@kerimff7rgb).</description>
    <link>https://dev.to/kerimff7rgb</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%2F4131553%2Fe8696198-0386-495e-b26f-eeb3243831ec.jpeg</url>
      <title>DEV Community: Annamyrat Hallyyev</title>
      <link>https://dev.to/kerimff7rgb</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kerimff7rgb"/>
    <language>en</language>
    <item>
      <title>pandas turned my money column into dates, and 123 tests said nothing</title>
      <dc:creator>Annamyrat Hallyyev</dc:creator>
      <pubDate>Fri, 18 Sep 2026 13:15:34 +0000</pubDate>
      <link>https://dev.to/kerimff7rgb/pandas-turned-my-money-column-into-dates-and-123-tests-said-nothing-3gbh</link>
      <guid>https://dev.to/kerimff7rgb/pandas-turned-my-money-column-into-dates-and-123-tests-said-nothing-3gbh</guid>
      <description>&lt;p&gt;I sell a small Python tool that takes a folder of messy exports — CSV, TSV, Excel — and turns them into one clean workbook. Its whole pitch is that nothing is changed silently: every alteration is written to a Summary sheet so the client can check the work instead of trusting it.&lt;/p&gt;

&lt;p&gt;Two days ago I found out it had been silently destroying money columns. Then I found the same defect in two more tools. Then a different defect, in all three. This is what happened, in order, because the order is the lesson.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day one: &lt;code&gt;4141.98&lt;/code&gt; becomes the year 4141
&lt;/h2&gt;

&lt;p&gt;The tool guesses column types. A column of text that looks like dates becomes dates, text that looks like numbers becomes numbers. To decide whether a value &lt;em&gt;might&lt;/em&gt; be a date before spending a parser on it, there was a cheap regex:&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;DATE_HINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\d[-/.]\d|\d{1,2}:\d{2}|[A-Za-z]{3,}&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;Digit, separator, digit. &lt;code&gt;03.02.2026&lt;/code&gt; matches. So does &lt;code&gt;4141.98&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;That alone would be harmless, because the parser should reject &lt;code&gt;4141.98&lt;/code&gt; as a date. It doesn't. &lt;code&gt;pd.to_datetime("4141.98", format="mixed")&lt;/code&gt; happily returns &lt;strong&gt;the year 4141&lt;/strong&gt;. And a column that has already been read as float — &lt;code&gt;4821.55&lt;/code&gt; — goes through a different path and comes out as 1 January 1970 plus 4821 nanoseconds.&lt;/p&gt;

&lt;p&gt;Nothing crashed. The workbook opened. The Summary said "columns to dates: 1". The invoice amounts were gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;How it was found:&lt;/strong&gt; not by the 123 tests, not by the 52 adversarial inputs, not by the 600,000-row performance run. A client-facing demo needed fresh sample data, and this time I wrote the amounts with &lt;code&gt;"{:.2f}"&lt;/code&gt; — no thousands separator. Every previous fixture had written &lt;code&gt;4,141.98&lt;/code&gt;, and the comma breaks the date heuristic before it starts. A hundred and twenty-three tests had been checking the same shape of number over and over.&lt;/p&gt;

&lt;p&gt;The fix: a date written in digits needs &lt;em&gt;two&lt;/em&gt; separators, and a column of bare numbers is vetoed before any parsing is attempted.&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;DATE_HINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\d[-/.]\d+[-/.]\d|\d{1,2}:\d{2}|[A-Za-z]{3,}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;BARE_NUMBER&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^[-+]?[\d\s]*[\d][\s\d]*(?:[.,]\d+)?$&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;Fourteen new tests. Done, I thought.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day two: two separators are necessary but not sufficient
&lt;/h2&gt;

&lt;p&gt;The next evening I was auditing a second tool — a web scraper that writes the same kind of workbook — and tried a column of product sizes: &lt;code&gt;10.5.2&lt;/code&gt;, &lt;code&gt;11.5.3&lt;/code&gt;, &lt;code&gt;12.5.4&lt;/code&gt;.&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Widget A&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1234.56&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2002&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten May 2002. Two separators, exactly as required. The fix from day one was correct and useless here.&lt;/p&gt;

&lt;p&gt;Worse: it was unstable. Article numbers like &lt;code&gt;10.20.30&lt;/code&gt;, &lt;code&gt;11.20.31&lt;/code&gt;, &lt;code&gt;12.20.32&lt;/code&gt; — three of them became dates. Add a fourth, &lt;code&gt;13.20.33&lt;/code&gt;, and month 13 fails to parse, the share of parseable values drops below the threshold, and the whole column stays text. &lt;strong&gt;Same site, same selector: scrape three items and lose the column, scrape four and keep it.&lt;/strong&gt; No test will ever see a bug that depends on how many rows the page had that day.&lt;/p&gt;

&lt;p&gt;The rule that actually separates a date from a size is not the number of separators. It is the year: written with dots, a date carries a four-digit year. &lt;code&gt;03.02.2026&lt;/code&gt; is a date. &lt;code&gt;10.5.2&lt;/code&gt; is a size. Slashes and dashes keep the two-digit year, because &lt;code&gt;12/31/26&lt;/code&gt; is how half the world writes 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="n"&gt;DATE_HINT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;\d{1,2}[-/]\d{1,2}[-/]\d{2,4}(?!\d)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;      &lt;span class="c1"&gt;# 31/12/26, 31-12-2026
&lt;/span&gt;    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|\d{4}[-/.]\d{1,2}[-/.]\d{1,2}(?!\d)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;     &lt;span class="c1"&gt;# 2026-01-12
&lt;/span&gt;    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|\d{1,2}\.\d{1,2}\.\d{4}(?!\d)&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;           &lt;span class="c1"&gt;# 03.02.2026 - dots need a full year
&lt;/span&gt;    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|\d{1,2}:\d{2}&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
    &lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;|[A-Za-z]{3,}&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;h2&gt;
  
  
  The part that changed how I work
&lt;/h2&gt;

&lt;p&gt;The scraper had the &lt;em&gt;original&lt;/em&gt; one-separator regex. So did a third tool that pulls JSON from APIs. All three had been written from the same template, and a defect in the template had been copied faithfully into every copy.&lt;/p&gt;

&lt;p&gt;Once I looked, it wasn't just the regex:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;defect&lt;/th&gt;
&lt;th&gt;found in&lt;/th&gt;
&lt;th&gt;also present in&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;one-separator date heuristic&lt;/td&gt;
&lt;td&gt;CSV cleaner&lt;/td&gt;
&lt;td&gt;scraper, API tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;requirements.txt&lt;/code&gt; says &lt;code&gt;pandas&amp;gt;=1.5&lt;/code&gt;; code uses &lt;code&gt;format="mixed"&lt;/code&gt;, which needs 2.0&lt;/td&gt;
&lt;td&gt;CSV cleaner&lt;/td&gt;
&lt;td&gt;scraper, API tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;float(Retry-After)&lt;/code&gt; — crashes when the header is an HTTP date, which the spec allows&lt;/td&gt;
&lt;td&gt;scraper&lt;/td&gt;
&lt;td&gt;API tool&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;10.5.2&lt;/code&gt; read as a date&lt;/td&gt;
&lt;td&gt;scraper&lt;/td&gt;
&lt;td&gt;CSV cleaner&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four times in twenty-four hours, the same sentence: &lt;em&gt;found in one, present in the others.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;The rule I now follow, written on the wall: &lt;strong&gt;when you find a defect in one tool, check every sibling the same day.&lt;/strong&gt; Not next sprint. The same day, while you still remember exactly what the defect looks like.&lt;/p&gt;




&lt;h2&gt;
  
  
  Day three: not a date bug at all
&lt;/h2&gt;

&lt;p&gt;With the date rule fixed in all three tools, I ran the API tool against a real public API and opened the result to look at it. The phone column read:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;8,025,285,988
8,028,575,318
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Money formatting on a telephone number. The API had sent &lt;code&gt;"8025285988"&lt;/code&gt; as a string. The tool saw ten digits, called it a number, and Excel added the commas. Any leading zero would have been dropped for good.&lt;/p&gt;

&lt;p&gt;Then I checked the CSV cleaner with a column of postal codes:&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;05401&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;05672&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;05344&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;  &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;  &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;5401.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5672.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;5344.0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The leading zero is gone and it is not coming back. Of everything in this post, that is the one you cannot recover from.&lt;/p&gt;

&lt;p&gt;The guard is two cheap signals. A value that starts with &lt;code&gt;0&lt;/code&gt; and has more digits after it is an identifier at any length — no quantity is written &lt;code&gt;05401&lt;/code&gt;. And a long unbroken run of digits where every value in the column is &lt;strong&gt;the same width&lt;/strong&gt; is an identifier too: phones, accounts and barcodes are fixed-width, populations and amounts are not.&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;LONG_DIGITS&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^\d{7,}$&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;LEADING_ZERO&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;re&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;r&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;^0\d+$&lt;/span&gt;&lt;span class="sh"&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;is_identifier&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.8&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;vals&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="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LEADING_ZERO&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))).&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;longs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vals&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="k"&gt;lambda&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;bool&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;LONG_DIGITS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;longs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;threshold&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;widths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;zip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vals&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;longs&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;k&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;widths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="bp"&gt;False&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In the API tool there is a third signal, and it is the strongest: the source sent the digits &lt;strong&gt;as a string&lt;/strong&gt;. A JSON number arrives as a number and never reaches this code. If the API bothered to quote it, it is telling you it is not a quantity.&lt;/p&gt;




&lt;h2&gt;
  
  
  What the tests were doing all this time
&lt;/h2&gt;

&lt;p&gt;Passing. All of them. 123, then 137, then 150.&lt;/p&gt;

&lt;p&gt;None of these defects crashed. Every one finished, wrote a workbook that looked correct, and was wrong. That is the only kind of failure that reaches a user, and it is the kind a test suite is worst at, because a test checks what you thought of, and you can only think of what you have already seen.&lt;/p&gt;

&lt;p&gt;Three of the four were found by &lt;strong&gt;looking at the output&lt;/strong&gt; — a demo image that happened to use different numbers, a preview of a listing where the phones had commas, a summary sheet opened in LibreOffice where a label was cut in half. None of those are tests. All of them are now.&lt;/p&gt;

&lt;p&gt;The suite is at 167 checks, 35 of them marked &lt;code&gt;REGRESSION&lt;/code&gt; — each one a bug that was in the code and shipped nothing, because it was caught. I keep the count honest by counting them with a command, after discovering that a number I had typed into the README from memory was wrong by two.&lt;/p&gt;




&lt;h2&gt;
  
  
  If you type-coerce columns, three questions
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Does your date heuristic accept a bare decimal?&lt;/strong&gt; Try &lt;code&gt;4141.98&lt;/code&gt;. Try a float &lt;code&gt;4821.55&lt;/code&gt;. Try &lt;code&gt;10.5.2&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;What happens to &lt;code&gt;05401&lt;/code&gt;?&lt;/strong&gt; If the answer is 5401, you are destroying postal codes, account numbers and product codes, and nobody will tell you.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Do your fixtures all write numbers the same way?&lt;/strong&gt; Mine did. A hundred and twenty-three tests, one shape of number.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the one that isn't about code: &lt;strong&gt;do you have a sibling tool built from the same template?&lt;/strong&gt; Go and check it. Today.&lt;/p&gt;




&lt;p&gt;The tool, its tests and the adversarial suite are public: &lt;a href="https://github.com/kerimff7-rgb/csv-excel-cleaner" rel="noopener noreferrer"&gt;github.com/kerimff7-rgb/csv-excel-cleaner&lt;/a&gt;. Every number in this post can be reproduced from it. If you find one that can't, I would like to know.&lt;/p&gt;

</description>
      <category>python</category>
      <category>pandas</category>
      <category>testing</category>
      <category>datascience</category>
    </item>
  </channel>
</rss>
