Run this, close Python, then run it again. The count is back to zero every single time.
count = 0
count = count + 1
print('expenses saved:', count)expenses saved: 1
Nothing you put in a variable outlives the program. When it stops, the memory it used is handed back and your data is gone. A real tool has to remember. That memory lives in a file on disk, and by the end of this part your expense tracker keeps its data after you quit.
.write(). To load, open it in read mode and call .read(), or step through it one line at a time. Always open a file with a with block so Python closes it for you. For rows of data like your expenses, skip hand made text and use the csv module.This part is for the reader building along from the start, and equally for an IT person who needs a script to read and write data. It assumes Part 10, where you formatted a clean receipt with f-strings. You need Python 3 and about twenty five minutes. Every snippet runs as printed, and you can copy it and run it as is. One small warning that matters the moment you copy code with quotes in it: a webpage sometimes turns straight quotes into curly ones, and Python rejects a curly quote with a SyntaxError. If a copied line breaks, retype the quotes yourself.
Writing your first file
Opening a file gives you a handle you can read from or write to. The clean way to do it is a with block. You open the file, do your work inside the indented block, and Python closes the file the moment the block ends, even if something goes wrong partway.
with open('note.txt', 'w',
encoding='utf-8') as f:
f.write('first line\n')
f.write('second line\n')
print('done')done
Three things are happening. The 'w' means write mode. The encoding='utf-8' tells Python how to turn text into bytes, and utf-8 is the sane default that handles every language and symbol. The n at the end of each string is a newline, because .write() puts down exactly the characters you give it and nothing more. Leave the n out and both lines run together.
Figure 1. Writing sends text out to a file. Reading pulls it back on the next run.
Reading it back
Open the same file in read mode, which is the default, so you can leave the mode out. The simplest read pulls the whole file into one string with .read().
with open('note.txt',
encoding='utf-8') as f:
text = f.read()
print(text)first line second line
For a big file you rarely want it all at once. Looping over the file hands you one line at a time, which stays light no matter how large the file grows. Each line still carries its trailing newline, so .rstrip() trims it off for a clean print.
with open('note.txt',
encoding='utf-8') as f:
for line in f:
print(line.rstrip())first line second line
That trailing newline is the single most common surprise with files. If your printed lines have odd blank gaps, or a comparison like line == 'done' never matches, the leftover n is the reason. Strip it and the problem disappears.
The three modes you actually need
The letter you pass to open() decides what it will let you do, and what it does to a file that is already there. Getting this wrong is how beginners lose data, so it is worth a table.
| Mode | Opens for | If the file exists | If it is missing |
|---|---|---|---|
'r' | reading | reads it | raises an error |
'w' | writing | wipes it first | creates it |
'a' | appending | adds to the end | creates it |
'x' | writing | refuses, raises an error | creates it |
Table 1. The open modes, and what each one does to an existing file.
'w' empties the file the instant it opens, before you write a single thing. Point 'w' at a file you care about and its old contents are gone. When you want to add to a file instead of replacing it, use 'a':
with open('log.txt', 'a',
encoding='utf-8') as f:
f.write('one more line\n')'w' version five times and you get one.Figure 2. Write mode starts blank. Append mode keeps what was there and adds after it.
When the file is not there
Try to read a file that does not exist and Python stops with a traceback. Read it the way Part 2 taught: last line first.
Traceback (most recent call last):
File "read.py", line 1, in <module>
open('missing.txt', encoding='utf-8')
FileNotFoundError: [Errno 2] No such
file or directory: 'missing.txt'The last line names the problem plainly. FileNotFoundError means Python looked for that file in the folder your program is running from and did not find it. Usually the file name is misspelled, or it lives in a different folder than you think. This is a normal message, not a crash to fear, and it is telling you exactly what went wrong.
The fix for a tracker is to check before you read. If the save file is not there yet, start with an empty list instead of failing:
from pathlib import Path
path = Path('expenses.csv')
if path.exists():
print('found it, loading')
else:
print('no file yet, starting fresh')no file yet, starting fresh
Paths that work on every operating system
Freshers write code on Windows, macOS and Linux, and file paths are one place they differ. Windows separates folders with a backslash, as in dataexpenses.csv. macOS and Linux use a forward slash, as in data/expenses.csv. If you hard code one style, your code breaks on the other machine.
The standard library solves this with pathlib. You build a path with the / operator and Python prints the right separator for whatever system runs it:
from pathlib import Path
data = Path('data') / 'expenses.csv'
print(data)data/expenses.csv
On Windows that same code prints dataexpenses.csv. You wrote it once and it is correct everywhere. Path objects also carry handy checks like .exists() and readers like .read_text(), so you lean on them instead of gluing strings together by hand.
CSV, the right tool for rows of data
You could save each expense as a line of text and split it back on commas, the way Part 10 showed. It works until an expense note contains a comma, and then your split cuts the line in the wrong place and the data is wrong. The standard library ships a module built exactly for rows of fields, and it handles those edge cases for you. It is called csv.
Here is my honest opinion, and it runs against the advice you will see in tutorials: do not import pandas to read a ten line file. Pandas is a fine library for large tables and analysis, but pulling in a heavy dependency to save a handful of expenses is the wrong reach. The csv module comes with Python, needs no install, and is the right size for this job. Reach for the small tool that fits.
csv.DictWriter writes each dictionary as a row, and csv.DictReader reads each row back as a dictionary. You tell the writer the field names once:
import csv
rows = [
{'amount': 120.5, 'category': 'food'},
{'amount': 40, 'category': 'travel'},
]
with open('expenses.csv', 'w', newline='',
encoding='utf-8') as f:
fields = ['amount', 'category']
writer = csv.DictWriter(f,
fieldnames=fields)
writer.writeheader()
writer.writerows(rows)
print('saved', len(rows), 'rows')saved 2 rows
The newline='' looks odd but matters: it stops the csv module from leaving blank lines between rows on Windows. Always pass it when you open a file for csv. The file it writes is plain text you can open in any editor or a spreadsheet:
amount,category 120.5,food 40,travel
Figure 3. A with block opens the file, lets you work, and closes it even if an error happens.
Reading it back is the mirror image. DictReader uses the header row for the keys and hands you one dictionary per line:
import csv
with open('expenses.csv', newline='',
encoding='utf-8') as f:
reader = csv.DictReader(f)
rows = list(reader)
for row in rows:
print(row){'amount': '120.5', 'category': 'food'}
{'amount': '40', 'category': 'travel'}Look closely at the amounts. They came back as text, '120.5' in quotes, not numbers. Every value read from a file arrives as a string. You convert with float() or int() before you do any maths. That conversion is exactly where things can go wrong if the file holds a typo, and handling that safely is the whole of Part 12.
expenses.csv and reads it back next time it starts. This is the moment the project stops being a toy.
import csv
from pathlib import Path
FIELDS = ['amount', 'category']
PATH = Path('expenses.csv')
def save_expenses(items):
with open(PATH, 'w', newline='',
encoding='utf-8') as f:
w = csv.DictWriter(f,
fieldnames=FIELDS)
w.writeheader()
w.writerows(items)
def load_expenses():
if not PATH.exists():
return []
with open(PATH, newline='',
encoding='utf-8') as f:
return list(csv.DictReader(f))
expenses = load_expenses()
expenses.append(
{'amount': 55, 'category': 'books'})
save_expenses(expenses)
print('now have', len(expenses), 'expenses')now have 1 expenses
with open(...) pattern and knowing that 'w' wipes while 'a' adds are daily tools, not trivia. An engineer who reaches for a with block by reflex and never leaves a file open looks like someone who has done this before. It is a small habit that shows up in code review on day one.with statement instead of calling open() and close() ourselves?" The answer they want: the with block closes the file automatically when the block ends, even if an error is raised inside it, so you never leak an open file or lose unwritten data. A strong follow up is that a file left open can hold a lock or lose buffered writes. It is a quick way for them to see whether you understand cleanup, not just reading and writing.hello.txt, the words hello and world, each on its own line. Then read the file back and print it. Expected output:
hello world
Show solution
with open('hello.txt', 'w',
encoding='utf-8') as f:
f.write('hello\n')
f.write('world\n')
with open('hello.txt',
encoding='utf-8') as f:
print(f.read().rstrip())Write mode with two .write() calls, each ending in n, then a second block to read it back. The .rstrip() trims the final newline so there is no trailing blank line.
File questions that come up early
Where does the file get saved if I only give a name? In the folder your program is running from, called the working directory. Give a full path, or build one with pathlib, when you want it somewhere specific.
Do I have to close the file myself? Not if you use a with block, which closes it for you. If you ever call open() on its own, you are responsible for calling .close(), which is one more reason to prefer with.
What is the difference between .read() and looping over the file? .read() pulls the entire file into memory as one string. Looping hands you one line at a time and stays light, which is what you want for anything large.
Why did my numbers come back as text? Everything read from a file is a string. Convert with int() or float() before doing maths. This is normal and expected.
Should I use CSV or JSON to save data? CSV suits flat rows like a table. JSON suits nested data with lists inside dictionaries. Your tracker is flat rows, so CSV fits now. Part 16 shows JSON for when the shape gets richer.
Your tracker finally remembers. Next comes the part that keeps it from falling over when someone types nonsense into it: reading errors, catching them, and keeping the program alive. Save your expenses.py and bring it to Part 12.
References
Python docs: Reading and writing files
Python docs: csv module
Python docs: pathlib


DrJha