Functions and Return Values
Wrap a piece of work in a name, hand it inputs, get an answer back. The difference between printing and returning is the thing worth getting right.
Overview
return, and the None you did not ask for
return sends a value back and ends the function immediately.
def classify(score):
if score >= 90:
return "distinction" # exits here
if score >= 50:
return "pass"
return "fail"A function with no return returns None. So does a function whose return line is never reached — and that is the source of a common confusing bug:
def find(items, target):
for item in items:
if item == target:
return item
# no return here: returns None when nothing matchesprint is not return. Printing shows a value on screen; returning hands it back to the caller so it can be stored, tested or passed on. A function that only prints cannot be used in a calculation.
Returning several values is really returning one tuple, which the caller can unpack:
def stats(numbers):
return min(numbers), max(numbers), sum(numbers) / len(numbers)
low, high, mean = stats([1, 2, 3])
Define once, call many times
def names the function, the parentheses list what it needs, and return hands a value back.
Returning versus printing
One of these gives you a value you can use. The other just puts characters on the screen and hands back None.
print is not return
return ends the function immediately and sends a value back to whoever called it.return still returns something: None. That is why b in the second editor is None.greeting="Hello" - which makes it optional at the call site.Functions and Return Values: A Practical Guide
A function packages a piece of work behind a name. return sends a value back to the caller - and a function without one still returns something, which is the source of a great many confusing bugs.
Packaging a piece of work behind a name
A function bundles some steps, gives them a name, and lets you run them again with different inputs.
def area(width, height):
"""Return the area of a rectangle."""
return width * height
area(3, 4) # 12
area(height=4, width=3) # 12 - keyword arguments, order no longer mattersThe line beginning def is the definition; nothing runs until the function is called. The string on the first line is the docstring, which help(area) and every editor will show.
Three reasons functions matter, in the order you feel them:
- No repetition. Fix a bug once rather than in six places.
- A name is documentation.
calculate_vat(total)explains itself; the same three lines inline do not. - Testability. A function with inputs and a return value can be tested; a block of code in the middle of a script cannot.
Arguments: positional, keyword and defaults
def greet(name, greeting="Hello", punctuation="!"):
return f"{greeting}, {name}{punctuation}"
greet("Ada") # 'Hello, Ada!'
greet("Ada", "Hi") # 'Hi, Ada!'
greet("Ada", punctuation="?") # 'Hello, Ada?'Parameters with defaults must come after those without. Keyword arguments make a call readable at the point where somebody reads it, which matters more than the keystrokes it costs — send(retry=False) beats send(False).
For variable numbers of arguments:
def total(*numbers): # a tuple of positional arguments
return sum(numbers)
def configure(**options): # a dict of keyword arguments
print(options)
total(1, 2, 3) # 6
configure(debug=True, port=80) # {'debug': True, 'port': 80}
The mutable default argument
This is the most famous trap in the language, and it catches everyone once:
def add_item(item, basket=[]): # WRONG
basket.append(item)
return basket
add_item("apple") # ['apple']
add_item("pear") # ['apple', 'pear'] -- the same list, still thereThe default is created once, when the function is defined, not each time it is called. Every call that does not pass a basket shares that one list.
The fix is always the same:
def add_item(item, basket=None):
if basket is None:
basket = []
basket.append(item)
return basketAny mutable default — list, dict, set — needs this treatment.
Exploration guide
- Forget the return. Write a function that computes a value without returning it, then print the call. You get None, and no error to explain why.
- Return early. Put a return before other statements and confirm the later ones never run.
- Compare print and return. Write one function that prints and one that returns, then try to use each result in an arithmetic expression. Only the returning one works.
- Trigger the mutable default. Define the buggy version above and call it three times. The list grows across calls, because there is only ever one of it.
Traps worth knowing
- Printing instead of returning. The value is displayed and then lost.
- Falling off the end. A branch with no return silently yields None.
- Mutable default arguments. Shared state between calls; use None.
- Reaching for global. Almost always better solved by returning a value and assigning it at the call site.
- Calling without parentheses. f is the function object; f() calls it.
The short version
A function takes parameters, runs a body, and returns a value; without an explicit return it returns None, which is why a missing return shows up as a confusing error far from its cause. Return exits immediately, local names vanish when the call ends, and default arguments are evaluated once at definition time — so a mutable default is shared by every call that relies on it.
Scope: what a function can see
Names created inside a function are local to it and disappear when it returns. Names from the enclosing module are readable but not assignable without saying so.
count = 0
def increment():
count = count + 1 # UnboundLocalError: assigning makes it local
def increment_properly():
global count # rarely the right answer, but it works
count += 1Python resolves names in the order local, enclosing, global, builtin. The error above happens because the assignment marks count as local for the whole function, so the read on the right-hand side finds nothing yet.
Reaching for global is usually a sign the design wants rethinking. Passing the value in and returning the new one keeps the function testable and its behaviour obvious:
def increment(count):
return count + 1The same discipline applies to arguments. A function that modifies a list passed into it changes the caller's list, because both names point at the same object. That is sometimes intended and should always be deliberate — and named accordingly, such as sort_in_place(items).
Writing functions worth reusing
- One job each. If the name needs an "and", it is two functions.
- Take arguments, return values. A function that reads a global and prints its result can only be used one way.
- Short. Beyond roughly 30 lines, something inside usually wants a name of its own.
- Name it after what it does, as a verb phrase:
calculate_tax,load_config,is_valid. - Docstring first. One line saying what it returns is enough for most functions.
- Fail loudly. Raising
ValueErroron bad input is kinder than returningNoneand letting the caller discover it later.
A worked example: one job, one return shape
A small function doing the thing this page recommends — taking arguments, returning values, handling the empty case explicitly:
def summarise(scores):
"""Return (count, mean, best) for a list of scores."""
if not scores:
return 0, 0.0, None
return len(scores), sum(scores) / len(scores), max(scores)
count, mean, best = summarise([88, 92, 79])
print(count, round(mean, 1), best)
print(summarise([]))3 86.3 92
(0, 0.0, None)Four decisions are visible. The empty case is handled first as a guard clause, so the main path is not indented and does not have to worry about dividing by zero. Both return statements produce a tuple of the same three things, so the caller can unpack the result without checking which branch ran. The docstring says what comes back, which is the one thing a caller cannot see from the signature. And nothing is printed — the caller decides what to do with the values.
That last point is the one worth dwelling on. A version that printed the summary instead of returning it could not be tested, could not be totalled across several groups, and could not be written to a file. Returning keeps every one of those open.
The empty case returning None for the best score is deliberate: there is no best score, and 0 would be a lie that arithmetic downstream would happily believe.
Returning early
A return in the middle of a function ends it immediately, and using that deliberately is what keeps functions flat.
The pattern is to deal with the cases you cannot handle first, each with its own return, leaving the main work at the bottom at zero indentation. Every refusal sits next to the condition that caused it, adding a rule is one line rather than another level of nesting, and the reader can see the whole set of preconditions without scrolling.
The objection is the old single-exit rule, which came from languages where every exit path had to free memory by hand and missing one leaked. Python has neither manual cleanup nor that hazard — with handles resources regardless of how the block is left — so the rule brings no benefit and costs the readability that guard clauses provide. Early returns are standard Python style.
Two cautions. Every path should return the same *kind* of thing; a function that returns a number on one path and a tuple on another forces every caller to find out which. And if the function assigns to a variable that the code after the branches uses, make sure every path assigns it, or return from each branch so there is no "after".
Docstrings, and what belongs in one
The string on the first line of a function is its docstring, available as help(func) and func.__doc__, and shown by every editor at the call site.
One line is enough for most functions, and it should say what the function *returns* or *does*, not restate the name. """Return the area of a rectangle.""" is useful; """This function calculates area.""" is the name again with more words.
When more is warranted, the conventional order is: a one-line summary, a blank line, then the details — what the parameters mean where it is not obvious, what is returned, and what exceptions it raises deliberately. Anything a caller has to know that the signature does not already say belongs here, and nothing else does.
Two things not to put in one. Types, if you are using type hints — the hints say it in a form tools can check, and repeating it in prose means two places to update. And implementation detail: the docstring is for the caller, and describing how the function works internally makes it wrong the first time the implementation changes.
The habit worth adopting is writing the docstring before the body. If it is hard to state what the function returns in one sentence, the function is probably doing more than one thing, and that is easier to fix before it is written than after.
Functions as values
A def statement creates an object and binds it to a name, exactly as an assignment does. That single fact is behind several features that otherwise look unrelated.
Because a function is an object, it can be stored in a list or a dictionary, passed as an argument, returned from another function, and given attributes. sorted(items, key=len) passes len itself, not a call to it — note the absence of brackets, which is the difference between the function and its result.
That difference is worth stating plainly, because it is a common early mistake. f is the function; f() calls it. Writing callback = handler() in a place expecting a function stores the *result* of calling it, usually None, and the failure arrives later when something tries to call None.
Three things follow directly. A dictionary of functions replaces a chain of elif when dispatching on a value. A function that takes a function is how map, filter, sorted and every decorator work. And a function that returns a function is how you build one with some arguments already fixed, which functools.partial also does.
The practical habit is to notice when you are writing the same function shape several times with one value changed. That value can usually become a parameter, and if it cannot, the function itself can become the thing being passed around.
Questions people ask
What is the difference between an argument and a parameter? The parameter is the name in the definition; the argument is the value passed in the call.
Can a function return more than one value? It returns one tuple, which unpacks into several names — which feels identical in practice.
What is a lambda? A small anonymous function, lambda x: x * 2, useful as a key= argument. Anything longer deserves a def and a name.
Does Python pass by value or by reference? Neither exactly: it passes the reference to the object by value. Mutating an argument affects the caller; rebinding the name does not.
Can functions be stored in variables? Yes — they are objects. You can put them in lists and dictionaries, pass them as arguments and return them, which is what makes decorators and callbacks possible.
What are type hints for? Documenting the expected types for readers and tools. Python does not enforce them at runtime.
Can I define a function inside another? Yes, and the inner one sees the outer one's variables. That is a closure, and it is how decorators and factory functions are built.
How many arguments is too many? When you are counting them at the call site. Group related ones into a small object, or split the function.
Should a function ever return different types? Prefer not. A caller then has to discover which it got, and the check spreads to every call site.
Recap in one screen
defnames a block of work; nothing runs until it is called.returnsends a value back and exits; without it, the function returnsNone.- Printing is not returning — only a returned value can be used by the caller.
- Defaults are evaluated once at definition time, so never use a mutable default; use
None. - Assigning to a name inside a function makes it local for the whole function.