Python10 min readCode verified

Functions: return vs print, and Where Your Variables Went

Two mistakes account for most of the marks lost on functions in a first Python course, and neither one looks like a mistake on screen. Every result on this page was executed, not remembered.

Defining is not running#

A def statement does not execute the body. It creates the function and moves on. The body runs only when something calls it:

def greet():
    print("hi")

print("start")
greet()
print("end")
start
hi
end

"hi" is not printed first even though it appears first in the file. Python read the def, remembered it, and carried on to print("start"). Only at greet() does it detour into the body, and when the body finishes it comes back to exactly where it left off.

That detour-and-return is the whole flow-of-execution idea, and it is what makes the rest of this page make sense.

Parameter vs. argument#

These two words get used interchangeably in conversation and separately on exams, so they are worth thirty seconds:

TermWhere it livesExample
Parameterin the def line — a placeholder namedef area(side):side
Argumentat the call — the actual value handed overarea(10)10

One sentence version: you define parameters and you pass arguments.

return vs. print — the expensive one#

These two look interchangeable while you are testing by eye, because both put the number on your screen. They are not remotely the same thing:

def square_p(x):
    print(x * x)

def square_r(x):
    return x * x

a = square_p(5)
b = square_r(5)
print(a)
print(b)
25
None
25

Read that output carefully, because all three lines matter. The 25 on top came from inside square_p — it was printed the moment the function ran. Then print(a) gives None, because square_p never handed anything back. Then print(b) gives 25, the value square_r returned.

print shows a value to a person. return hands a value back to the code. If you want to use a result — add it to something, compare it, test it, put it in a list — it has to be returned.

Why this costs marks specifically

A function that prints looks completely correct when you run it by hand. It fails the moment anything checks the result — an assert, an autograder, or your own later code. The screen said 25 and the test still failed, which is a genuinely confusing place to be until you know this distinction.

Every function returns something#

def add(a, b):
    total = a + b

result = add(2, 3)
print(result)
None

The addition happened. The answer was stored in total. Then the function ended, total ceased to exist, and the caller got None — which is what Python returns when a function falls off the end of its body without a return.

return also stops the function immediately, which is easy to forget:

def classify(n):
    if n < 0:
        return "negative"
    print("checking...")
    return "non-negative"

print(classify(-5))
print(classify(5))
negative
checking...
non-negative

The first call never reaches print("checking..."). Nothing after a return on the path taken will run.

Local variables, and where they go#

A variable created inside a function is local to it. Its scope — where the name is visible — is the function body, and its lifetime ends when the call ends:

def f():
    x = 10

f()
print(x)
Traceback (most recent call last):
  File "scope.py", line 5, in <module>
    print(x)
          ^
NameError: name 'x' is not defined

x genuinely existed, briefly. By the time line 5 runs it is gone. This is not a restriction to work around — it is the feature that lets you use i or total in fifty different functions without them colliding. The way to get a value out is to return it.

Globals, and the error that makes no sense#

A variable defined outside every function is global, and a function can read it:

count = 0

def show():
    print(count)

show()
0

Try to change it, though, and you get one of the strangest-looking errors in Python:

count = 0

def bump():
    count = count + 1

bump()
Traceback (most recent call last):
  File "bump.py", line 6, in <module>
    bump()
  File "bump.py", line 4, in bump
    count = count + 1
            ^^^^^
UnboundLocalError: cannot access local variable 'count' where it is not associated with a value

The global count is right there, and Python says it cannot access it. The reason: assigning to a name anywhere in a function makes that name local for the entire function, including on lines before the assignment. So count + 1 is trying to read a local count that has not been given a value yet.

The keyword global overrides that:

count = 0

def bump():
    global count
    count = count + 1

bump()
print(count)
1

It works, and you should still almost never use it. A function that reaches out and edits globals is hard to test (its result depends on state you cannot see at the call), hard to reuse anywhere else, and hard to debug — when the value is wrong, every function that touches it is a suspect. Pass values in as arguments and hand results back with return.

What happens to what you pass in#

This is the classic trace question, and both halves are on the same page for a reason:

def bump(n):
    n += 1

def add_item(items):
    items.append("new")

number = 5
things = ["a"]
bump(number)
add_item(things)
print(number)
print(things)
5
['a', 'new']

Same shape, opposite outcomes. The parameter is another name for the thing you passed — so items.append(...) reaches the caller's actual list, while n += 1 on a number simply points the local name n somewhere else and leaves number alone. Numbers, strings and tuples cannot be changed in place at all, so nothing a function does to one is ever visible outside.

The exam version of this usually looks like:

def increment(a, b):
    a = a + 1
    b += 1

x = 42
y = 7
increment(x, y)
print(x, y)
42 7

Neither += nor = a + 1 escapes the function when the value is a number. The full story of why mutable values behave differently is in Why changing one list changed the other.

Returning more than one value#

def min_max(nums):
    return min(nums), max(nums)

lo, hi = min_max([4, 9, 1])
print(lo, hi)
1 9

Python has no multiple-return feature. The comma builds a tuple, and lo, hi = unpacks it. Which means the counts have to match:

def min_max(nums):
    return min(nums), max(nums)

lo, hi, extra = min_max([4, 9, 1])
Traceback (most recent call last):
  File "mm.py", line 4, in <module>
    lo, hi, extra = min_max([4, 9, 1])
    ^^^^^^^^^^^^^
ValueError: not enough values to unpack (expected 3, got 2)
Keep the return shape consistent

If one path through a function returns two values and another returns None, the caller cannot use the result safely — unpacking None raises, and checking for it clutters every call site. Decide what the function hands back and make every path hand back that same shape.

Functions calling functions#

Breaking a big problem into smaller ones is the actual point of functions, and it is called functional decomposition:

def square(x):
    return x * x

def sum_of_squares(a, b):
    return square(a) + square(b)

print(sum_of_squares(3, 4))
25

Once everything is in a function, the usual convention is to put your top-level code in a main() and call it once at the bottom, so the file reads as a list of capabilities followed by one line that starts the program:

def area(w, h):
    return w * h

def main():
    print(area(3, 4))

main()
12

Docstrings, and testing with assert#

A docstring is a triple-quoted string as the first line of the body. It is not a comment — Python keeps it, and tooling reads it:

def square(x):
    """Return the square of a number."""
    return x * x

print(square.__doc__)
Return the square of a number.

assert checks that something is true and says nothing when it is. That silence is the pass:

def is_even(n):
    return n % 2 == 0

assert is_even(4) == True
assert is_even(7) == False
print("all tests passed")
all tests passed

Note that both a true case and a false case are tested. A test suite that only ever checks cases that should pass will happily approve a function that returns True for everything. When an assertion does fail, it fails loudly:

def is_even(n):
    return n % 2 == 0

assert is_even(3) == True
Traceback (most recent call last):
  File "t.py", line 4, in <module>
    assert is_even(3) == True
           ^^^^^^^^^^^^^^^^^^
AssertionError

And this is the payoff of the very first section: assert examines a returned value. A function that prints its answer instead of returning it cannot be tested this way at all.

Where these go wrong#

  • Printing instead of returning. The output looks right and every test fails. The single most common lost mark on functions.
  • Forgetting the call. Defining a function and never calling it produces a program that runs, prints nothing, and reports no error.
  • Using a local variable after the function ends. NameError. Return it instead.
  • Assigning to a global without saying global. UnboundLocalError, pointing at a line where the name looks perfectly defined.
  • Expecting a number argument to change. It will not. Return the new value and assign it at the call site.
  • Not expecting a list argument to change. It will, if the function mutates it.
  • Mismatched unpacking. Catching two returned values in three names, or three in one.
  • Code after a return on the same path. It never runs, and it never warns you.
  • Naming a variable the same as a function. square = square(4) replaces the function with a number, and the next call raises TypeError.

More Python guides

Free study guides for a first Python course — every listing executed before publication.

See the Python course →

Frequently asked questions#

What is the difference between print and return in Python?

print shows a value to a person; return hands a value back to the code that called the function. A function that prints instead of returning gives you None when you assign its result, which is why a unit test on it fails even though the right number appeared on screen.

What is the difference between a parameter and an argument?

A parameter is the name in the function definition, the placeholder. An argument is the actual value passed in at the call. In def area(side) the parameter is side; in area(10) the argument is 10.

Why does my function return None?

Because it has no return statement, or because execution reached the end of the body without hitting one. A function with no return still returns something: None.

Why can't I use a variable outside the function it was created in?

Because it is local to that function. Its scope is the function body and its lifetime ends when the call ends. Using it outside gives NameError: name 'x' is not defined. To get a value out, return it.

Why do I get UnboundLocalError when the variable clearly exists?

Because assigning to a name anywhere in a function makes it local for the whole function, so count = count + 1 tries to read a local that has not been given a value yet. Pass the value in and return the new one, or declare global count if you truly must.

Does a function change the variable I pass into it?

Only if the value is mutable and the function mutates it. Appending to a list parameter changes the caller's list. Rebinding a parameter, including n += 1 on a number, does not.