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
# 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:
| Sign | What is happening |
|---|---|
| assigning into something that arrived as an argument | changes the caller's data |
open(), writing to a file | changes the disk |
conn.execute(), INSERT, UPDATE | changes the database |
requests, await session.post() | changes something on another server |
global, assigning to a module variable | changes 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 cartA 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
For this function answer three questions:
what it takes, what it returns, what it changes outside itself.
One line per question.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.
-> Nonemeans 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?"