Lesson 8 of 16 · Level 2: Intermediate
Python Files, Error Handling and Your First Project
- ⏱ 14 min read
- ❓ 4 review questions
- ✍️ Exercise with solution
- 📥 Downloadable template
In this lesson
- Handle errors with try/except.
- Read and write text, CSV and JSON files.
- Build a complete command-line expense tracker.
- Wrap up the Intermediate level with a real project.
- Process a CSV with broken rows without crashing the program.
Put the file in the same folder as your program to test reading the CSV and the summary by category.
- expenses.csv · 40 sample household expenses for the expense tracker
This lesson closes the Intermediate level, and it's where your programs become genuinely useful: they'll save and read data in files so nothing is lost when they close, and they'll handle errors so they don't crash when something goes wrong. At the end you'll build a complete project: a personal expense tracker that stores everything in a CSV file you can open in Excel or Google Sheets.
"r", write "w" and append "a"expenses.csv so you can test the projectwith block is all it takes for a file to close itself, even if an error happensHandling errors with try / except
When something fails (the user types letters where you expected a number, a file doesn't exist, you divide by zero), Python raises an exception and the program stops. With try/except you catch it and decide what to do:
try:
age = int(input("Age: "))
print(f"Next year you'll be {age + 1}")
except ValueError:
print("That's not a whole number")
The most common exceptions
| Exception | When it happens |
|---|---|
ValueError | A conversion that can't work: int("hello") |
ZeroDivisionError | Dividing by zero |
KeyError | A missing key in a dictionary |
IndexError | A missing position in a list |
FileNotFoundError | The file doesn't exist |
TypeError | An operation with incompatible types: "5" + 3 |
The full structure
try:
a = float(input("Dividend: "))
b = float(input("Divisor: "))
result = a / b
except ValueError:
print("Please enter valid numbers")
except ZeroDivisionError:
print("You can't divide by zero")
else:
print(f"Result: {result:.2f}") # only if there was no error
finally:
print("Done") # always runs
With 10 and 0 you get "You can't divide by zero" and then "Done"; with 10 and 4, "Result: 2.50" and "Done." Python always walks through the structure in the same order:
- tryAttempt the codeIf a line fails, Python jumps straight to the matching
except; the rest of thetryis skipped. - exceptHandle the expected errorOnly the block whose type matches (
ValueError,ZeroDivisionError…). If none matches, the program still stops. - elseContinue if all went wellRuns only when the
tryfinished without errors. - finallyAlways wrap upNo matter what. Ideal for closing messages or releasing resources.
Ask for a number until it's valid
Here's a function you'll reuse in almost every program. It even accepts amounts typed as "$1,024.50":
def ask_amount(prompt):
while True:
text = input(prompt).strip().replace("$", "").replace(",", "")
try:
return float(text)
except ValueError:
print("Invalid value, please try again.")
Avoid a bare except: or except Exception: on its own, because it hides programming bugs you need to see. Catch the specific exceptions you expect.
1. Which exception does int("hello") raise?
ValueError appears when the type is right (a string) but its content can't be converted.
Reading and writing text files
The right way to work with files is with with, which closes them automatically even if there's an error:
# Write (creates the file or overwrites it)
with open("notes.txt", "w", encoding="utf-8") as f:
f.write("First line\n")
f.write("Second line\n")
# Append to the end without erasing what's there
with open("notes.txt", "a", encoding="utf-8") as f:
f.write("Third line\n")
# Read line by line
with open("notes.txt", "r", encoding="utf-8") as f:
for line in f:
print(line.strip())
"r" · read
The default mode. If the file doesn't exist, it raises FileNotFoundError.
"w" · write
Creates the file or erases its contents and writes from scratch.
"a" · append
Writes at the end and keeps everything already there. The expense tracker uses it.
Always pass encoding="utf-8": it prevents trouble with characters like "é" or emoji, especially on Windows. Starting with Python 3.15, UTF-8 is the default encoding, but writing it explicitly keeps your program working the same way on older versions.
2. You open an existing file in "w" mode. What happens?
"w" overwrites; to add to the end without erasing, use "a".
CSV files: data shaped like a table
CSV is the universal format for exchanging tables: Excel, Google Sheets, QuickBooks exports and every database can read it. Python includes the csv module:
import csv
expenses = [
{"date": "2026-10-01", "category": "food", "amount": 35.5},
{"date": "2026-10-02", "category": "transportation", "amount": 12.0},
]
# Write
with open("my_expenses.csv", "w", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=["date", "category", "amount"])
writer.writeheader()
writer.writerows(expenses)
# Read
with open("my_expenses.csv", newline="", encoding="utf-8") as f:
for row in csv.DictReader(f):
print(row["category"], float(row["amount"]))
When you read a CSV, every value arrives as text, so convert numbers with float(). If Excel shows "Café" instead of "Café" when you open your file, write it with encoding="utf-8-sig", which adds a small marker Excel uses to recognize UTF-8.
JSON files: structured data
JSON stores dictionaries and lists exactly as they are. It's the format of web APIs and many configuration files:
import json
contacts = {"Zoe Kim": "312-555-0199", "Adam Lee": "312-555-0123"}
with open("contacts.json", "w", encoding="utf-8") as f:
json.dump(contacts, f, ensure_ascii=False, indent=2)
with open("contacts.json", encoding="utf-8") as f:
contacts = json.load(f)
That solves the contact book problem from lesson 6: load the file when the program starts and save it when the user quits.
3. You want to save a dictionary that contains lists and load it back exactly as it was. What do you use?
JSON keeps the structure of dictionaries and lists; CSV is for flat tables.
Real case: a CSV with broken rows
Real files contain mistakes: a blank price, a quantity typed as a word. If your program stops at the first bad row, it's useless for daily work. An office supply store in Raleigh, North Carolina, exports its sales from a spreadsheet; this program adds up the valid rows and records which ones need checking:
import csv
# 1) Create a sample CSV: sales at an office supply store in Raleigh, NC
rows = """date,product,quantity,price
2026-03-02,Spiral notebook,12,3.50
2026-03-02,Blue pens 12-pack,30,4.25
2026-03-03,Backpack,2,
2026-03-03,Colored pencils,5,6.99
2026-03-04,Ruler 12 in,three,1.50
"""
with open("store_sales.csv", "w", encoding="utf-8") as f:
f.write(rows)
# 2) Process it without letting one bad row stop everything
total = 0
errors = []
with open("store_sales.csv", newline="", encoding="utf-8") as f:
for line, row in enumerate(csv.DictReader(f), start=2):
try:
amount = int(row["quantity"]) * float(row["price"])
except ValueError:
errors.append(line)
continue
total += amount
print(f"Total of valid rows: ${total:,.2f}")
print(f"Check these lines: {errors}")
Total of valid rows: $204.45
Check these lines: [4, 6]
Three design decisions worth copying:
- The
trywraps only the line that can fail, not the whole loop, so you know exactly what broke. continueskips the bad row and processing moves on: $42.00 + $127.50 + $34.95 = $204.45 in valid sales.enumerate(..., start=2)numbers from 2 because line 1 is the header. That way "line 4" matches row 4 in Excel, and whoever checks finds the backpack with no price and the ruler with "three" instantly.
4. Your program shows FileNotFoundError: [Errno 2] No such file or directory: 'expenses.csv', but the file is sitting in your Downloads folder. What's the most likely cause?
A path like "expenses.csv" is relative to the folder you run the program from. Put the CSV next to your .py file and run it from that folder, or use the full path. An encoding problem would raise UnicodeDecodeError, not FileNotFoundError.
Intermediate project: a personal expense tracker
This program brings together everything so far: variables, conditionals, loops, dictionaries, functions, files and error handling. It records expenses, shows a summary by category and saves everything to expenses.csv.
import csv
from datetime import date
from pathlib import Path
FILE = Path("expenses.csv")
FIELDS = ["date", "category", "description", "amount"]
def load_expenses():
"""Read the CSV and return a list of dictionaries."""
if not FILE.exists():
return []
with open(FILE, newline="", encoding="utf-8") as f:
expenses = list(csv.DictReader(f))
for e in expenses:
e["amount"] = float(e["amount"])
return expenses
def save_expense(expense):
"""Append one expense to the CSV (creating the file if needed)."""
is_new = not FILE.exists()
with open(FILE, "a", newline="", encoding="utf-8") as f:
writer = csv.DictWriter(f, fieldnames=FIELDS)
if is_new:
writer.writeheader()
writer.writerow(expense)
def ask_amount(prompt):
while True:
try:
value = float(input(prompt).strip().replace("$", "").replace(",", ""))
if value > 0:
return value
print("The amount must be greater than 0.")
except ValueError:
print("Please enter a valid number.")
def add_expense():
category = input("Category (food, transportation, housing...): ").strip().lower() or "other"
description = input("Description: ").strip()
amount = ask_amount("Amount: $")
expense = {"date": date.today().isoformat(), "category": category,
"description": description, "amount": amount}
save_expense(expense)
print("✓ Expense saved")
def summary(expenses):
if not expenses:
print("No expenses yet.")
return
total = sum(e["amount"] for e in expenses)
by_category = {}
for e in expenses:
by_category[e["category"]] = by_category.get(e["category"], 0) + e["amount"]
print(f"\n{'Category':16}{'Amount':>10}{'%':>8}")
print("-" * 34)
for cat, amount in sorted(by_category.items(), key=lambda x: x[1], reverse=True):
print(f"{cat:16}{amount:10,.2f}{amount / total:8.1%}")
print("-" * 34)
print(f"{'TOTAL':16}{total:10,.2f}")
biggest = max(expenses, key=lambda e: e["amount"])
print(f"\nBiggest expense: {biggest['description']} (${biggest['amount']:,.2f})")
def main():
while True:
option = input("\n1) Add expense 2) View summary 3) Quit: ").strip()
if option == "1":
add_expense()
elif option == "2":
summary(load_expenses())
elif option == "3":
print("See you next time!")
break
else:
print("Invalid option")
if __name__ == "__main__":
main()
Put the practice file expenses.csv in the same folder, choose option 2, and you'll see this summary of the 40 sample expenses:
Category Amount %
----------------------------------
food 739.41 42.3%
housing 404.31 23.1%
entertainment 311.98 17.9%
transportation 291.38 16.7%
----------------------------------
TOTAL 1,747.08
Biggest expense: Electric bill ($128.37)
- Food (42.3%) $739.41Groceries, lunches and coffee shops: 18 of the 40 expenses.
- Housing (23.1%) $404.31Electric bills, internet and cleaning supplies.
- Entertainment (17.9%) $311.98
- Transportation (16.7%) $291.38
summary() function calculates with the :8.1% format.What you just used
pathlib.Pathto check whether the file exists.or "other": if the user leaves the category blank, a default is used (an empty string is falsy).- Column formatting with f-strings:
{cat:16}reserves 16 characters,{amount:10,.2f}right-aligns with commas and{x:8.1%}shows a percentage. sorted(..., key=..., reverse=True)to rank categories from highest to lowest spending.
Common mistakes with files and exceptions
If you open a CSV that Excel saved on Windows in its older encoding, a single "é" breaks the read:
UnicodeDecodeError: 'utf-8' codec can't decode byte 0xe9 in position 14: invalid continuation byte
Byte 0xe9 is "é" in the Windows encoding (cp1252). Open it with encoding="cp1252" or, better, re-save the file from Excel as "CSV UTF-8."
Writing except: pass "so it doesn't crash."
The program stops crashing but silently gives wrong results. Catch the specific error and report or log it.
Opening your history file with "w".
To add records use "a"; "w" erases everything.
Adding up CSV values as they arrive.
Everything arrives as text: convert with float() or you'll see TypeError: unsupported operand type(s) for +: 'int' and 'str'.
Forgetting newline="" when writing a CSV.
On Windows you'd get blank rows between records. Always use it with the csv module.
Challenges to improve the project
- Add an option to see only the current month's expenses (hint: compare
e["date"][:7]withdate.today().isoformat()[:7]). - Define a budget per category in a dictionary and warn when a category goes over.
- Add an option to delete the last expense (you'll have to rewrite the whole file).
- Install pandas (
pip install pandas) and build the summary withpd.read_csv("expenses.csv").groupby("category")["amount"].sum().
See solution
def this_months_expenses(expenses):
current_month = date.today().isoformat()[:7] # "2026-11"
return [e for e in expenses if e["date"][:7] == current_month]
# In the menu:
# elif option == "4":
# summary(this_months_expenses(load_expenses()))
This solves challenge 1. Comparing the first 7 characters works because ISO dates always start with "YYYY-MM."
Summary
try/exceptcatches specific errors;elseandfinallycomplete the structure.- Always open files with
withandencoding="utf-8"; use"a"to append and"w"only to overwrite. - Use CSV for tables (
csvmodule) and JSON for structured data (jsonmodule). - Wrap only the risky line in
tryand usecontinueto skip bad rows. - The expense tracker combines everything you've learned into a real program.
You've completed the Intermediate level! The Advanced level starts now.
Next lesson: Python Classes and Objects: Object-Oriented Programming. You'll turn your expense tracker's dictionaries into classes with their own attributes and methods.
Create your account in 30 seconds, mark your lessons and download your certificate at the end.