৬০ লাইনে Python expense tracker CLI বানান (test সহ)

ছোট প্রজেক্ট যা argparse, ফাইল সামলানো, dictionary ও testing শেখায়। নিচের কোড প্রকাশের আগে চালিয়ে পরীক্ষা করা হয়েছে।

expense তালিকা ও সারাংশ দেখানো একটি terminal

এই প্রজেক্ট এক সন্ধ্যায় শেষ করার মতো ছোট, অথচ আসল কোডে যা ব্যবহার করবেন তা শেখায়: command-line আর্গুমেন্ট, ফাইল পড়া ও লেখা, dictionary, error সামলানো এবং unit test। এটি শুধু Python-এর standard library ব্যবহার করে, তাই কিছু install করতে হয় না। কোড ও নিচের নমুনা আউটপুট এই লেখা প্রকাশের আগে Python 3-এ চালানো হয়েছে।

আমরা কী বানাচ্ছি

তিনটি কমান্ডের একটি command-line টুল:

  • একটি খরচ add করা (বিভাগ, পরিমাণ, ঐচ্ছিক নোট)।
  • সব খরচ list করা।
  • বিভাগ অনুযায়ী যোগফল summary, বড়টি আগে।

Data সাধারণ CSV ফাইলে থাকে, তাই spreadsheet-এও খুলতে পারবেন।

কোড

এটি expenses.py নামে সংরক্ষণ করুন:

#!/usr/bin/env python3
"""A tiny expense tracker: add expenses, list them and show totals per category."""
import argparse
import csv
from collections import defaultdict
from datetime import date
from pathlib import Path

FILE = Path("expenses.csv")
FIELDS = ["date", "category", "amount", "note"]


def load():
    if not FILE.exists():
        return []
    with FILE.open(newline="", encoding="utf-8") as f:
        return list(csv.DictReader(f))


def add(category, amount, note=""):
    is_new = not FILE.exists()
    with FILE.open("a", newline="", encoding="utf-8") as f:
        writer = csv.DictWriter(f, fieldnames=FIELDS)
        if is_new:
            writer.writeheader()
        writer.writerow({"date": date.today().isoformat(), "category": category,
                         "amount": f"{amount:.2f}", "note": note})


def summary(rows):
    totals = defaultdict(float)
    for row in rows:
        totals[row["category"]] += float(row["amount"])
    return dict(sorted(totals.items(), key=lambda item: item[1], reverse=True))


def main(argv=None):
    parser = argparse.ArgumentParser(description="Simple expense tracker")
    sub = parser.add_subparsers(dest="command", required=True)
    p_add = sub.add_parser("add", help="add an expense")
    p_add.add_argument("category")
    p_add.add_argument("amount", type=float)
    p_add.add_argument("--note", default="")
    sub.add_parser("list", help="list all expenses")
    sub.add_parser("summary", help="total per category")
    args = parser.parse_args(argv)

    if args.command == "add":
        if args.amount <= 0:
            parser.error("amount must be greater than zero")
        add(args.category, args.amount, args.note)
        print(f"Added {args.amount:.2f} to {args.category}")
    elif args.command == "list":
        for row in load():
            print(f'{row["date"]}  {row["category"]:<10} {float(row["amount"]):>8.2f}  {row["note"]}')
    else:
        rows = load()
        for category, total in summary(rows).items():
            print(f"{category:<10} {total:>8.2f}")
        print(f'{"TOTAL":<10} {sum(float(r["amount"]) for r in rows):>8.2f}')


if __name__ == "__main__":
    main()

চালিয়ে দেখুন

$ python3 expenses.py add food 120.5 --note "lunch"
Added 120.50 to food
$ python3 expenses.py add travel 300
Added 300.00 to travel
$ python3 expenses.py add food 80
Added 80.00 to food
$ python3 expenses.py list
2026-10-27  food         120.50  lunch
2026-10-27  travel       300.00
2026-10-27  food          80.00
$ python3 expenses.py summary
travel       300.00
food         200.50
TOTAL        500.50
$ python3 expenses.py add food -5
usage: expenses.py [-h] {add,list,summary} ...
expenses.py: error: amount must be greater than zero

(তারিখ আপনার মেশিনের আজকের তারিখ হবে।)

এটি কীভাবে কাজ করে

argparse command line-কে কাঠামোবদ্ধ data বানায়। add_subparsers প্রতিটি নিজস্ব আর্গুমেন্টসহ আলাদা কমান্ড দেয় এবং বিনা খরচে --help তৈরি করে। type=float পরিমাণ রূপান্তর করে এবং abc-এর মতো লেখা প্রত্যাখ্যান করে।

csv.DictWriter ও DictReader header-এর নাম দিয়ে dictionary হিসেবে row পড়ে ও লেখে, যা সংখ্যা-করা কলামের চেয়ে পরিষ্কার। ফাইল নতুন হলেই শুধু header লিখি।

with open(...) error হলেও ফাইল স্বয়ংক্রিয়ভাবে বন্ধ করে। csv module যেমন চায়, newline="" এবং স্পষ্ট encoding দিই।

defaultdict(float) প্রথমবার ব্যবহারে অনুপস্থিত key 0.0 দিয়ে বানায়, তাই যোগফলে if key in dict যাচাই লাগে না।

sorted(..., key=lambda item: item[1], reverse=True) বিভাগগুলো যোগফল অনুযায়ী সাজায়, বড়টি আগে।

যাচাই: parser.error(...) ব্যবহারের বার্তা ছাপে এবং code 2 দিয়ে বের হয়, যা command-line ব্যবহার ভুলের আদর্শ।

একটি test যোগ করুন

লক্ষ্য করুন summary একটি pure function: row ঢোকে, যোগফল বেরোয়, কোনো ফাইল বা print নেই। Pure function test করা সহজ। test_expenses.py নামে সংরক্ষণ করুন:

import unittest

import expenses


class SummaryTest(unittest.TestCase):
    def test_groups_and_sorts(self):
        rows = [
            {"category": "food", "amount": "10.50"},
            {"category": "travel", "amount": "40"},
            {"category": "food", "amount": "4.50"},
        ]
        self.assertEqual(expenses.summary(rows), {"travel": 40.0, "food": 15.0})


if __name__ == "__main__":
    unittest.main()

বিল্ট-ইন test runner দিয়ে চালান:

$ python3 -m unittest -v
...
Ran 1 test in 0.000s

OK

জানা সীমা (উন্নত করার ভালো জিনিস)

  • Floating-point সংখ্যায় টাকা রাখলে rounding ভুল দেখা যেতে পারে। আসল হিসাবে decimal module বা পূর্ণসংখ্যায় পয়সা সংরক্ষণ করুন।
  • এটি সবসময় বর্তমান folder-এর ফাইল ব্যবহার করে। option বা environment variable দিয়ে path কনফিগারযোগ্য করুন।
  • দুটি প্রোগ্রাম একসাথে লিখলে row মিশে যেতে পারে। SQLite-এর মতো database নিরাপদতর।
  • এখনো খরচ সম্পাদনা বা মোছার উপায় নেই।

বাড়ানোর চ্যালেঞ্জ

১. summary-তে --month 2026-10 filter যোগ করুন। ২. Row নম্বর দিয়ে delete কমান্ড যোগ করুন। ৩. প্রতি বিভাগে বাজেট সীমা দিন এবং ছাড়ালে সতর্ক করুন। ৪. মাসিক রিপোর্ট টেক্সট বা HTML ফাইলে export করুন। ৫. CSV-র বদলে SQLite-এ data রাখুন (Python standard library-তে sqlite3 আছে)। ৬. প্যাকেজ করুন যাতে যেকোনো জায়গা থেকে expenses add ... চালানো যায়।

উপরের কমান্ড দেখানো README-সহ GitHub-এ রাখুন। এটি ভালো প্রথম portfolio প্রজেক্ট হয়।

মূল কথা: কাজের টুলের জন্য শুধু argparse, একটি CSV ফাইল ও কয়েকটি ছোট function লাগে। যুক্তি pure function-এ রাখুন যাতে test করা সহজ হয়, এবং উপরের চ্যালেঞ্জ দিয়ে প্রজেক্ট বাড়ান।


হাতে-কলমে প্রজেক্ট সিরিজ, পর্ব ১ (ছোট পরীক্ষিত প্রজেক্ট): 1 নম্বর (মোট 5)।

আরও পড়ুন

Python দিয়ে এলোমেলো CSV পরিষ্কার: নাম, তারিখ, ফোন ও ডুপ্লিকেট

আসল data এলোমেলো। standard library দিয়ে contacts পরিষ্কার করা, প্রতিটি বাতিল row ব্যাখ্যা করা ও test-সহ একটি script বানান।

প্রজেক্ট•৫ মিনিটে পড়া