<?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: Mudasir Blogger</title>
    <description>The latest articles on DEV Community by Mudasir Blogger (@mudasir_blogger).</description>
    <link>https://dev.to/mudasir_blogger</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%2F4243929%2F4eca5e88-c2dc-45e5-b635-b3dd90ae8c1a.png</url>
      <title>DEV Community: Mudasir Blogger</title>
      <link>https://dev.to/mudasir_blogger</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mudasir_blogger"/>
    <language>en</language>
    <item>
      <title>What a data pipeline actually does, with 60 lines of Python you can run</title>
      <dc:creator>Mudasir Blogger</dc:creator>
      <pubDate>Sat, 14 Nov 2026 06:17:46 +0000</pubDate>
      <link>https://dev.to/mudasir_blogger/what-a-data-pipeline-actually-does-with-60-lines-of-python-you-can-run-163n</link>
      <guid>https://dev.to/mudasir_blogger/what-a-data-pipeline-actually-does-with-60-lines-of-python-you-can-run-163n</guid>
      <description>&lt;p&gt;A data pipeline moves data from the place it is produced to the place it gets used, and changes its shape on the way. That is the whole idea. Most explanations stop at a diagram with three boxes labelled extract, transform, load, which tells you nothing about what the code looks like or why it breaks.&lt;/p&gt;

&lt;p&gt;So here is a pipeline you can actually run. About 60 lines of Python, no framework, no cloud account. Then I will go through what it deliberately does not handle, because that gap is where most of the real work lives.&lt;/p&gt;

&lt;p&gt;I build these for a living at Brickclay, mostly on client data platforms, so the failure list near the end is not theoretical. It is what has actually gone wrong on jobs I had to fix.&lt;/p&gt;

&lt;h2&gt;
  
  
  The input, which is messy on purpose
&lt;/h2&gt;

&lt;p&gt;Save this as &lt;code&gt;orders.csv&lt;/code&gt;. It has five rows and two of them are broken, which is roughly the ratio you get from a real export.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;id,customer,amount_cents,placed_at
A-1001,Ada@Example.com ,124900,03/04/2026
A-1002,  Grace@example.com,8050,03/05/2026
A-1003,lin@example.com,not_a_number,03/05/2026
A-1004,hopper@example.com,45000,2026-03-06
A-1005,ken@example.com,99900,03/07/2026
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Row A-1003 has a non-numeric amount. Row A-1004 has a date in a different format from the rest. The first two rows have whitespace and inconsistent email casing. Nobody writes a CSV like this on purpose. You get it when three different systems have written to the same file over two years.&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;logging&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;datetime&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timezone&lt;/span&gt;
&lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;pathlib&lt;/span&gt; &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;

&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;basicConfig&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;level&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="no"&gt;INFO&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;format&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"%(levelname)s %(message)s"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="no"&gt;SCHEMA&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"""
CREATE TABLE IF NOT EXISTS orders (
    order_id   TEXT PRIMARY KEY,
    customer   TEXT NOT NULL,
    amount_usd REAL NOT NULL,
    placed_at  TEXT NOT NULL,
    loaded_at  TEXT NOT NULL
)
"""&lt;/span&gt;

&lt;span class="no"&gt;UPSERT&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"""
INSERT INTO orders VALUES (:order_id, :customer, :amount_usd, :placed_at, :loaded_at)
ON CONFLICT(order_id) DO UPDATE SET
    customer   = excluded.customer,
    amount_usd = excluded.amount_usd,
    placed_at  = excluded.placed_at,
    loaded_at  = excluded.loaded_at
"""&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;newline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;encoding&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;fh&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;yield&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="n"&gt;csv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;DictReader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fh&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;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="s2"&gt;"order_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"customer"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;strip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"amount_usd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;int&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"amount_cents"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"placed_at"&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;strptime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"placed_at"&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"%m/%d/%Y"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;date&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s2"&gt;"loaded_at"&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;now&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timezone&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;utc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isoformat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;timespec&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"seconds"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;


&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;db_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sqlite3&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;db_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;SCHEMA&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rejected&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;row&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;extract&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;csv_path&lt;/span&gt;&lt;span class="p"&gt;)&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;clean&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="nf"&gt;transform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;KeyError&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;ValueError&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;rejected&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="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;no id&amp;gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kt"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;exc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;executemany&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="no"&gt;UPSERT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;commit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"loaded %d rows, rejected %d"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rejected&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;why&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="n"&gt;rejected&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;logging&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;warning&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"rejected %s: %s"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;order_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;why&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;


&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;__name__&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"__main__"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"orders.csv"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;Path&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"orders.db"&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;Run it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;INFO loaded 3 rows, rejected 2
WARNING rejected A-1003: invalid literal for int() with base 10: 'not_a_number'
WARNING rejected A-1004: time data '2026-03-06' does not match format '%m/%d/%Y'
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three rows landed. Two were rejected with the reason attached to the order id, so you can go and ask someone about A-1003 instead of guessing which row died.&lt;/p&gt;

&lt;p&gt;Four things are happening in those 60 lines, and each one maps to a decision you will have to make again on a larger pipeline.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;extract&lt;/code&gt; is a generator. It yields one row at a time instead of loading the file into memory. On a 4 GB export this is the difference between a job that runs and a job the kernel kills.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;transform&lt;/code&gt; converts rather than repairs. Cents become dollars, the US date format becomes an ISO date, emails get lowercased and trimmed. It raises on anything it cannot convert instead of writing a zero or a null. A null that should have been 1249.00 will survive every test you write and surface three months later in a revenue report.&lt;/p&gt;

&lt;p&gt;The rejection list is separate from the load. Bad rows do not stop good rows, and they do not disappear either. This single choice is the difference between a pipeline people trust and one people re-run by hand every morning.&lt;/p&gt;

&lt;p&gt;The load is an upsert, keyed on &lt;code&gt;order_id&lt;/code&gt;. That is what makes the job safe to run twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Run it twice, then count the rows
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdpg1waw3p7e27skh38ic.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fdpg1waw3p7e27skh38ic.png" alt="Two runs of the pipeline in a terminal, loading three rows and rejecting two each time, with the row count still three" width="799" height="313"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Still three rows after two runs. If &lt;code&gt;UPSERT&lt;/code&gt; were a plain &lt;code&gt;INSERT&lt;/code&gt;, the second run would either crash on the primary key or, with a table that has no key, quietly double your revenue.&lt;/p&gt;

&lt;p&gt;This property has a name worth knowing: idempotency. Running the job twice produces the same end state as running it once. You want it because you will re-run jobs. The network drops, a credential expires, someone fixes the source file at 2am and asks you to load it again. A pipeline you cannot safely re-run becomes a pipeline somebody has to babysit.&lt;/p&gt;

&lt;h2&gt;
  
  
  What these 60 lines do not handle
&lt;/h2&gt;

&lt;p&gt;This is the honest part. The script above is a pipeline, but it is the first ten percent of one.&lt;/p&gt;

&lt;p&gt;Schema drift. Someone renames &lt;code&gt;amount_cents&lt;/code&gt; to &lt;code&gt;amount&lt;/code&gt; and every row rejects at once. The fix is to validate the header against an expected contract and fail the whole run with a clear message, rather than reporting 40,000 individual rejections.&lt;/p&gt;

&lt;p&gt;Partial failure and retries. If the load dies halfway through a large batch, &lt;code&gt;executemany&lt;/code&gt; inside one transaction rolls back and you are clean. Start writing in chunks and committing per chunk, which you will do for memory reasons, and you now need to track which chunks landed.&lt;/p&gt;

&lt;p&gt;Incremental loads. This script reads the whole file every time. At real volume you need a watermark, usually the maximum &lt;code&gt;placed_at&lt;/code&gt; or an updated timestamp from the source, so each run only picks up what is new. Then you need a way to force a full reload when the watermark logic itself is wrong.&lt;/p&gt;

&lt;p&gt;Late-arriving and corrected data. An order placed on the 4th can arrive on the 9th with a different amount. The upsert handles the correction. Any report you already sent out does not.&lt;/p&gt;

&lt;p&gt;Observability. Right now the only record that the job ran is a log line in a terminal. The minimum useful version is a run table: start time, end time, rows in, rows loaded, rows rejected. Without it you cannot answer whether today's numbers are low because sales were low or because the job failed.&lt;/p&gt;

&lt;p&gt;Orchestration. Something has to decide when this runs, what runs after it, and who gets told when it fails. That is what Airflow, Dagster and Prefect exist for, and it is also why you should not reach for them on day one.&lt;/p&gt;

&lt;h2&gt;
  
  
  The three failures that actually show up
&lt;/h2&gt;

&lt;p&gt;Across the data platform work I do at Brickclay, three problems come up far more often than any argument about tooling.&lt;/p&gt;

&lt;p&gt;The silent partial load. The job reports success, loads 80 percent of the rows, and nobody notices for a week. Caused by swallowing exceptions in a loop. Fixed by counting rows in and rows out and failing when the difference is not explained by your rejection list.&lt;/p&gt;

&lt;p&gt;The timezone that was never a timezone. A source writes local timestamps with no offset, the pipeline stores them as UTC, and every daily total is wrong by the number of hours in the shift. This one is expensive because the data looks completely reasonable.&lt;/p&gt;

&lt;p&gt;The pipeline nobody can re-run. Usually a plain insert, or a transform that reads the current date, so re-running yesterday's job produces different output than it did yesterday. Notice that &lt;code&gt;loaded_at&lt;/code&gt; in the script above has exactly this smell. It records when the row was written, which is fine for an audit trail, but it means the output bytes differ between runs. Keep that kind of field out of anything you compare run to run.&lt;/p&gt;

&lt;p&gt;If you are starting one of these from scratch, build the rejection list and the run table before you add a scheduler. The scheduler makes the job automatic. Those two make it trustable, and on every project I have worked on, trust was the part that took longer.&lt;/p&gt;

&lt;p&gt;If you have hit a failure mode I did not list, put it in the comments, I collect these.&lt;/p&gt;

</description>
      <category>python</category>
      <category>dataengineering</category>
      <category>sqlite</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
