<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Rich Polo's Blog]]></title><description><![CDATA[Rich Polo's Blog]]></description><link>https://rich202603.hashnode.dev</link><generator>RSS for Node</generator><lastBuildDate>Mon, 02 Nov 2026 10:57:06 GMT</lastBuildDate><atom:link href="https://rich202603.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Build a CSV Expense Summarizer in Python (No Pandas Required)]]></title><description><![CDATA[Spreadsheets are great until you want the same summary every month. Let's write a small, dependency-free Python CLI that reads a CSV of expenses and prints…]]></description><link>https://rich202603.hashnode.dev/build-a-csv-expense-summarizer-in-python-no-pandas-required</link><guid isPermaLink="true">https://rich202603.hashnode.dev/build-a-csv-expense-summarizer-in-python-no-pandas-required</guid><category><![CDATA[Python]]></category><category><![CDATA[cli]]></category><category><![CDATA[Beginner Developers]]></category><dc:creator><![CDATA[Rich Polo]]></dc:creator><pubDate>Fri, 23 Oct 2026 16:48:09 GMT</pubDate><content:encoded><![CDATA[<p>Spreadsheets are great until you want the same summary every month. Let's write a small, dependency-free Python CLI that reads a CSV of expenses and prints totals per category and per month.</p>
<h2>Input format</h2>
<p><code>expenses.csv</code>:</p>
<pre><code class="language-csv">date,category,amount
2026-09-02,food,12.50
2026-09-03,transport,3.20
2026-09-15,food,20.00
2026-10-01,rent,450.00
2026-10-04,food,8.75
</code></pre>
<h2>The script</h2>
<p><code>summarize.py</code>:</p>
<pre><code class="language-python">import argparse
import csv
from collections import defaultdict
from datetime import datetime
from decimal import Decimal, InvalidOperation


def load_rows(path):
    with open(path, newline="", encoding="utf-8") as f:
        for lineno, row in enumerate(csv.DictReader(f), start=2):
            try:
                date = datetime.strptime(row["date"], "%Y-%m-%d").date()
                amount = Decimal(row["amount"])
            except (ValueError, InvalidOperation, KeyError) as e:
                print(f"skipping line {lineno}: {e}")
                continue
            yield date, row["category"].strip().lower(), amount


def summarize(rows):
    by_cat = defaultdict(Decimal)
    by_month = defaultdict(Decimal)
    for date, cat, amount in rows:
        by_cat[cat] += amount
        by_month[date.strftime("%Y-%m")] += amount
    return by_cat, by_month


def print_table(title, data):
    print(f"\n{title}")
    width = max((len(k) for k in data), default=0)
    for key, value in sorted(data.items(), key=lambda kv: -kv[1]):
        print(f"  {key:&lt;{width}}  {value:&gt;10.2f}")
    print(f"  {'TOTAL':&lt;{width}}  {sum(data.values()):&gt;10.2f}")


def main():
    parser = argparse.ArgumentParser(description="Summarize an expense CSV")
    parser.add_argument("csv_path")
    args = parser.parse_args()
    by_cat, by_month = summarize(load_rows(args.csv_path))
    print_table("By category", by_cat)
    print_table("By month", by_month)


if __name__ == "__main__":
    main()
</code></pre>
<h2>Run it</h2>
<pre><code class="language-bash">python3 summarize.py expenses.csv
</code></pre>
<p>Output:</p>
<pre><code>By category
  rent           450.00
  food            41.25
  transport        3.20
  TOTAL          494.45

By month
  2026-10      458.75
  2026-09       35.70
  TOTAL        494.45
</code></pre>
<h2>Why these choices</h2>
<ul>
<li><strong><code>Decimal</code> instead of <code>float</code></strong>: <code>0.1 + 0.2</code> is not <code>0.3</code> in floats. For money, use <code>Decimal</code>.</li>
<li><strong>Generator (<code>yield</code>)</strong>: rows stream one by one, so a huge file doesn't fill memory.</li>
<li><strong>Skip bad lines, don't crash</strong>: real exports always have a broken row somewhere.</li>
</ul>
<h2>Next steps</h2>
<p>Add a <code>--month 2026-10</code> filter, or export the result to JSON with <code>json.dumps</code>. Both are under 10 lines.</p>
]]></content:encoded></item></channel></rss>