AlgoMaster Logo

return Statement

High Priority26 min readUpdated June 6, 2026
Listen to this chapter
Unlock Audio

A function that doesn't return anything can only talk to the outside world through side effects: prints, file writes, mutations of objects it was handed. That's fine for some jobs, but the moment you want to use a function's result in a larger expression, store it for later, or test it, you need a real return value. This lesson covers the return statement, what happens when you leave it out, how to return more than one value, when an early return makes a function easier to read, and why returning a value is different from printing one.

The return Statement

return does two things at once. It hands a value back to whoever called the function, and it ends the function immediately. Any code after return in the same execution path never runs.

The function builds up total, returns it, and the caller stores it in result. From that point, result is an ordinary float you can do arithmetic on, format, pass to another function, or anything else. The function itself is done; its local variable total is gone.

The value after return can be any expression. Python evaluates it and hands back the result.

sum(cart) is evaluated first, then its result is returned. The function has one expression in its body, which is a perfectly normal shape for a small function. Note that sum([]) returns 0 (an integer), not 0.0, because that's how the built-in sum is defined.

Once return runs, the function is over. Anything below it in the same path is dead code.

The print after return is never reached. Python won't warn you about it; it just sits there as a footnote in the source code. Most linters will flag it as unreachable code, which is one reason to run a linter on anything beyond a quick script.

A return with no expression returns None. That's a deliberate way to say "I'm done, and there's no useful value to hand back."

The bare return at the top of the function is an early exit. The function's main job is to mutate orders, not to compute a value, so handing back None is honest. The caller can ignore it.

Implicit None: When You Forget to Return

A function that finishes without hitting any return statement quietly returns None. There's no warning, no error, and no syntax difference between "I meant to return nothing" and "I forgot a return". The runtime treats both the same.

The function did all the work of summing the cart, then threw the answer away by not returning it. The caller gets None because that's what every function returns when execution falls off the end without a return.

The most common version of this bug is when you mix up print and return. Code that "looks right" prints the answer but doesn't return it, and the caller stores None.

What's wrong with this code?

The function prints 54.97, which is misleading because it makes the function look like it's "working". But compute_total returns None, so cart_total is None, and the multiplication on the next line blows up. The fix is to swap print for return:

Fix:

Now the caller actually has a number to work with. The function can still print for debugging if you want, but the meaningful output has to leave the function through return.

A subtler version of the same mistake is chaining print around a function call. print(some_function()) calls the function, then prints whatever it returned. If the function already printed inside itself, you'll see the value once from the inner print and then None from the outer one. The fix is the same: decide whether the function's job is to print or to return, and don't do both.

Returning Multiple Values

A function can hand back more than one value by separating them with commas. Under the hood, Python builds a tuple and returns that, but the syntax makes it feel like you're returning multiple things directly.

The return subtotal, discount, tax, total line is shorthand for return (subtotal, discount, tax, total). The parentheses are optional in this position. Either way, the function returns a single tuple, and the caller has one value to handle.

The caller usually unpacks the tuple immediately, which is what makes "multiple return values" feel real. Unpacking matches the names on the left to the positions on the right:

Four names on the left, four values in the returned tuple, matched by position. The function reads as if it returned four named values, even though the underlying mechanism is one tuple.

The cyan box is the call. The orange step is where Python packs the four values into a single tuple (the teal box) before handing it back. On the caller side, the assignment with four names unpacks that tuple into four green variables. The "tuple in the middle" is invisible when you write code, but it's the actual mechanism, and knowing it explains every behavior in this section.

The number of names on the left has to match the number of values in the tuple, or you get a ValueError.

Four values came back, three names tried to catch them. Python won't drop the extras silently. If you genuinely only care about some of the values, the convention is to use _ (a single underscore) as the name for ones you want to throw away:

The underscore is just a regular variable name; it has no special meaning to Python. The convention is "I'm not going to use this", which is enough of a signal for readers and many editors will dim the line accordingly.

You can also accept the whole tuple without unpacking and index into it (result[0], result[-1]), which is useful when you want to pass the bundle along. The unpacking form is clearer when callers use all the values; the indexing form is fine for one or two reads.

A common pattern in real code is a "lookup that might fail", returning both the result and a found/not-found flag:

Two return paths, both returning a (product, found) pair. The caller always unpacks the same way, then branches on found. This is one of those shapes you'll see often once you start looking for it.

Early Return: Guard Clauses

A function with one return at the end is a common shape, but it isn't the only one. Early return (also called a "guard clause") is a return that fires at the top of a function for an edge case, so the rest of the function can focus on the normal path.

The motivating example is a function with several special cases nested in if/else. Without early returns, the indentation grows:

The interesting work is at the bottom of a four-deep pyramid. With early returns, each edge case gets handled at the top and the main logic stays at the outermost level:

Three guard clauses at the top, each handling one edge case. The actual logic at the bottom doesn't need any if. The reader can see at a glance "this function handles three special cases and then does the math".

The pattern reads naturally in plain English: "If the cart is missing, the total is zero. If the cart is empty, the total is zero. If the discount is out of range, the total is zero. Otherwise, here's the math." Each sentence is one guard clause.

Each diamond is one guard. Every "yes" branch leads straight out of the function with a small return value. Every "no" branch falls through to the next check, and eventually to the main computation. The shape is wide and shallow, not deep and nested.

Guard clauses work well when:

  • There are several short, unrelated reasons to bail out.
  • Each edge case has an obvious "default" answer (0, None, False, an empty list).
  • The main logic only makes sense once the edge cases are out of the way.

They work less well when:

  • All paths share post-processing (logging, building a response object) that you don't want to duplicate at every return site.
  • The function only has one or two simple branches; an if/else reads just as well.

Empty-cart is the textbook E-Commerce example. Many small functions in checkout code start with if not cart: return 0. The rest of the function gets to assume there's something to work on.

The empty-cart guard is the first line. Below it, the function gets to assume cart has at least one item. No if cart: ... wrapping the rest of the body, no extra indentation. The reader sees "if empty, bail; otherwise, summarize".

Multiple Returns vs Single Return

A long-running style debate in programming is whether functions should have exactly one return (single-exit) or many (multiple-exit, including guard clauses). Both work, both produce correct programs, and both have fans. Knowing the trade-offs lets you pick the right one for the function in front of you.

Multiple returns scatter the exits throughout the function, usually as guard clauses and early matches.

Four explicit return points. Each one reads as a clear answer to a specific question. No accumulating state, no flag variable. You can scan the function top-to-bottom and trace each path.

Single return keeps one exit at the bottom, often with a result variable that gets updated along the way:

One return at the end. Every path goes through it. If you ever needed to add logging or modify the result before returning, you'd have exactly one place to do that.

StyleStrengthWeakness
Multiple returnsLess nesting, edge cases handled and forgotten, easy to read top-downHarder to inject "always-do-this-before-returning" logic; more total return statements to maintain
Single returnOne exit point makes pre-return work straightforward; one canonical answerMore indentation, more elif chains, more state to track mentally

Python culture leans toward multiple returns with guard clauses for clarity, especially in small functions. The single-return style shows up more often in long functions that build a result over several steps. The honest answer is "use whichever makes this particular function easier to read", and don't enforce one style globally.

One concrete case where single return wins: when there's cleanup or formatting work that should happen on every path. If a function has to print a log line, write to a file, or run any other "always do this before exiting" step, a single return at the bottom lets you place that step once. Returning early would either skip the step or force you to duplicate it at every exit.

Returning Rich Types

The return value doesn't have to be a number or a string. Functions can hand back lists, dictionaries, custom objects, sets, tuples, or anything else that's a Python value.

Returning a list is common when the function's job is "give me everything that matches".

The function builds a fresh list, fills it in, and returns it. The caller owns that list; whatever the caller does with it doesn't affect anything inside the function. This is a clean shape because the result and the input don't share any mutable state.

Returning a dictionary is the right call when the result has multiple named fields and you want them keyed by name, not by tuple position.

A dictionary scales better than a tuple when the function's result has several named pieces. Compare summary["total"] to result[2]: the dictionary version tells you what you're reading. If a future version of the function adds another field, the existing callers keep working as long as they only read the keys they already cared about.

A tuple return shines when the number of values is small and fixed (two or three), the order is obvious (like a (value, found) pair), and you want unpacking on the caller side. A dictionary shines when the count might grow, the fields are named, or you want to pass the result around without unpacking it.

Returning a custom object is the next step up. You've already used objects: a datetime is one, a file handle is one, a list is one. A function can return any of them. When you write your own class, you'll return instances of it the same way you'd return a list or a dict.

A common rule of thumb: pick the simplest return type that lets the caller do its job.

  • One value: return that value (number, string, list, object).
  • A small fixed group: tuple plus unpacking.
  • A larger or growing group with named pieces: dictionary.
  • A complex value with behavior (methods, state): a custom class instance.

Return vs Print

The single most useful split to understand about return values is this: printing is for humans, returning is for code.

A function that only prints can talk to a person reading the screen. A function that returns can talk to other functions. The two roles don't conflict, but mixing them up leads to functions that "work" when you run them by hand and break the moment you try to use them inside something larger.

Start with two versions of the same idea:

The first version is fine if all you want is to see the number on the screen. The second version is fine for the same thing (print(compute_total(cart))) and lets you do more with the result.

The print_total version couldn't do any of those follow-ups. Once it printed, the number was gone. The compute_total version hands the number back, and the caller decides what to do with it. That's composability: each function does one job and produces a value the next function can build on.

The same split matters for testing. A function that returns is straightforward to test with a plain equality check (assert compute_total([10, 20, 30]) == 60). A function that only prints is harder to test, because the value lives inside stdout instead of in a variable you can compare against.

A useful guideline: a function should generally return or print, not both for its primary output. Debug print calls during development are fine, but the actual answer should leave the function through return. The caller can wrap the call in print if they want it on the screen.

There are exceptions. A function whose job is the display itself (a renderer, a logger, a "format this thing nicely") is doing its real work through printing. Even then, splitting "compute the formatted string" from "print the formatted string" is often cleaner:

format_total returns a string; print_total is a thin wrapper that prints it. Now you can use the formatted version for printing, logging, sending in an email, or stuffing into a larger string, without rewriting the formatting logic.

Returning None Deliberately

None isn't always a mistake. There are real reasons to return it on purpose: as a sentinel for "no result", as the marker that a function did its work through side effects, or as the explicit "not found" value.

The lookup-with-no-match shape is the most common:

The function either returns the matching product or None. The caller checks with is None, not == None, because None is a singleton and identity is the right comparison. The (product, found) tuple style we saw earlier is one alternative; returning None is the simpler version when there's no separate "found" flag to track.

A function that exists for its side effects often returns None on purpose, just to be explicit:

You could also leave off the return None and rely on implicit None. Both are valid. Some teams prefer the explicit form because it documents "I'm choosing to return nothing", and some prefer the implicit form because it's shorter. Pick a style and stick to it; the code's behavior is identical either way.

The danger with None is when callers forget to check for it.

result is None, and you can't index into None. The fix is to check first:

When you return None for "no result", document it. The caller has to know that None is a possible answer, otherwise they'll skip the check and run into the error above the first time the data doesn't have what they expected.

If None is a value the caller might also use as real data (a config value where None legitimately means "unset", for example), the (value, found) tuple shape is clearer than overloading None. A function that returns (value, True) for a key whose stored value is None, and (None, False) for a missing key, lets the caller tell the two cases apart. A plain None return collapses them into one indistinguishable answer.

Quiz

return Statement Quiz

10 quizzes