18.4 Return Values: return, None, and Multiple Returns
A function’s ability to accept input is only half of its power; its true utility lies in its capacity to produce output. This output, known as the return value, is the result of the function’s computation and is sent back to the part of the program that called it. The return statement is the mechanism for this, immediately terminating the function’s execution and optionally passing a value back to the caller. When a function lacks an explicit return statement or the return statement has no value following it, the function implicitly returns the special value None. This value is a built-in constant that represents the absence of a value. It is crucial to understand that None is not the same as zero, an empty string, or False; it is a unique type (NoneType) signifying “nothing here.”
The return Statement and Implicit None
The return statement performs two critical actions: it specifies the value to be sent back to the caller, and it exits the function immediately. Any code after a return statement within the same block is unreachable and will not be executed. This makes return useful for handling edge cases and early exits. If a function completes without encountering a return statement, Python automatically returns None. This implicit behavior is a common source of bugs for beginners who expect a function that modifies a data structure in-place, like list.append(), to return the modified object. Since such methods return None, assigning their result to a variable leads to unexpected outcomes.
def get_length(some_list):
"""Returns the length of the list."""
return len(some_list)
def print_length(some_list):
"""Prints the length of the list but returns None."""
print(len(some_list))
# No return statement, so None is returned
my_list = [1, 2, 3]
result1 = get_length(my_list) # result1 is assigned the integer 3
result2 = print_length(my_list) # Prints '3', but result2 is assigned None
print(result1, result2) # Output: 3 None
# Common Pitfall: Expecting an in-place method to return the object
new_list = my_list.append(4) # my_list is now [1, 2, 3, 4]
print(new_list) # But new_list is None, not the updated list
Returning Multiple Values (Using Tuples)
A function can only return a single object. However, this object can be a compound data type, such as a tuple, list, or dictionary, allowing you to effectively package multiple distinct values together. The most common and Pythonic idiom for this is to return a tuple. The parentheses around a tuple are often optional in return statements, making the syntax clean and readable. When calling such a function, you can unpack the returned tuple directly into multiple variables, which is a concise and efficient way to handle multiple outputs.
def analyze_number(number):
"""Analyzes a number and returns its square and cube."""
square = number ** 2
cube = number ** 3
# Returning a tuple (parentheses are optional here)
return square, cube
def get_user_info():
"""Returns user information as a tuple."""
name = "Alice"
age = 30
email = "alice@example.com"
return (name, age, email) # Parentheses make it explicit
# Unpacking the returned tuple into separate variables
squared, cubed = analyze_number(5)
print(f"Square: {squared}, Cube: {cubed}") # Output: Square: 25, Cube: 125
# You can also capture the entire tuple as a single variable
results = analyze_number(3)
print(results) # Output: (9, 27)
print(results[0]) # Output: 9
# Unpacking a more complex return
user_name, user_age, user_email = get_user_info()
Early Returns and Conditional Logic
The return statement is not limited to the end of a function. It can be used at any point to exit the function early. This is particularly valuable for handling error conditions, validating input, or implementing conditional logic where further computation is unnecessary. Early returns can significantly simplify code flow by reducing nested if-else blocks and making the function’s exit points clear. This pattern is often referred to as a “guard clause.”
def safe_divide(dividend, divisor):
"""
Safely divides two numbers.
Returns None if division by zero is attempted.
"""
# Early return to handle the error case
if divisor == 0:
print("Error: Cannot divide by zero.")
return None # Exit the function early
# If the function didn't return above, proceed with the calculation
return dividend / divisor
result = safe_divide(10, 2) # Returns 5.0
print(result)
result = safe_divide(10, 0) # Prints error message and returns None
print(result)
def get_grade(score):
"""Returns a letter grade based on the score."""
# Series of conditional early returns
if score >= 90:
return 'A'
if score >= 80:
return 'B'
if score >= 70:
return 'C'
if score >= 60:
return 'D'
return 'F' # Default return if no other condition is met
grade = get_grade(85)
print(grade) # Output: B
Best Practices and Common Pitfalls
- Be Explicit: While Python implicitly returns
None, it is often better to be explicit for clarity, especially in functions whereNoneis a possible valid return value signaling a specific outcome (e.g., “not found”). - Understand
None: Always be aware of which functions returnNone(typically those that perform an action rather than a calculation). A common mistake is to assume that a function that modifies a mutable argument, likelist.sort(), returns the sorted list; it does not, it returnsNone. - Type Hints: Use type hints to clearly document what a function returns. For multiple returns, use the
tupletype hint specifying the types of each element (e.g.,-> tuple[str, int]). For functions that can returnNone, use theOptionaltype (e.g.,-> Optional[float]). - Single Responsibility: When returning multiple values, ensure the function remains focused on a single responsibility. If you are returning many unrelated values, it might be a sign that the function should be refactored into smaller, more focused functions.