RizTech Academy logo
RizTech Academy
FunctionsLesson 3 of 715 min

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.

Check your work

A function with no return returns None. So does a bare return. There is no such thing as a function returning nothing.

Nothing after a return on the same path runs. The print never executes.

Finding a user.

def find_user(users, email):
    for user in users:
        if user["email"] == email:
            return user
    return None

The final return None is what would happen anyway, and writing it means a reader does not have to know that.

Guard clauses.

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"

Flat, one line per case, and the happy path at the bottom with nothing left to check.

get_stats returns a tuple.

print(get_stats([3, 7, 2, 9]))     # (2, 9, 5.25)

The commas packed the values into a tuple; the unpacking on the other side took them out. This is why divmod(17, 5) gives (3, 2).

The dictionary version is better past three values, because callers name what they want and cannot get the order wrong.

The string-on-error version breaks any caller that does arithmetic with the result, and the failure appears far from the cause. Returning None is fine when "no result" is normal; raising is better when the input was wrong, which is module 6.

Practice

  1. Write a function with no return, assign its result, and print it.
  2. Write check(age) with a return followed by a print on the same path. Confirm the print never runs.
  3. Write find_user(users, email) over a list of dictionaries, returning the user or None. Test both outcomes.
  4. Rewrite the deeply nested process(order) above using guard clauses without looking at the version given.
  5. 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.
  6. Rewrite get_stats to return a dictionary with four values. Which version would you rather call?
  7. Write divide(a, b) returning a string on error, then write a caller that breaks because of it. Fix it by returning None instead.
  8. 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. Selected students who finish a course can join our internship, with real-world projects and code review from a working engineer.

About the internship