Return values in Python are how a function hands a result back to its caller, and the single most common beginner bug is forgetting that every function returns something, whether you asked it to or not. That something is usually None, and None is not an empty result — it is a specific value you have to handle.
What does return do?
A return statement ends the function immediately and sends the value on its right-hand side back to whoever called it. The expression is evaluated first, then the frame is torn down and the value is handed over. Any code below the return in that function never runs.
def classify(score):
if score >= 90:
return "A"
elif score >= 80:
return "B"
return "C"
print(classify(95))
print(classify(83))
print(classify(41))
Note the shape of that function: it returns a label rather than printing it. The caller decides whether to print it, store it, put it in a web response, or compare it against something else. A function that printed its result would force that decision on everyone who called it, and would make the function awkward to test.
Two different statements matter here. A bare return exits the function and hands back None. A return value exits and hands back that value. Python has no return; construct, so every exit from a function goes through one of these, whether you wrote it or not.
Why does a function without return give None?
Because a function always has a return value. If control reaches the bottom of the body without hitting a return, Python supplies None automatically. This is why list.append returns None: it modifies the list in place and has nothing to hand back.
def add_item(bucket, item):
bucket.append(item)
items = []
result = add_item(items, "apple")
print("items:", items)
print("result is None:", result is None)
# The classic surprise
numbers = []
try:
numbers.append(1).append(2)
except AttributeError as exc:
print("AttributeError:", exc)
That last line raises AttributeError: 'NoneType' object has no attribute 'append', because the first append returned None and there is no append on None. The stack trace points at the second append, which is why this one confuses people. The fix is to call append twice, or to use extend.
The practical rule: if a function mutates a collection in place, it should not also claim to return something. Pick one job.
How do you check for None correctly?
Use is None, not == None and not a plain truth test. is asks whether two names point at the same object, and None is a singleton, so the comparison is exact. == can be fooled by a custom class that defines equality against None, and a truth test throws away a second distinction that matters: an empty list, an empty string and 0 are all falsy, yet none of them is None.
print(None == False) # False
print(bool(None)) # False
print(bool([])) # False
print([] is None) # False
def find_user(users, name):
for user in users:
if user["name"] == name:
return user
return None
user = find_user([{"name": "Ada"}, {"name": "Grace"}], "Grace")
if user is not None:
print("found:", user["name"])
else:
print("not found")
There is one situation where a truth test is the wrong tool in a different way: a function that legitimately returns 0, "" or an empty list is still returning a real answer. if result: treats those as failure, while if result is None: treats them as success. Pick the check that matches the contract of your function.
| Value | Falsy in a truth test? | Equal to None? |
|---|---|---|
None | yes | yes |
0 or 0.0 | yes | no |
"" | yes | no |
[] or {} | yes | no |
False | yes | no |
One more wrinkle: is None only works with the actual None object, which is why numpy users write value is None and get a different answer from value == None on arrays. For plain Python values the two are interchangeable, and is None remains the safer default.
How do you return multiple values?
There is no multi-return syntax in Python. What looks like one is really a single tuple that the caller then unpacks, which is why the results arrive in a fixed order and why unpacking the wrong number of them raises an error rather than silently misaligning.
def stats(values):
return min(values), max(values), sum(values) / len(values)
low, high, average = stats([3, 1, 4, 1, 5, 9, 2, 6])
print(low, high, round(average, 3))
# The same call, unpacked the wrong way
try:
low, high = stats([3, 1, 4])
except ValueError as exc:
print("ValueError:", exc)
Unpacking is a strict contract. Two names want exactly two values, so a three-value tuple raises ValueError: too many values to unpack (expected 2). That strictness is a feature: it means a change to what a function returns breaks loudly at the call site rather than quietly corrupting your data.
Three return values is usually the point where the design should change. Beyond a couple of loosely related values, a dict with named keys or a small dataclass reads better, because low, high, average tells the reader nothing while result["low"] and result.average do.
def parse_pair(text):
parts = text.split(",")
if len(parts) != 2:
return None, None
return parts[0].strip(), parts[1].strip()
print(parse_pair("left, right"))
print(parse_pair("broken"))
Returning None, None for a failure case is a common pattern, but it is not the only one. A single None is simpler when the whole result is invalid, and raising an exception is better when the caller cannot possibly continue. Choose based on whether the failure is routine or exceptional.
When should a function return early?
Deeply nested conditionals are a sign that a function is doing too much. The alternative is a guard clause: check the precondition, bail out immediately if it does not hold, and let the rest of the body run at one indentation level. The return doubles as an early exit, which is one of the reasons it is more than just a value carrier.
def discount_for(customer):
if customer is None:
return 0.0
if not customer.get("active"):
return 0.0
if customer.get("orders", 0) >= 10:
return 0.2
return 0.05
print(discount_for(None))
print(discount_for({"active": True, "orders": 2}))
print(discount_for({"active": True, "orders": 12}))
Each guard removes one nesting level from everything below it, and the reader sees the rejected cases up front instead of hunting for them. The same shape works for validation loops, for parsing functions, and for any code where the happy path is the interesting part.
Next steps
- Every function returns a value. If you cannot name it, the function is either a side-effecting helper that should return
Noneexplicitly, or it is missing areturn. - Check for
Nonewithis None, and only treat0,""and empty collections as falsy when that is genuinely what you mean. - Multiple return values are one tuple. Unpacking them is strict, which turns a changed contract into a loud failure.
- Prefer a dict or a dataclass once a function needs to return more than two or three related values.
Try it yourself: write def safe_divide(numerator, denominator) that returns the quotient, or None when the denominator is zero, and never raises. Then write def summarise(numbers) that returns the count, the total and the mean as a tuple, and returns None for an empty input. Write a third function def clamp(value, low, high) that returns the value unchanged when it is already in range and returns None when the bounds are inverted. Call each one at least four times, including the edge cases, and add a comment on every call site explaining which check you used to decide the result was valid.