Python講座 / 最終実習:SQLiteの支出管理アプリを作る

この支出管理アプリは、日付、分類、金額を受け取り、SQLiteへ保存して一覧や分類別合計を表示します。初期化、検証、トランザクションとコマンド処理を明示して、基礎技術をつなぎます。

アプリの範囲と入力規則を決める

金額は韓国ウォン(KRW)の整数単位で非負、日付はYYYY-MM-DD、分類は空白だけでは不可とします。銀行連携、支払い、多人数の認証は含みません。addを一回実行するたびに新しい支出を一件作り、同じコマンドの再実行でも件数が増えます。

Python 3.12以降と標準ライブラリを使います。新しい練習フォルダーへledger.pyを保存します。--dbで別のデータベースを選べ、既定のledger.dbは現在の作業フォルダー基準です。業務や無関係な練習データを指定しないでください。

保存規則と自動テストは、sqlite3で保存しパラメーター付きSQLを使うで扱う規則とつながります。

プログラム全体を保存する

次が初期化から照会までを含む全体です。mainガードにより、importだけで引数解析や保存処理が始まることを避けます。

import argparse
import sqlite3
from datetime import date
from pathlib import Path

MAX_AMOUNT = 1_000_000_000_000

def amount_value(text):
    try:
        value = int(text)
    except ValueError as error:
        raise argparse.ArgumentTypeError("expected an integer") from error
    if not 0 <= value <= MAX_AMOUNT:
        raise argparse.ArgumentTypeError("amount must be between 0 and 1000000000000")
    return value

def date_value(text):
    try:
        parsed = date.fromisoformat(text)
    except ValueError as error:
        raise argparse.ArgumentTypeError("invalid date") from error
    if parsed.isoformat() != text:
        raise argparse.ArgumentTypeError("use YYYY-MM-DD")
    return text

def main():
    parser = argparse.ArgumentParser()
    parser.add_argument("--db", type=Path, default=Path("ledger.db"))
    commands = parser.add_subparsers(dest="command", required=True)
    commands.add_parser("init")
    add = commands.add_parser("add")
    add.add_argument("day", type=date_value)
    add.add_argument("category")
    add.add_argument("amount", type=amount_value)
    commands.add_parser("list")
    commands.add_parser("summary")
    args = parser.parse_args()
    if args.command != "init" and not args.db.is_file():
        parser.error("database not initialized")
    if args.command == "add" and not args.category.strip():
        parser.error("category is required")
    con = sqlite3.connect(args.db, autocommit=False)
    try:
        if args.command == "init":
            with con:
                con.execute("CREATE TABLE IF NOT EXISTS expenses (id INTEGER PRIMARY KEY, day TEXT NOT NULL, category TEXT NOT NULL CHECK(length(trim(category))>0), amount INTEGER NOT NULL CHECK(amount BETWEEN 0 AND 1000000000000))")
            print("initialized")
        elif args.command == "add":
            with con:
                con.execute("INSERT INTO expenses(day,category,amount) VALUES(?,?,?)", (args.day, args.category.strip(), args.amount))
            print("added")
        elif args.command == "list":
            for row in con.execute("SELECT id,day,category,amount FROM expenses ORDER BY day,id"):
                print(*row)
        else:
            totals = {}
            for category, amount in con.execute("SELECT category,amount FROM expenses"):
                totals[category] = totals.get(category, 0) + amount
            for category, total in sorted(totals.items()):
                print(category, total)
    finally:
        con.close()

if __name__ == "__main__":
    main()

初期化・入力・照会を順に実行する

同じフォルダーのターミナルで実行します。

python ledger.py init
python ledger.py add 2026-10-17 food 3500
python ledger.py add 2026-10-17 food 8000
python ledger.py add 2026-10-17 transport 2500
python ledger.py list
python ledger.py summary

初期化はinitialized、追加はaddedを表示します。listはID、日付、分類、金額を日付とIDの順で表示し、最後のsummaryは次になります。

food 11500
transport 2500

initは既存の表を消しません。空の状態で繰り返すには、新しいフォルダーか新しい--dbのパスを使います。全体オプションはpython ledger.py --db other-ledger.db initのようにサブコマンドより前へ置きます。練習の初期化のために、未知のデータベースを削除しないでください。

拒否と永続化を確認する

2026-02-30、負の金額、空白だけの分類を一つずつ試し、保存前に拒否され、一覧の件数が増えていないことを確認します。プログラムを再起動してlistで永続化を確かめます。別の新しいDBでも同じ3件と合計を再現してください。

値パラメーターは入力をSQLの構造から分け、保存側の制約は金額と分類の規則を補います。失敗時はwithで取り消し、finallyで接続を閉じます。スキーマ移行、バックアップと復元、同時利用には追加設計が必要です。

大きくなったときに責務を分ける

最初は一つの完全なファイルで流れを把握し、成長したら引数解析、検証、保存と集計をモジュールへ分けられます。テストで既存の動作を守りながら構成を変えます。

実際の個人の支出を扱う場合は、ファイル権限、バックアップ、共有先を決めます。正しい合計が出ることに加え、不正入力の拒否と再実行の意味まで確認して完成と判断します。

関連する公式リファレンス

流れは引数解析→日付・金額検証→DB存在確認→トランザクション書き込みまたは検索→表示です。amount_valueは0〜1,000,000,000,000の整数、date_valueは実在するYYYY-MM-DD、mainは分岐と接続寿命を担当します。summaryはSQL SUMではなくPython整数で累積し、SQLiteの合計オーバーフローを避けます。行は順次読みますが、集計辞書のメモリーは分類数に比例します。既存のCHECKは自動更新されないので新しいDBを使います。下の結合テストで正常保存、日付・負数・上限超過の拒否、一覧、合計を一時DBで確認します。

全体を check_ledger.py に保存し、同じ練習フォルダーで python check_ledger.py を実行します。 先に ledger.py も同じ場所へ置きます。

import subprocess
import sys
import tempfile
from pathlib import Path
program = Path("ledger.py").resolve()
with tempfile.TemporaryDirectory() as folder:
    db = Path(folder) / "test.db"
    def run(*args):
        return subprocess.run([sys.executable, str(program), "--db", str(db), *args], capture_output=True, text=True)
    assert run("init").returncode == 0
    assert run("add", "2026-10-17", "food", "8000").returncode == 0
    assert run("add", "2026-02-30", "food", "8000").returncode != 0
    assert run("add", "2026-10-17", "food", "-1").returncode != 0
    assert run("add", "2026-10-17", "food", "1000000000001").returncode != 0
    result = run("summary")
    assert result.returncode == 0 and result.stdout.strip() == "food 8000"
    assert len(run("list").stdout.splitlines()) == 1
print("integration checks passed")

予想結果:

integration checks passed

動作と適用条件の公式説明:SQLite: sum and total