~/TechPurAI
~/tutorials/python-from-scratch/functions-and-control-flow
beginner·part 4 of 6·3 min read

Functions and control flow

Updated Aug 11, 2026Python

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:

python
def add_expense(expenses, amount, category, note):
    expense = {"amount": amount, "category": category, "note": note}
    expenses.append(expense)
    return expense

The 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:

python
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:

python
def add_expense(expenses, amount, category, note=""):
    expense = {"amount": amount, "category": category, "note": note}
    expenses.append(expense)
    return expense

Parameters with defaults must come after those without. Now both of these work:

python
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.

Never default to a list

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:

python
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.

python
print(total_for(expenses))
print(total_for(expenses, "food"))

Branching with if / elif / else

Conditionals pick one path out of several:

python
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:

python
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

raise 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

python
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.

VK

Vijay Kumar

Founder of TechPurAI — writing hands-on tutorials and honest tool breakdowns.

LinkedIn ↗
← previous3. Lists and dictionariesnext →5. Saving data to a file with JSON