Return values, and functions that return nothing
return looks like the simplest part of a function. It has three behaviours
worth knowing properly, and one of them explains a bug you met back in module 2.
Every function returns something
Even when you do not say so:
def greet(name):
print(f"Hello, {name}!")
result = greet("Priya")
print(result)
Hello, Priya!
None
A function with no return returns None. So does a bare return with nothing
after it. There is no such thing as a function that returns nothing at all —
only one that returns None.
This is why numbers.sort() gave you None in module 2: it sorts in place and
has nothing to hand back. The rule from the list-methods lesson — if a method
changes the thing, it gives you nothing back — is this behaviour, followed
consistently.
When you see AttributeError: 'NoneType' object has no attribute ..., the cause
is almost always that you used the result of a function that returned None.
return ends the function
Immediately, wherever it is:
def check(age):
if age < 0:
return "Invalid"
print("This only runs for non-negative ages")
return "Valid"
Nothing after a return on the same path ever runs. Useful deliberately:
def find_user(users, email):
for user in users:
if user["email"] == email:
return user
return None
The return inside the loop exits the whole function the moment it finds a
match — no break, no flag, no second variable. The final return None handles
"not found", and is worth writing explicitly even though it is what would happen
anyway. A reader should not have to know the rule to understand the function.
Early returns keep code flat
Compare:
def process(order):
if order is not None:
if order["items"]:
if order["paid"]:
return "ready to ship"
else:
return "awaiting payment"
else:
return "empty order"
else:
return "no order"
with:
def process(order):
if order is None:
return "no order"
if not order["items"]:
return "empty order"
if not order["paid"]:
return "awaiting payment"
return "ready to ship"
Identical behaviour. The second is flat, each case is one line, and the happy path sits at the bottom with nothing left to check. Adding a fifth condition is one more line rather than one more level of indentation.
This is the guard clause pattern, and it is one of the highest-value habits in this course.
Returning several values
Separate them with commas:
def get_stats(numbers):
return min(numbers), max(numbers), sum(numbers) / len(numbers)
lowest, highest, average = get_stats([3, 7, 2, 9])
print(lowest, highest, average)
2 9 5.25
What actually happens: the values are packed into a tuple and unpacked on the other side. You can see it:
print(get_stats([3, 7, 2, 9]))
(2, 9, 5.25)
This is the tuple lesson made useful, and it is why divmod(17, 5) gave you
(3, 2).
Ignore parts you do not need with _:
_, highest, _ = get_stats(numbers)
Past three values, return a dictionary instead. Unpacking four or five things in the right order is exactly the positional-argument problem again:
def get_stats(numbers):
return {
"min": min(numbers),
"max": max(numbers),
"average": sum(numbers) / len(numbers),
"count": len(numbers),
}
stats = get_stats([3, 7, 2, 9])
print(stats["average"])
Callers name what they want and cannot get the order wrong.
Returning different types
Legal, and usually a mistake:
def divide(a, b):
if b == 0:
return "Cannot divide by zero"
return a / b
Sometimes a number, sometimes a string. Every caller must now check which it got, and one that forgets will try arithmetic on an error message.
Better options, in increasing order of how much you will like them later:
def divide(a, b):
if b == 0:
return None # caller checks for None
return a / b
def divide(a, b):
if b == 0:
raise ValueError("Cannot divide by zero")
return a / b
Raising is usually right, and it is module 6. For now: keep a function's
return type consistent. Returning None for "no result" is fine, because
is None is an easy and obvious check. Returning an error message pretending to
be data is not.
Returning nothing on purpose
Plenty of good functions return None, because their job is an effect rather
than an answer:
def save_to_file(data, filename):
...
That is fine. What is not fine is a function that does a calculation and only prints it — you have thrown the answer away and nobody else can use it.
A useful split: functions that work things out should return; functions that do things may not. Keeping those separate makes the calculating ones testable, which module 10 depends on.
Practice
- Write a function with no
return, assign its result, and print it. - Write
check(age)with areturnfollowed by aprinton the same path. Confirm the print never runs. - Write
find_user(users, email)over a list of dictionaries, returning the user orNone. Test both outcomes. - Rewrite the deeply nested
process(order)above using guard clauses without looking at the version given. - Write
get_stats(numbers)returning min, max and average as a tuple. Unpack it. Then print the raw return value and confirm it is a tuple. - Rewrite
get_statsto return a dictionary with four values. Which version would you rather call? - Write
divide(a, b)returning a string on error, then write a caller that breaks because of it. Fix it by returningNoneinstead. - Find a place in your guessing game from module 3 where a calculation is only printed. Change it to return, and print at the call site.
Next: scope — why a variable you changed inside a function is unchanged outside it.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship