Functions and control flow
The script works, but adding an expense and totalling one are written inline and can only happen in the order you typed them. Functions give those blocks names and let you call them whenever you need — including from the command-line interface you'll build in part 6.
Defining a function
def names a block of code and lists what it needs:
def add_expense(expenses, amount, category, note):
expense = {"amount": amount, "category": category, "note": note}
expenses.append(expense)
return expenseThe body is indented — Python uses indentation instead of braces, and inconsistent indentation is a syntax error rather than a style complaint. return hands a value back to the caller; without it a function returns None.
Calling it looks like this:
expenses = []
add_expense(expenses, 12.5, "food", "lunch")
add_expense(expenses, 40.0, "transport", "train ticket")
print(len(expenses))Default arguments
Not every expense needs a note. Give the parameter a default and the caller can skip it:
def add_expense(expenses, amount, category, note=""):
expense = {"amount": amount, "category": category, "note": note}
expenses.append(expense)
return expenseParameters with defaults must come after those without. Now both of these work:
add_expense(expenses, 8.75, "food")
add_expense(expenses, 8.75, "food", note="coffee beans")Naming the argument at the call site (note="coffee beans") is worth the extra characters on anything that isn't obvious — it survives someone reordering the parameters later.
def add_expense(expenses=[], ...) looks reasonable and is a real bug: the default list is created once when the function is defined, so every call that relies on it shares — and keeps appending to — the same list. Use None as the default and create a fresh list inside the function instead.
Returning computed values
Give the totalling logic a name too. A function that returns a value is far more useful than one that prints, because the caller decides what to do with the result:
def total_for(expenses, category=None):
if category is None:
return sum(e["amount"] for e in expenses)
return sum(e["amount"] for e in expenses if e["category"] == category)Called with one argument it totals everything; called with a category it totals just that one. Note is None rather than == None — is compares identity, and it's the standard way to test for None because it can't be fooled by a class that defines its own equality.
print(total_for(expenses))
print(total_for(expenses, "food"))Branching with if / elif / else
Conditionals pick one path out of several:
def describe(amount):
if amount >= 100:
return "large"
elif amount >= 20:
return "medium"
else:
return "small"Python checks each condition top to bottom and runs the first that's true, so ordering matters: swap the first two branches and every amount over 100 would be reported as medium, because amount >= 20 is also true for 250.
Guarding against bad input
Functions are the natural place to reject nonsense before it reaches your data:
def add_expense(expenses, amount, category, note=""):
if amount <= 0:
raise ValueError("Amount must be greater than zero")
if not category:
raise ValueError("Category is required")
expense = {"amount": amount, "category": category, "note": note}
expenses.append(expense)
return expenseraise stops the function and reports the problem to the caller. not category is true for an empty string, which is Python's way of treating empty values as false — the same test covers "" and a missing value without a separate check for each.
The script so far
def add_expense(expenses, amount, category, note=""):
if amount <= 0:
raise ValueError("Amount must be greater than zero")
if not category:
raise ValueError("Category is required")
expense = {"amount": amount, "category": category, "note": note}
expenses.append(expense)
return expense
def total_for(expenses, category=None):
if category is None:
return sum(e["amount"] for e in expenses)
return sum(e["amount"] for e in expenses if e["category"] == category)
expenses = []
add_expense(expenses, 12.5, "food", "lunch")
add_expense(expenses, 40.0, "transport", "train ticket")
print(f"Total: ${total_for(expenses):.2f}")Everything still disappears when the script exits. That's the next part: writing the list to a file and reading it back.