Track your spending from the terminal or through a beautiful responsive web dashboard.
- π Project Overview
- πΈ Preview
- β¨ Features
- π οΈ Tech Stack
- π Project Structure
- π Installation
- π CLI Usage
- π Web Interface
- ποΈ Architecture
- πΎ Database Schema
- π§ͺ Running Tests
- π©Ή Troubleshooting
- π οΈ Development
- πΊοΈ Roadmap
- π€ Contributing
- π License
- π FAQ
- β Show Your Support
| π Language | Python |
| π Web Framework | Flask |
| πΎ Database | SQLite |
| π₯ Interface | CLI + Web |
| π± Responsive | β |
| π§ͺ Tested | Pytest |
| π License | GPL-3.0 |
βββββββββββββββββββββββ Summary β 2026-06 βββββββββββββββββββββββ
Total: 2687.49 across 5 expenses
Budget: 3000.00 Remaining: 312.51
By Category
ββββββββββββββββββ¬ββββββββ¬ββββββββββ¬βββββββ
β Category β Count β Total β % β
ββββββββββββββββββΌββββββββΌββββββββββΌβββββββ€
β Housing β 1 β 2500.00 β 93.0 β
β Food β 2 β 132.50 β 4.9 β
β Transport β 1 β 45.00 β 1.7 β
β Entertainment β 1 β 9.99 β 0.4 β
ββββββββββββββββββ΄ββββββββ΄ββββββββββ΄βββββββ
ββββββββββββββββββββ¬βββββββββββββββββββββββββ¬ββββββββββββββββββ
β Total β 2026-06 β Budget remaining β Top category β
β 2,687.49 β 312.51 β Housing β
β 5 expenses β ββββββββββββββ 89% β 2,500.00 β
ββββββββββββββββββββ΄βββββββββββββββββββββββββ΄ββββββββββββββββββ
Per-category budgets
ββββββββββββββ¬ββββββββββ¬ββββββββββ¬βββββββββββ¬βββββββββββββββββββββ
β Category β Budget β Spent β Remainingβ Usage β
ββββββββββββββΌββββββββββΌββββββββββΌβββββββββββΌβββββββββββββββββββββ€
β π Food β 300.00 β 132.50 β 167.50 β ββββββββββ 44% β
β π Transp. β 150.00 β 45.00 β 105.00 β ββββββββββ 30% β
β π¬ Entert. β 50.00 β 9.99 β 40.01 β ββββββββββ 20% β
ββββββββββββββ΄ββββββββββ΄ββββββββββ΄βββββββββββ΄βββββββββββββββββββββ
- β Add / Edit / Delete expenses with description, amount, date, and category
- π·οΈ Manage categories β create custom ones with your own colors
- π Filter & search by date range and category
- π΅ Set monthly budgets β overall or per-category
- π Monthly summary with totals, counts, percentages, and budget tracking
- π€ Export to CSV for spreadsheet analysis or backup
- ποΈ Local SQLite β no servers, no cloud, your data stays on your machine
- π¨ Beautiful terminal UI powered by Rich
- π Visualize spending as a horizontal bar chart (PNG via matplotlib)
- π§ͺ Fully tested with pytest (6 tests, all passing)
- π₯οΈ Single-page application β Dashboard, Expenses, Categories, Reports, Budget
- π Interactive charts powered by Chart.js (bar + doughnut)
- π― Per-category budget management with progress bars
- πͺ Modal forms with validation and toast notifications
- π± Responsive layout β works on phone, tablet, desktop
- π Same SQLite database β CLI and web share data seamlessly
| Layer | Tool |
|---|---|
| Language | Python 3.10+ (tested on 3.14) |
| CLI framework | Click 8.x |
| Terminal UI | Rich 13.x |
| Web framework | Flask 3.x π |
| Frontend | Vanilla JS + Chart.js 4.x π |
| Design system | Custom CSS (light/dark tokens, fluid grid, no framework) |
| Database | SQLite (Python stdlib) |
| Charts (CLI) | Matplotlib 3.x |
| Testing | pytest 7.x |
| Packaging | pyproject.toml (PEP 621, modern standard) |
π‘ Zero runtime dependencies outside the standard library except for Click, Rich, Matplotlib, and Flask β all installable with one command.
expense-tracker/
β
βββ pyproject.toml # Project metadata & dependencies (PEP 621)
βββ requirements.txt # Pip-installable dependencies
βββ README.md # You are here
βββ CHANGELOG.md # Release history
βββ .gitignore # Ignore __pycache__, *.db, etc.
β
βββ src/
β βββ expense_tracker/
β βββ __init__.py # Package marker & version
β βββ __main__.py # Enables: python -m expense_tracker
β βββ cli.py # All Click commands
β βββ database.py # SQLite setup, schema, connection
β βββ models.py # Dataclasses + repository classes
β βββ reports.py # Monthly aggregation logic
β βββ visualization.py # Matplotlib charts
β βββ web.py # π Flask app + JSON REST API
β β
β βββ templates/ # π
β β βββ index.html # Single-page app shell
β β
β βββ static/ # π
β βββ css/style.css # Design system
β βββ js/app.js # SPA logic (routing, CRUD, charts)
β
βββ tests/
βββ conftest.py # Shared pytest fixtures (in-memory DB)
βββ test_models.py # Repository unit tests
βββ test_reports.py # Report generation tests
The src/ layout is the modern Python best practice β it prevents accidental imports from the working directory and forces proper packaging.
- Python 3.10 or higher β check with
py --versionorpython3 --version - pip β usually bundled with Python
git clone https://github.com/ItsWanheda/expense-tracker.git
cd expense-trackerWindows (PowerShell):
py -m venv .venv
.\.venvScripts\Activate.ps1macOS / Linux:
python3 -m venv .venv
source .venv/bin/activateπ‘ Windows tip: If PowerShell blocks script activation, run this once:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Recommended (editable + dev tools):
pip install -e ".[dev]"Or just runtime dependencies:
pip install -r requirements.txtpy -m expense_tracker --versionExpected output:
0.2.0
# 1. See what categories exist (7 are auto-seeded)
py -m expense_tracker categories list
# 2. Add a few expenses
py -m expense_tracker add -a 12.50 -d "Lunch at cafe" -c Food
py -m expense_tracker add -a 45.00 -d "Uber to airport" -c Transport
py -m expense_tracker add -a 120.00 -d "Weekly groceries" -c Food
py -m expense_tracker add -a 9.99 -d "Netflix" -c Entertainment
py -m expense_tracker add -a 2500.00 -d "Rent" -c Housing
# 3. View your expenses
py -m expense_tracker list
# 4. Set a budget and see your summary
py -m expense_tracker budget 3000
py -m expense_tracker summary
# 5. Generate a chart
py -m expense_tracker chart -o my-spending.png
# 6. Export for spreadsheet
py -m expense_tracker export -o expenses.csv| Command | Description |
|---|---|
add |
Add a new expense |
list |
List expenses (with filters) |
edit ID |
Edit an existing expense |
delete ID |
Delete an expense by ID |
summary [-m MONTH] |
Show monthly summary + budget status |
budget AMOUNT [-m MONTH] [-c CATEGORY] |
Set a monthly budget (overall or per-category) π |
chart [-o FILE] |
Generate a PNG bar chart |
export [-o FILE] |
Export to CSV |
categories list |
List all categories |
categories add NAME |
Create a new category |
categories delete ID |
Delete a category |
# Minimal β defaults to today's date, no category
py -m expense_tracker add -a 25.00 -d "Book"
# Full
py -m expense_tracker add -a 45.00 -d "Uber" -c Transport --date 2024-05-15
# Create a brand-new category on the fly (it will ask)
py -m expense_tracker add -a 9.99 -d "Netflix" -c Subscriptions
# ? Category 'Subscriptions' doesn't exist. Create it? [y/N]: y# Most recent 20
py -m expense_tracker list
# Filter by date range
py -m expense_tracker list --from 2024-05-01 --to 2024-05-31
# Filter by category
py -m expense_tracker list -c Food
# Combine filters and show more
py -m expense_tracker list -c Food --from 2024-05-01 --to 2024-05-31 -n 50# Only the fields you pass get updated (others stay the same)
py -m expense_tracker edit 3 -a 130.00 -d "Weekly groceries (updated)"
py -m expense_tracker edit 3 -c Transport # change category only
py -m expense_tracker edit 3 --date 2024-05-20 # change date only
# Delete by ID
py -m expense_tracker delete 5# Current month
py -m expense_tracker summary
# Specific month
py -m expense_tracker summary -m 2024-05Output:
ββββββββββββββββββββ Summary β 2024-05 ββββββββββββββββββββ
Total: 2687.49 across 4 expenses
Budget: 3000.00 Remaining: 312.51
By Category
ββββββββββββββββ¬ββββββββ¬ββββββββββ¬ββββββ
β Category β Count β Total β % β
ββββββββββββββββΌββββββββΌββββββββββΌββββββ€
β Housing β 1 β 2500.00 β 93% β
β Food β 2 β 132.50 β 5% β
β Transport β 1 β 45.00 β 2% β
β Entertainmentβ 1 β 9.99 β 0% β
ββββββββββββββββ΄ββββββββ΄ββββββββββ΄ββββββ
If you've exceeded your budget, Remaining will turn red automatically.
# Overall monthly budget
py -m expense_tracker budget 3000
py -m expense_tracker budget 3000 -m 2024-05
# Per-category budget π
py -m expense_tracker budget 300 -c Food
py -m expense_tracker budget 50 -c Entertainment -m 2024-05You can mix both β an overall budget caps total spending, while per-category budgets cap individual categories. They're evaluated independently.
py -m expense_tracker chart -o may.png # saves may.png
py -m expense_tracker chart -o may.png -m 2024-05 # specific monthpy -m expense_tracker export -o expenses.csv
py -m expense_tracker export -o may.csv --from 2024-05-01 --to 2024-05-31The CSV has columns: id, date, category, amount, description.
New in 0.2.0. A complete single-page application that talks to the same SQLite database as the CLI β every entry you add in the browser shows up in the terminal and vice-versa.
py -m expense_tracker.webThen open http://127.0.0.1:5000 in your browser.
You can also use the Flask CLI:
export FLASK_APP=expense_tracker.web # macOS / Linux
$env:FLASK_APP = "expense_tracker.web" # Windows PowerShell
flask run --debug| Page | What it does |
|---|---|
| π Dashboard | Total spent this month, budget remaining with a progress bar, top category, and recent expenses |
| π Expenses | Full CRUD with date/category filters, modal forms for add/edit, inline delete confirmation |
| π·οΈ Categories | Grid of colored category cards with add/delete and a color picker |
| π Reports | Interactive bar + doughnut charts (Chart.js) plus a category breakdown table with Budget & Remaining columns when applicable |
| π― Budget | Manage overall and per-category budgets in one place, with per-category progress bars and an active-budgets table with Edit/Delete |
The web UI talks to a small JSON REST API. You can use it directly too:
| Method | Endpoint | Purpose |
|---|---|---|
GET |
/api/health |
Health check |
GET |
/api/categories |
List categories |
POST |
/api/categories |
Create category |
DELETE |
/api/categories/<id> |
Delete category |
GET |
/api/expenses |
List expenses (filters: from, to, category_id, limit) |
POST |
/api/expenses |
Create expense |
PUT |
/api/expenses/<id> |
Update expense (partial) |
DELETE |
/api/expenses/<id> |
Delete expense |
GET |
/api/reports/summary?month=YYYY-MM |
Monthly report (incl. category_budgets) |
GET |
/api/budget?month=β¦&category_id=β¦ |
Get a single budget |
GET |
/api/budgets?month=YYYY-MM |
List all budgets for a month π |
PUT |
/api/budget |
Set/update a budget (category_id optional) π |
DELETE |
/api/budget?month=β¦&category_id=β¦ |
Delete a budget π |
GET |
/api/export.csv |
Download CSV export |
Example with curl:
# Add an expense via the API
curl -X POST http://127.0.0.1:5000/api/expenses \
-H "Content-Type: application/json" \
-d '{"amount": 12.50, "description": "Lunch", "category_id": 1, "date": "2026-06-30"}'
# Get this month's summary as JSON
curl http://127.0.0.1:5000/api/reports/summary
# Set a per-category budget
curl -X PUT http://127.0.0.1:5000/api/budget \
-H "Content-Type: application/json" \
-d '{"month": "2026-06", "amount": 300, "category_id": 1}'π‘οΈ For development only. Don't expose
app.run()to the internet β usegunicorn 'expense_tracker.web:create_app()'behind a reverse proxy in production.
This project follows a layered architecture that separates concerns cleanly:
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Presentation Layer β
β ββββββββββββββββββββββββ ββββββββββββββββββββββββββββ β
β β cli.py (Click+Rich) β β web.py (Flask + JS SPA) β β
β ββββββββββββ¬ββββββββββββ ββββββββββββββββ¬ββββββββββββ β
βββββββββββββββΌββββββββββββββββββββββββββββββββΌββββββββββββββββ
β β
βββββββββββββββββ¬ββββββββββββββββ
β calls
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Repository Layer (models.py) β
β - Static methods per entity (CRUD + queries) β
β - Returns dataclasses, not raw rows β
β - Shared by both the CLI and the web app β
ββββββββββββββββββββββββββββ¬βββββββββββββββββββββββββββββββββββ
β uses
βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β Data Layer (database.py) β
β - SQLite connection management (context manager) β
β - Schema definition & migrations β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
Cross-cutting:
reports.py β aggregation (used by CLI summary + web reports)
visualization.py β matplotlib charts (CLI only)
| Layer | Responsibility | Why it matters |
|---|---|---|
| Data | Manage the connection & schema | One place to change the database |
| Repository | Translate Python β SQL | Easy to swap SQLite for Postgres later |
| Presentation | Talk to the user (terminal or browser) | Multiple UIs share the same logic |
The web layer is just a thin Flask wrapper around the same repositories the CLI uses β zero duplicated SQL, zero duplicated business logic.
- Idempotent
initialize_database()β called on every startup (CLI + web); safe to run repeatedly - Idempotent
CategoryRepository.create()β returns existing ID if name is taken (no surprises) - Python-level upserts for
BudgetRepositoryβ avoids SQLite's NULL + UNIQUE pitfall - Timezone-aware datetimes β
datetime.now(timezone.utc), not deprecatedutcnow() - Context-managed DB connections β auto-commit on success, auto-rollback on error
- Single DB, two UIs β CLI and web read/write the same
~/.expense_tracker/expenses.db
CREATE TABLE categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL UNIQUE,
color TEXT DEFAULT '#3498db',
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE expenses (
id INTEGER PRIMARY KEY AUTOINCREMENT,
amount REAL NOT NULL CHECK (amount > 0),
description TEXT NOT NULL,
category_id INTEGER,
date TEXT NOT NULL, -- ISO: YYYY-MM-DD
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE SET NULL
);
CREATE TABLE budgets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER, -- NULL = overall budget
month TEXT NOT NULL, -- YYYY-MM
amount REAL NOT NULL CHECK (amount >= 0),
UNIQUE (category_id, month),
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
);Food π Β· Transport π Β· Housing π Β· Entertainment π¬ Β· Health π Β· Shopping ποΈ Β· Other π¦
| OS | Path |
|---|---|
| Windows | C:\Users\<you>\.expense_tracker\expenses.db |
| macOS / Linux | ~/.expense_tracker/expenses.db |
# Run all tests, verbose
py -m pytest -v
# Run with coverage report
py -m pytest --cov=expense_tracker --cov-report=term-missing
# Run a single file
py -m pytest tests/test_models.py -v
# Run a single test
py -m pytest tests/test_models.py::test_update_expense -vtests/test_models.py::test_add_and_get_expense PASSED
tests/test_models.py::test_update_expense PASSED
tests/test_models.py::test_delete_expense PASSED
tests/test_models.py::test_list_with_filters PASSED
tests/test_models.py::test_budget_set_and_get PASSED
tests/test_reports.py::test_monthly_report PASSED
========================== 6 passed in 0.4s ==========================
The tmp_db fixture in conftest.py:
- Creates a temporary SQLite file for each test (via
tmp_path) - Patches
database.get_db_pathto point at it - Initializes the schema
- Cleans up automatically when the test ends
This means tests never touch your real database β completely safe.
β python is not recognized (Windows)
Reinstall Python from python.org and check the box:
β Add python.exe to PATH
Alternatively, use py (the Python Launcher) which is installed automatically on Windows:
py --versionβ PowerShell blocks script activation
Run once as your user:
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUserThen try activating again:
.\.venv\Scripts\Activate.ps1β IndentationError after pasting code
Code copied from chat sometimes loses spaces. Verify the syntax with:
py -m compileall src\expense_tracker -qIf there's an error, open the offending file in your editor and fix the indentation manually.
β OperationalError: no such table
Both the CLI and the web app auto-initialize the database on startup. If you still see this, your ~/.expense_tracker/ directory may be locked or unreadable. Try:
# Delete and recreate
Remove-Item -Recurse -Force ~\.expense_tracker
py -m expense_tracker categories list # this re-creates everythingβ UNIQUE constraint failed: categories.name
You tried to create a category that already exists. The CLI handles this automatically by asking, but if you see it in code, use CategoryRepository.create() which is idempotent:
# Returns existing ID if "Food" exists, otherwise creates it
cat_id = CategoryRepository.create("Food")
β Web UI won't load (404 on /static/β¦)
Make sure you launched via the package, not a stray script:
# β
Correct
py -m expense_tracker.web
# β Wrong (Flask can't find templates/static)
cd src && py expense_tracker/web.py
- Open
src/expense_tracker/cli.py
- Add a new function decorated with
@cli.command() (or @<group>.command())
- Implement it using existing repositories β don't add SQL here
- (Optional) Add a matching API endpoint in
web.py
- (Optional) Add a UI section in
static/js/app.js
- Add a test in
tests/
- Add the column to
SCHEMA in database.py
- Add a migration note to handle existing databases
- Update the relevant dataclass in
models.py
- Update repository methods that touch that field
- Update
reports.py / web.py / app.js if the field is exposed to users
- Add tests for the new behavior
- PEP 8 for naming and layout
- Type hints on all public functions
- Docstrings for all public classes and functions
- Dataclasses for value objects, not plain dicts
- No raw SQL in CLI or web code β always go through a repository
Planned for future releases:
- Core CRUD for expenses and categories
- Monthly summary & overall budget tracking
- CSV export
- Matplotlib charts (CLI)
- Pytest test suite
- Per-category budgets with progress bars β
(
0.2.0)
- Web interface using the same repositories β
(
0.2.0)
- Interactive charts (Chart.js in the web UI) β
(
0.2.0)
- Responsive design β sidebar drawer + stacked layouts + scrollable tables β
( 0.3.0)
- Light/dark theme with live chart recoloring β
( 0.3.0)
- Command palette & keyboard shortcuts β
( 0.3.0)
- Toast with Undo action for accidental deletes β
( 0.3.0)
- Recurring expenses (rent, subscriptions)
- Multi-currency support with conversion rates
- Interactive REPL mode (
expense shell)
- JSON import / export
- Telegram / Discord bot integration
- GitHub Actions CI (run tests on every push)
- Publish to PyPI (
pip install expense-tracker)
- Tag system (many-to-many)
- Pre-commit hooks (black, ruff, mypy)
- User Authentication
- Cloud Sync
- Notifications
- Multiple Wallets
- Multi-Currency Support
- Advanced Charts
- AI Spending Insights
- Progressive Web App (PWA)
- Docker Support
- PDF Report Export
- Advanced Analytics
- Localization & Multi-language
- User Accounts & Profiles
- Excel Export
- PostgreSQL/MySQL Support
Contributions of all sizes are welcome! Here's the workflow:
- Fork the repository
- Create a branch for your feature:
git checkout -b feature/per-category-budgets
- Make your changes and add tests
- Run the test suite to make sure nothing broke:
py -m pytest -v
- Commit with a clear message:
git commit -m "Add per-category budgets with progress bars"
- Push and open a Pull Request
Please open an issue first if you want to discuss a big change before implementing it.
This project is licensed under the MIT License β see the LICENSE file for details. You're free to use, modify, and distribute it, commercially or otherwise.
Q: Is my financial data safe?
A: 100%. Everything is stored in a single SQLite file on your machine. There is no cloud sync, no telemetry, no analytics β nothing leaves your computer.
Q: Can I sync between machines?
A: Yes β just copy ~/.expense_tracker/expenses.db between devices. You could put it in Dropbox/Syncthing/etc. for automatic syncing.
Q: Can I import data from my bank?
A: Not yet, but a CSV import command is on the roadmap. In the meantime, you can bulk-insert via a small Python script using the existing repositories.
Q: Why Click instead of argparse?
A: Click gives us nested subcommands (categories add), automatic --help for every level, and better ergonomics with about 60% less code than argparse.
Q: Can I extend this with a web UI?
A: You don't have to β there's already one in 0.2.0! py -m expense_tracker.web starts the Flask server. If you want to add your own, the repository pattern makes it trivial: import the repos and return JSON.
Q: Can the CLI and web app be used at the same time?
A: Yes. SQLite supports concurrent reads from the same connection pool. Both UIs read/write the same expenses.db, so changes in one appear instantly in the other.
Q: Why no ORMs (SQLAlchemy, Tortoise)?
A: For a small project, raw SQL with the repository pattern is simpler, faster, and gives you full control. ORMs add abstraction layers that aren't justified at this scale.
Q: Why Flask and not FastAPI?
A: Flask's templating + static-file serving made the bundled SPA dead-simple to ship without a separate build step. The REST API uses plain JSON over HTTP, so a future migration to FastAPI is mostly mechanical if performance or async becomes important.
If this project helped you learn something or saved you time, give it a star on GitHub! It helps others discover it.
Made with β€οΈ and lots of β