Return Values
The return statement is how a function hands a result back to whoever called it. Python is flexible about what — and how many things — a function can return, which is powerful but also means it's worth learning the common patterns and pitfalls.
Returning a Single Value
The simplest case: compute something and send it back with return.
def square(n):
return n * n
result = square(7)
print(result)49
Returning Multiple Values via a Tuple
Python doesn't have true "multiple return values" — instead, when you write return a, b, c, Python packs them into a tuple. The caller can then unpack that tuple into separate variables.
def min_max(numbers):
return min(numbers), max(numbers)
lowest, highest = min_max([4, 8, 15, 16, 23, 42])
print(lowest, highest)
# Or keep the tuple as-is
result = min_max([4, 8, 15])
print(result)
print(type(result))4 42 (4, 15) <class 'tuple'>
Returning Nothing (Implicit `None`)
A function with no return statement — or a bare return — hands back None. This is appropriate for functions whose job is to perform an action (print, mutate, save) rather than compute a value.
def save_to_log(message):
print(f"Saving: {message}")
# nothing to give back — this function performs an action
outcome = save_to_log("User logged in")
print(outcome)Saving: User logged in None
Early Returns for Guard Clauses
Returning early — as soon as you know the answer, or as soon as an invalid case is detected — keeps functions flat and avoids deeply nested if/else chains.
def get_discounted_price(price, is_member):
if price < 0:
return None # guard clause: invalid input
if not is_member:
return price # early return: no discount to apply
return price * 0.9 # main logic, reached only if both checks pass
print(get_discounted_price(100, True))
print(get_discounted_price(100, False))
print(get_discounted_price(-5, True))90.0 100 None
Compare this to the nested alternative, which does the same thing but is harder to follow as more conditions are added:
def get_discounted_price_nested(price, is_member):
if price >= 0:
if is_member:
return price * 0.9
else:
return price
else:
return NoneReturning Different Types Conditionally
Python won't stop you from returning a string in one branch and a number in another, but doing so usually makes a function harder to use correctly, since every caller has to check what type came back.
def find_user(user_id, users):
for user in users:
if user["id"] == user_id:
return user
return "User not found" # inconsistent: dict in one branch, str in another
users = [{"id": 1, "name": "Ada"}]
result = find_user(2, users)
if isinstance(result, str):
print(result)
else:
print(result["name"])User not found
def find_user(user_id, users):
for user in users:
if user["id"] == user_id:
return user
return None # consistent: either a dict, or None
result = find_user(2, users)
if result is None:
print("User not found")
else:
print(result["name"])User not found
Returning a Function
Because functions are first-class objects in Python, a function can return another function. This is the foundation of closures and decorators, which you'll explore in depth in upcoming lessons.
def make_multiplier(factor):
def multiply(n):
return n * factor
return multiply # returning a function, not calling it
double = make_multiplier(2)
triple = make_multiplier(3)
print(double(5)) # 10
print(triple(5)) # 1510 15
Quick Reference
Pattern | Example | Use case |
|---|---|---|
Single value |
| Most common case |
Multiple values |
| Packed into a tuple, unpacked by the caller |
Nothing |
| Action-oriented functions |
Early return |
| Guard clauses, avoid deep nesting |
Returning a function |
| Closures, decorators, factories |
def split_name(full_name):
if not full_name:
return None, None
parts = full_name.strip().split(" ", 1)
if len(parts) == 1:
return parts[0], ""
return parts[0], parts[1]
first, last = split_name("Ada Lovelace")
print(first, "|", last)
print(split_name(""))Ada | Lovelace (None, None)