Chapter 11
Putting It Together
Ten chapters, one idea at a time: values, names, decisions, loops,
collections, functions, modules, files, and handling what breaks. This
chapter assembles them into one real script, track.py, that loads your
expense history, lets you add to it, and saves it back.
The shape
Every version of this script does the same three things, in order: load what's already there, act on it, save the result. That's the whole plan. Everything below is one of those three steps, or a small piece that supports one of them.
The functions, assembled
These are every one of them from earlier chapters, gathered in one place.
Nothing here is new, and notice as you read: not one of these functions
calls input(). Each takes exactly the values it needs as arguments and
hands back a result with return, which is exactly why every one of them
can be tested for real, right here, the same way the rest of this book has
been.
def load_expenses(path):
expenses = []
try:
with open(path) as f:
for line in f:
line = line.strip()
if not line:
continue
raw_amount, category, note = line.split(",")
expenses.append({
"amount": float(raw_amount),
"category": category,
"note": note,
})
except FileNotFoundError:
pass
return expenses
def save_expenses(path, expenses):
with open(path, "w") as f:
for entry in expenses:
f.write(f"{entry['amount']},{entry['category']},{entry['note']}\n")
def add_expense(expenses, amount, category, note=""):
expenses.append({"amount": amount, "category": category, "note": note})
return expenses
def total(expenses):
running = 0
for entry in expenses:
running += entry["amount"]
return running
def expenses_over(expenses, amount):
matches = []
for entry in expenses:
if entry["amount"] > amount:
matches.append(entry)
return matches
One more, new but built from the same pieces as everything above: a short summary of a list of entries.
def report(expenses):
lines = [f"{len(expenses)} expenses, total {total(expenses):.2f}"]
by_category = {}
for entry in expenses:
by_category[entry["category"]] = (
by_category.get(entry["category"], 0) + entry["amount"]
)
for category, amount in by_category.items():
lines.append(f" {category}: {amount:.2f}")
return "\n".join(lines)
report() reuses total() rather than adding up amounts a second way, and
builds up a dict of per-category totals with the same "default, then update"
pattern from Chapter 9's practice. f"{amount:.2f}" is new: the :.2f part
formats a number to exactly two decimal places, which is why every dollar
amount below prints with two digits even when the underlying float doesn't
carry a trailing zero.
One more small function, using the by-category totals report() already
builds, to show which category is costing the most:
def top_category(expenses):
by_category = {}
for entry in expenses:
by_category[entry["category"]] = (
by_category.get(entry["category"], 0) + entry["amount"]
)
return max(by_category, key=by_category.get)
max() normally compares the values you hand it directly, which would try
to compare category names alphabetically here, not what you want.
key=by_category.get tells max() to compare each category by looking up
its total in the dict first, and hand back whichever key had the largest
one. This is the same key= idea sorted() and .sort() use when you want
to sort by something other than the plain values themselves.
Trying it end to end
Load the fixture history, add one new entry, and check the whole thing works
together, all still with no input() anywhere in sight:
expenses = load_expenses("expenses.txt")
add_expense(expenses, 6.00, "coffee", "treat")
print(report(expenses))
6 expenses, total 108.35
coffee: 19.25
groceries: 77.10
transit: 12.00big = expenses_over(expenses, 20)
print(len(big))
2print(top_category(expenses))
groceriessave_expenses("final.txt", expenses)
reloaded = load_expenses("final.txt")
print(len(reloaded))
6Everything above ran, checked, and passed, exactly like every other example
in this book. That's the whole point of keeping input() out of these
functions: there was nothing here that couldn't be checked.
The part that talks to a person
Here's the rest of track.py: a main() function that asks for one new
expense and wires it into everything above, wrapped in the main-guard from
Chapter 8. This is the one piece of the script this book's build can't run
for you, because it genuinely needs someone at a keyboard.
def main():
path = "expenses.txt"
expenses = load_expenses(path)
raw_amount = input("Amount: ")
category = input("Category: ")
note = input("Note (optional): ")
try:
amount = float(raw_amount)
except ValueError:
print(f"'{raw_amount}' doesn't look like a number, nothing saved.")
return
add_expense(expenses, amount, category, note)
save_expenses(path, expenses)
print()
print(report(expenses))
if __name__ == "__main__":
main()
$ python track.py
Amount: 6.00
Category: coffee
Note (optional): treat
6 expenses, total 108.35
coffee: 19.25
groceries: 77.10
transit: 12.00Look at how little main() actually does: it calls three functions to
collect input, one try/except to convert and validate it, and two more
functions, already tested above, to record and save it. Nearly the entire
script is the tested part. Only the handful of lines that talk to a person
are the untested part, and that's exactly the boundary this book has been
building toward since Chapter 2 first raised it.
Where this leaves you
You've read the complete listing of a real, working Python script and seen
every part of it doing a job you understand: loading a file, converting and
validating input, updating a list of dictionaries, handling a bad value
without crashing, and saving the result. Nothing in track.py is more
advanced than what the last ten chapters covered. The closing chapter has a
few ways to extend it yourself, and points to where the parts you didn't
cover here, classes, decorators, a real database, live in the rest of the
series.