ساخت برنامه مدیریت هزینههای شخصی با پایتون؛ پروژه کامل با JSON و گزارش ماهانه

🗓 آخرین بهروزرسانی:
ساخت برنامه مدیریت هزینههای شخصی با پایتون یکی از پروژههای کاربردی برای تمرین کار با لیست، دیکشنری، فایل JSON، تاریخ، تابع، حلقه، شرط و مرتبسازی دادههاست. در این آموزش فقط چند عدد را جمع نمیکنیم؛ یک برنامه واقعی میسازیم که بتواند هزینههای روزانه را ثبت کند، برای هر هزینه دستهبندی و تاریخ نگه دارد، گزارش ماهانه بسازد، مجموع هزینه هر دسته را محاسبه کند، بزرگترین هزینه را پیدا کند و اطلاعات را در فایل JSON ذخیره کند تا بعد از بسته شدن برنامه از بین نروند.
این مقاله سیزدهمین پروژه فاز اول دسته تمرین و پروژههای پایتون است. اگر پروژه Todo List با پایتون یا مدیریت نمرات دانشآموزان با پایتون را انجام داده باشید، ساختار ذخیره داده در JSON برایتان آشنا خواهد بود. در این پروژه علاوه بر ذخیرهسازی، گزارشگیری و کار با تاریخ را هم اضافه میکنیم.
در این پروژه چه چیزی میسازیم؟
- ثبت هزینه جدید با عنوان، مبلغ، دسته و تاریخ
- شناسه یکتا برای هر هزینه
- نمایش همه هزینهها
- حذف هزینه با شناسه
- اعتبارسنجی مبلغ
- جلوگیری از ثبت مبلغ صفر یا منفی
- دستهبندی هزینهها
- ثبت خودکار تاریخ امروز
- اعتبارسنجی تاریخ واردشده
- گزارش جمع هزینه بر اساس دسته
- فیلتر هزینهها بر اساس دسته
- گزارش ماهانه با فرمت YYYY-MM
- محاسبه میانگین هزینهها
- پیدا کردن بزرگترین هزینه
- ذخیره و بازیابی اطلاعات از JSON
- مدیریت فایل JSON خراب یا نامعتبر
ساختار داده هر هزینه
هر Expense را بهصورت یک دیکشنری نگه میداریم:
{
"id": 1,
"title": "خرید مواد غذایی",
"amount": 850000,
"category": "خوراک",
"date": "2026-08-11"
}تمام هزینهها داخل یک لیست ذخیره میشوند. این ساختار برای JSON مناسب است و در مراحل بعدی بهراحتی میتوان آن را به دیتابیس منتقل کرد.
مرحله اول: ساخت فایل پروژه
یک فایل با نام زیر ایجاد کنید:
expense_manager.pyفایل داده نیز بهصورت خودکار با این نام ساخته میشود:
expenses.jsonمرحله دوم: ماژولهای مورد نیاز
import json
from pathlib import Path
from datetime import date, datetimeاز json برای ذخیره اطلاعات، از Path برای مدیریت فایل و از date و datetime برای ثبت و اعتبارسنجی تاریخ استفاده میکنیم. پروژه محاسبه سن با پایتون نیز نمونه مناسبی از کار با تاریخ در پایتون است.
مرحله سوم: تعریف دستهبندیها و فایل داده
DATA_FILE = Path("expenses.json")
CATEGORIES = (
"خوراک",
"حملونقل",
"قبوض",
"خرید",
"تفریح",
"سلامت",
"سایر",
)دستهها را در Tuple قرار دادهایم، چون در زمان اجرای برنامه قرار نیست مرتب تغییر کنند.
مرحله چهارم: بارگذاری اطلاعات از JSON
def load_expenses():
if not DATA_FILE.exists():
return []
try:
with DATA_FILE.open(
"r",
encoding="utf-8",
) as file:
data = json.load(file)
except (
json.JSONDecodeError,
OSError,
):
print(
"فایل اطلاعات قابل خواندن نیست. "
"برنامه با لیست خالی شروع شد."
)
return []
return data if isinstance(data, list) else []اگر فایل هنوز وجود نداشته باشد، برنامه با لیست خالی شروع میشود. اگر JSON خراب باشد نیز برنامه متوقف نمیشود.
مرحله پنجم: ذخیره هزینهها
def save_expenses(expenses):
with DATA_FILE.open(
"w",
encoding="utf-8",
) as file:
json.dump(
expenses,
file,
ensure_ascii=False,
indent=2,
)گزینه ensure_ascii=False باعث میشود عنوانها و دستههای فارسی در فایل خوانا باقی بمانند.
مرحله ششم: ساخت شناسه یکتا
def next_expense_id(expenses):
if not expenses:
return 1
return max(
expense.get("id", 0)
for expense in expenses
) + 1استفاده از طول لیست برای ID انتخاب خوبی نیست، چون بعد از حذف یک هزینه ممکن است شناسه تکراری ایجاد شود. این روش همیشه یک واحد بیشتر از بزرگترین ID موجود تولید میکند.
مرحله هفتم: دریافت مبلغ معتبر
def get_positive_amount():
value = input(
"مبلغ هزینه: "
).strip()
try:
amount = float(value)
except ValueError:
print("مبلغ باید عدد باشد.")
return None
if amount <= 0:
print(
"مبلغ باید بیشتر از صفر باشد."
)
return None
return amountدر این برنامه واحد پول را به منطق کد وابسته نمیکنیم. میتوانید مبلغها را بر اساس تومان، ریال، لیر یا هر واحد دیگری وارد کنید؛ فقط باید در تمام رکوردها یک واحد ثابت داشته باشید.
مرحله هشتم: انتخاب دسته هزینه
def choose_category():
print("\nدستهبندیها:")
for index, category in enumerate(
CATEGORIES,
start=1,
):
print(
f"{index}) {category}"
)
value = input(
"شماره دسته: "
).strip()
if not value.isdigit():
print(
"شماره دسته باید عدد باشد."
)
return None
index = int(value)
if not (
1 <= index <= len(CATEGORIES)
):
print("دسته نامعتبر است.")
return None
return CATEGORIES[index - 1]مرحله نهم: دریافت تاریخ هزینه
کاربر میتواند تاریخ را وارد کند یا با خالی گذاشتن ورودی، تاریخ امروز ثبت شود.
def get_expense_date():
value = input(
"تاریخ YYYY-MM-DD "
"(خالی = امروز): "
).strip()
if not value:
return date.today().isoformat()
try:
expense_date = date.fromisoformat(
value
)
except ValueError:
print(
"تاریخ معتبر نیست. "
"نمونه: 2026-08-11"
)
return None
if expense_date > date.today():
print(
"تاریخ هزینه نمیتواند "
"در آینده باشد."
)
return None
return expense_date.isoformat()مرحله دهم: افزودن هزینه جدید
def add_expense(expenses):
title = input(
"عنوان هزینه: "
).strip()
if not title:
print(
"عنوان نمیتواند خالی باشد."
)
return
amount = get_positive_amount()
if amount is None:
return
category = choose_category()
if category is None:
return
expense_date = get_expense_date()
if expense_date is None:
return
expense = {
"id": next_expense_id(
expenses
),
"title": title,
"amount": amount,
"category": category,
"date": expense_date,
}
expenses.append(expense)
save_expenses(expenses)
print(
"هزینه با موفقیت ثبت شد."
)مرحله یازدهم: نمایش همه هزینهها
def show_expenses(expenses):
if not expenses:
print(
"هنوز هزینهای ثبت نشده است."
)
return
print("\n=== فهرست هزینهها ===")
for expense in expenses:
print(
f"#{expense.get('id')} | "
f"{expense.get('date')} | "
f"{expense.get('category')} | "
f"{expense.get('title')} | "
f"{expense.get('amount', 0):,.2f}"
)مرحله دوازدهم: حذف هزینه
def get_expense_by_id(
expenses,
expense_id,
):
for expense in expenses:
if expense.get("id") == expense_id:
return expense
return None
def delete_expense(expenses):
show_expenses(expenses)
if not expenses:
return
value = input(
"شناسه هزینه برای حذف: "
).strip()
try:
expense_id = int(value)
except ValueError:
print(
"شناسه باید عدد صحیح باشد."
)
return
expense = get_expense_by_id(
expenses,
expense_id,
)
if expense is None:
print(
"هزینهای با این شناسه پیدا نشد."
)
return
expenses.remove(expense)
save_expenses(expenses)
print("هزینه حذف شد.")مرحله سیزدهم: گزارش بر اساس دستهبندی
برای هر دسته مجموع هزینه را محاسبه میکنیم و سپس دستهها را از بیشترین مبلغ به کمترین مرتب میکنیم.
def category_report(expenses):
if not expenses:
print(
"دادهای برای گزارش وجود ندارد."
)
return
totals = {}
for expense in expenses:
category = expense.get(
"category",
"سایر",
)
totals[category] = (
totals.get(category, 0)
+ expense.get("amount", 0)
)
print(
"\n=== گزارش دستهبندی ==="
)
sorted_rows = sorted(
totals.items(),
key=lambda item: item[1],
reverse=True,
)
for category, total in sorted_rows:
print(
f"{category}: {total:,.2f}"
)مرحله چهاردهم: گزارش ماهانه
کاربر ماه را با فرمت YYYY-MM وارد میکند. ابتدا فرمت را با datetime.strptime() بررسی میکنیم و سپس رکوردهای همان ماه را فیلتر میکنیم.
def monthly_report(expenses):
value = input(
"ماه موردنظر YYYY-MM: "
).strip()
try:
datetime.strptime(
value,
"%Y-%m",
)
except ValueError:
print(
"فرمت ماه معتبر نیست. "
"نمونه: 2026-08"
)
return
rows = [
expense
for expense in expenses
if expense.get(
"date",
"",
).startswith(value)
]
if not rows:
print(
"برای این ماه هزینهای ثبت نشده است."
)
return
total = sum(
expense.get("amount", 0)
for expense in rows
)
average = total / len(rows)
largest = max(
rows,
key=lambda expense: (
expense.get("amount", 0)
),
)
print(
f"\n=== گزارش {value} ==="
)
print(
f"تعداد تراکنشها: {len(rows)}"
)
print(
f"جمع هزینه: {total:,.2f}"
)
print(
f"میانگین هر هزینه: {average:,.2f}"
)
print(
"بزرگترین هزینه: "
f"{largest.get('title')} | "
f"{largest.get('amount', 0):,.2f}"
)مرحله پانزدهم: فیلتر بر اساس دسته
def filter_by_category(expenses):
category = choose_category()
if category is None:
return
rows = [
expense
for expense in expenses
if expense.get("category")
== category
]
if not rows:
print(
"در این دسته هزینهای ثبت نشده است."
)
return
show_expenses(rows)مرحله شانزدهم: خلاصه کل هزینهها
def overall_summary(expenses):
if not expenses:
print(
"دادهای برای خلاصه وجود ندارد."
)
return
total = sum(
expense.get("amount", 0)
for expense in expenses
)
average = total / len(expenses)
largest = max(
expenses,
key=lambda expense: (
expense.get("amount", 0)
),
)
print(
"\n=== خلاصه کل ==="
)
print(
f"تعداد هزینهها: {len(expenses)}"
)
print(
f"جمع کل: {total:,.2f}"
)
print(
f"میانگین: {average:,.2f}"
)
print(
"بزرگترین هزینه: "
f"{largest.get('title')} | "
f"{largest.get('amount', 0):,.2f}"
)کد کامل برنامه مدیریت هزینههای شخصی با پایتون
import json
from pathlib import Path
from datetime import date, datetime
DATA_FILE = Path("expenses.json")
CATEGORIES = (
"خوراک",
"حملونقل",
"قبوض",
"خرید",
"تفریح",
"سلامت",
"سایر",
)
def load_expenses():
if not DATA_FILE.exists():
return []
try:
with DATA_FILE.open(
"r",
encoding="utf-8",
) as file:
data = json.load(file)
except (
json.JSONDecodeError,
OSError,
):
print(
"فایل اطلاعات قابل خواندن نیست. "
"برنامه با لیست خالی شروع شد."
)
return []
return data if isinstance(data, list) else []
def save_expenses(expenses):
with DATA_FILE.open(
"w",
encoding="utf-8",
) as file:
json.dump(
expenses,
file,
ensure_ascii=False,
indent=2,
)
def next_expense_id(expenses):
if not expenses:
return 1
return max(
expense.get("id", 0)
for expense in expenses
) + 1
def get_positive_amount():
value = input(
"مبلغ هزینه: "
).strip()
try:
amount = float(value)
except ValueError:
print("مبلغ باید عدد باشد.")
return None
if amount <= 0:
print(
"مبلغ باید بیشتر از صفر باشد."
)
return None
return amount
def choose_category():
print("\nدستهبندیها:")
for index, category in enumerate(
CATEGORIES,
start=1,
):
print(f"{index}) {category}")
value = input(
"شماره دسته: "
).strip()
if not value.isdigit():
print(
"شماره دسته باید عدد باشد."
)
return None
index = int(value)
if not (
1 <= index <= len(CATEGORIES)
):
print("دسته نامعتبر است.")
return None
return CATEGORIES[index - 1]
def get_expense_date():
value = input(
"تاریخ YYYY-MM-DD "
"(خالی = امروز): "
).strip()
if not value:
return date.today().isoformat()
try:
expense_date = date.fromisoformat(
value
)
except ValueError:
print(
"تاریخ معتبر نیست. "
"نمونه: 2026-08-11"
)
return None
if expense_date > date.today():
print(
"تاریخ هزینه نمیتواند در آینده باشد."
)
return None
return expense_date.isoformat()
def add_expense(expenses):
title = input(
"عنوان هزینه: "
).strip()
if not title:
print(
"عنوان نمیتواند خالی باشد."
)
return
amount = get_positive_amount()
if amount is None:
return
category = choose_category()
if category is None:
return
expense_date = get_expense_date()
if expense_date is None:
return
expense = {
"id": next_expense_id(expenses),
"title": title,
"amount": amount,
"category": category,
"date": expense_date,
}
expenses.append(expense)
save_expenses(expenses)
print("هزینه ثبت شد.")
def show_expenses(expenses):
if not expenses:
print(
"هنوز هزینهای ثبت نشده است."
)
return
print("\n=== فهرست هزینهها ===")
for expense in expenses:
print(
f"#{expense.get('id')} | "
f"{expense.get('date')} | "
f"{expense.get('category')} | "
f"{expense.get('title')} | "
f"{expense.get('amount', 0):,.2f}"
)
def get_expense_by_id(
expenses,
expense_id,
):
for expense in expenses:
if expense.get("id") == expense_id:
return expense
return None
def delete_expense(expenses):
show_expenses(expenses)
if not expenses:
return
value = input(
"شناسه هزینه برای حذف: "
).strip()
try:
expense_id = int(value)
except ValueError:
print(
"شناسه باید عدد صحیح باشد."
)
return
expense = get_expense_by_id(
expenses,
expense_id,
)
if expense is None:
print(
"هزینهای با این شناسه پیدا نشد."
)
return
expenses.remove(expense)
save_expenses(expenses)
print("هزینه حذف شد.")
def category_report(expenses):
if not expenses:
print(
"دادهای برای گزارش وجود ندارد."
)
return
totals = {}
for expense in expenses:
category = expense.get(
"category",
"سایر",
)
totals[category] = (
totals.get(category, 0)
+ expense.get("amount", 0)
)
print(
"\n=== گزارش دستهبندی ==="
)
rows = sorted(
totals.items(),
key=lambda item: item[1],
reverse=True,
)
for category, total in rows:
print(
f"{category}: {total:,.2f}"
)
def monthly_report(expenses):
value = input(
"ماه موردنظر YYYY-MM: "
).strip()
try:
datetime.strptime(
value,
"%Y-%m",
)
except ValueError:
print(
"فرمت ماه معتبر نیست."
)
return
rows = [
expense
for expense in expenses
if expense.get(
"date",
"",
).startswith(value)
]
if not rows:
print(
"برای این ماه هزینهای ثبت نشده است."
)
return
total = sum(
expense.get("amount", 0)
for expense in rows
)
average = total / len(rows)
largest = max(
rows,
key=lambda expense: (
expense.get("amount", 0)
),
)
print(f"\n=== گزارش {value} ===")
print(
f"تعداد تراکنشها: {len(rows)}"
)
print(
f"جمع هزینه: {total:,.2f}"
)
print(
f"میانگین هر هزینه: {average:,.2f}"
)
print(
"بزرگترین هزینه: "
f"{largest.get('title')} | "
f"{largest.get('amount', 0):,.2f}"
)
def filter_by_category(expenses):
category = choose_category()
if category is None:
return
rows = [
expense
for expense in expenses
if expense.get("category") == category
]
if not rows:
print(
"در این دسته هزینهای ثبت نشده است."
)
return
show_expenses(rows)
def overall_summary(expenses):
if not expenses:
print(
"دادهای برای خلاصه وجود ندارد."
)
return
total = sum(
expense.get("amount", 0)
for expense in expenses
)
average = total / len(expenses)
largest = max(
expenses,
key=lambda expense: (
expense.get("amount", 0)
),
)
print("\n=== خلاصه کل ===")
print(
f"تعداد هزینهها: {len(expenses)}"
)
print(
f"جمع کل: {total:,.2f}"
)
print(
f"میانگین: {average:,.2f}"
)
print(
"بزرگترین هزینه: "
f"{largest.get('title')} | "
f"{largest.get('amount', 0):,.2f}"
)
def show_menu():
print(
"\n=== مدیریت هزینههای شخصی ==="
)
print("1) ثبت هزینه")
print("2) نمایش همه هزینهها")
print("3) گزارش دستهبندی")
print("4) گزارش ماهانه")
print("5) فیلتر بر اساس دسته")
print("6) خلاصه کل")
print("7) حذف هزینه")
print("0) خروج")
def main():
expenses = load_expenses()
while True:
show_menu()
choice = input(
"انتخاب شما: "
).strip()
if choice == "1":
add_expense(expenses)
elif choice == "2":
show_expenses(expenses)
elif choice == "3":
category_report(expenses)
elif choice == "4":
monthly_report(expenses)
elif choice == "5":
filter_by_category(expenses)
elif choice == "6":
overall_summary(expenses)
elif choice == "7":
delete_expense(expenses)
elif choice == "0":
print("برنامه پایان یافت.")
break
else:
print("گزینه نامعتبر است.")
if __name__ == "__main__":
main()نمونه اجرای برنامه
=== مدیریت هزینههای شخصی ===
1) ثبت هزینه
2) نمایش همه هزینهها
3) گزارش دستهبندی
4) گزارش ماهانه
5) فیلتر بر اساس دسته
6) خلاصه کل
7) حذف هزینه
0) خروج
انتخاب شما: 1
عنوان هزینه: خرید مواد غذایی
مبلغ هزینه: 850000
شماره دسته: 1
تاریخ YYYY-MM-DD (خالی = امروز):
هزینه ثبت شد.نمونه گزارش ماهانه
=== گزارش 2026-08 ===
تعداد تراکنشها: 12
جمع هزینه: 8,450,000.00
میانگین هر هزینه: 704,166.67
بزرگترین هزینه: اجاره | 3,500,000.00چرا گزارش دستهبندی مهم است؟
از دید برنامهنویسی، این بخش تمرین بسیار خوبی برای Aggregate کردن دادههاست. بهجای مشاهده تکتک رکوردها، دادهها بر اساس یک کلید مشترک گروهبندی و سپس جمع میشوند. همین الگو در سیستم فروش، گزارش سفارش، تحلیل لاگ و داشبوردهای مدیریتی نیز استفاده میشود.
اشتباههای رایج در پروژه مدیریت هزینه
ذخیره مبلغ به شکل رشته
اگر مبلغ را مستقیماً از input() ذخیره کنید، انجام محاسبات روی آن دشوار میشود. مقدار باید قبل از ذخیره به عدد تبدیل شود.
مخلوط کردن واحدهای پولی
کد واحد پول را تحمیل نمیکند، اما دادههای شما باید همگی بر اساس یک واحد ثابت باشند؛ در غیر این صورت جمع و میانگین معنای درستی نخواهند داشت.
استفاده از تاریخ آزاد بدون اعتبارسنجی
ذخیره متنهایی مثل «امروز» یا «شنبه» گزارش ماهانه را دشوار میکند. استفاده از فرمت ISO یعنی YYYY-MM-DD پردازش تاریخ را بسیار سادهتر میکند.
ذخیره نکردن بعد از تغییر
بعد از افزودن یا حذف هزینه باید فایل JSON بهروز شود تا تغییرات در اجرای بعدی باقی بمانند.
استفاده از ID برابر طول لیست
بعد از حذف رکورد ممکن است شناسه تکراری شود. تولید ID بر اساس بزرگترین شناسه موجود رفتار مطمئنتری دارد.
چطور پروژه را توسعه دهیم؟
- ثبت درآمد: یک فیلد
typeبا مقادیر expense و income اضافه کنید. - مانده مالی: مجموع درآمد منهای مجموع هزینه را محاسبه کنید.
- بودجه ماهانه: برای هر دسته سقف بودجه تعریف کنید.
- هشدار بودجه: هنگام عبور از سقف تعیینشده هشدار دهید.
- ویرایش تراکنش: امکان تغییر عنوان، مبلغ، دسته و تاریخ را اضافه کنید.
- جستوجو: هزینهها را بر اساس بخشی از عنوان جستوجو کنید.
- بازه زمانی: گزارش بین دو تاریخ مشخص ایجاد کنید.
- خروجی CSV: اطلاعات را برای Excel یا Google Sheets صادر کنید.
- نمودار: در نسخه پیشرفته با Matplotlib نمودار دستهبندیها را رسم کنید.
- SQLite: برای دادههای بیشتر JSON را با دیتابیس جایگزین کنید.
- رابط گرافیکی: نسخه Tkinter با فرم ثبت و جدول تراکنشها بسازید.
تمرین پیشنهادی: اضافه کردن درآمد
به هر رکورد یک فیلد جدید اضافه کنید:
"type": "expense"سپس امکان ثبت income را هم فراهم کنید و تابعی بسازید که نتیجه زیر را برگرداند:
balance = total_income - total_expensesبا این تغییر، پروژه از Expense Tracker به یک برنامه ساده مدیریت مالی شخصی تبدیل میشود.
این پروژه چه مفاهیمی را تمرین میدهد؟
- List و Dictionary
- Tuple
- JSON
- Pathlib
- datetime و date
- خواندن و نوشتن فایل
- توابع و پارامترها
- حلقه for و while
- شرطها
- Exception Handling
- List Comprehension
- Lambda و sorted
- sum، min، max و len
- اعتبارسنجی ورودی
- فیلتر و Aggregate دادهها
- گزارشگیری ماهانه
- ماندگاری اطلاعات
جمعبندی
در این آموزش یک برنامه کامل مدیریت هزینههای شخصی با پایتون ساختیم. هر هزینه با عنوان، مبلغ، دسته، تاریخ و ID ذخیره میشود، اطلاعات در JSON باقی میمانند و برنامه میتواند گزارش دستهبندی، گزارش ماهانه، خلاصه کل و بزرگترین هزینه را نمایش دهد.
ارزش اصلی این پروژه در عبور از یک برنامه CRUD ساده و رسیدن به تحلیل داده است. شما فقط رکورد ثبت نمیکنید؛ اطلاعات را فیلتر، گروهبندی، مرتب و خلاصه میکنید. برای توسعه بعدی، افزودن درآمد و محاسبه مانده مالی بهترین تمرین است.
📌 همچنین بخوانید: تمرین پایتون با جواب، ساخت بازی با پایتون و Pygame و ساخت ربات با پایتون