The Quiet Terminal

← Beginner

Functions: parameters, returns, and the print-vs-return trap

2026-05-21 · 3 min read

The first function I ever wrote was three copy-pasted print blocks folded into a def, and it felt like getting away with something. That's still the honest appeal: less typing, one place to fix things. But functions carry one trap that catches nearly every beginner — print and return look interchangeable and are not.

The refactor that sells itself

Start with the pain. Somewhere in a script I actually wrote:

print("-" * 40)
print("REPORT: march")
print("-" * 40)

print("-" * 40)
print("REPORT: april")
print("-" * 40)

Now the same thing with a def:

def report(month, width=40):
    line = "-" * width
    print(line)
    print("REPORT:", month)
    print(line)

report("march")
report("april", width=60)

month and width are parameters — the names in the def. "march" and 60 are arguments — the values you pass in. And width=40 is a default argument: callers who don't care get the normal width, and one call can override it just for itself. The refactor isn't dramatically shorter. The win is that the separator style now lives in exactly one place instead of everywhere, forever.

The trap: print is not return

def greet(name):
    print("hello", name)     # prints — hands nothing back

message = greet("Ada")
print(message)

# hello Ada
# None

Two lines of output, and the second is the trap. greet did print its greeting — but it returned nothing, so message ended up holding None, Python's official "nothing here". A function without a return returns None, always. The symptom shows up a screen later, as the error that finally taught me the difference: TypeError: object of type 'NoneType' has no len(). That message means something up there returned None and you tried to treat it like a value — scroll up, find the function, add the return.

The fixed version looks almost identical and behaves completely differently:

def greet(name):
    return f"hello {name}"

message = greet("Ada")
print(message.upper())    # HELLO ADA

Now message holds the string, so you can upper-case it, slice it, or fold it into a longer sentence. My rule after enough crashed scripts: print is for humans, return is for code. A function that returns can be stored, tested, and handed to the next line; a function that only prints performs once and leaves nothing behind.

Why return earns its keep

def average(numbers):
    return sum(numbers) / len(numbers)

avg = average([80, 95, 67])      # 80.66666666666667
if avg >= 80:
    print("strong term:", avg)   # strong term: 80.66666666666667

The question I ask when writing a helper now isn't "should it print or return" but "what does the rest of the program need from it" — and the answer is almost always the value, not the spectacle. Printing belongs at the edge of the program, where a human is watching; the middle of the program should pass values around.

One trap while we're here: default arguments are created once, when the def line runs. def add(item, bag=[]) shares a single list between every caller, and items accumulate in it like a shared pocket. If a default should start empty each time, make it None and build a fresh list inside the function. That one has bitten me exactly once, which was enough. For the error that starts all of this — the red wall — there's a whole post on reading tracebacks.