Programming with AI
ENRU
2.5

Functions

in, out, and what happens on the side

What it is

To read a function you do not have to take its insides apart. It is enough to answer three questions: what comes in, what goes out, and what changes outside.

The third question is the most important and the most often skipped. A function can return nothing at all and still turn half the program upside down.

What it looks like in code

Diagram from the book — to be redrawn for the web
A function is read along three directions. Side effects are what happens in addition — and what people usually forget to check.
# Pure function: same input -> same output, nothing else
def format_price(amount: float, currency: str = "NOK") -> str:
    return f"{amount:.2f} {currency}"


# Side effect: changes what it was given
def add_tax(items: list[dict], rate: float) -> None:
    for item in items:
        item["price"] = round(item["price"] * (1 + rate), 2)


# Side effect: writes to the outside world
def log_order(order_id: int) -> None:
    with open("orders.log", "a", encoding="utf-8") as f:
        f.write(f"order {order_id}\n")

A pure function

format_price takes a number and a string and returns a string. Call it a hundred times with the same arguments and you get the same thing a hundred times, and nothing else happens.

Functions like this are the easiest to check: compare the input with the output and you are done.

A function with a side effect

add_tax returns None, but it does do work — it changes the dictionaries in the list it was given. Here is what follows from that:

cart = [{"name": "coffee", "price": 100.0}]
add_tax(cart, 0.25)
print(cart)     # [{'name': 'coffee', 'price': 125.0}]
add_tax(cart, 0.25)
print(cart)     # [{'name': 'coffee', 'price': 156.25}]

The second call charged tax on the tax. The function is not protected against being called twice, and it will not tell you so — not by its name, not by its signature.

Where to look for side effects

Run your eye down the body of the function and look for four things:

SignWhat is happening
assigning into something that arrived as an argumentchanges the caller's data
open(), writing to a filechanges the disk
conn.execute(), INSERT, UPDATEchanges the database
requests, await session.post()changes something on another server
global, assigning to a module variablechanges the program's state

If none of these is there and a return is, the function is pure and life is simpler.

Default arguments

def format_price(amount: float, currency: str = "NOK") -> str:

currency may be omitted, and then it is "NOK". Easy to read, but there is a trap the AI reproduces regularly:

def add_item(item, cart=[]):    # don't do this
    cart.append(item)
    return cart

A list in a default value is created once — and stays shared across every call. The second add_item("bread") returns a list of two items. The right way is cart=None, and if cart is None: cart = [] inside.

What to tell the AI

Prompt
For this function answer three questions:
what it takes, what it returns, what it changes outside itself.
One line per question.
Prompt
What happens if I call this function twice in a row
with the same arguments?

The second question catches exactly the class of bug in the tax example.

What to look for

A function that both returns and changes. It does two jobs at once, which makes it hard to reuse and easy to call at the wrong moment. Usually worth splitting.

The name promises the wrong thing. get_user should only get. If a database record is created inside get_user, the name is lying, and the next reader will trip over it.

A mutable default value. =[] or ={} in the arguments is almost always a bug.

Too many arguments. More than five is a sign the function is doing several jobs. Check whether it should be split.

Cheat sheet

  • Three questions: what it takes · what it returns · what it changes outside.

  • Pure function: same input, same output, and nothing else.

  • -> None means all the work is in the side effects.

  • Look for side effects in: writes to arguments, files, the database, the network, global.

  • =[] and ={} as default arguments are a bug.

  • Ask: "what happens if I call it twice in a row?"