- Item 30: Know That Function Arguments Can Be Mutated
- Item 31: Return Dedicated Result Objects Instead of Requiring Function Callers to Unpack More Than Three Variables
- Item 32: Prefer Raising Exceptions to Returning None
- Item 33: Know How Closures Interact with Variable Scope and nonlocal
- Item 34: Reduce Visual Noise with Variable Positional Arguments
- Item 35: Provide Optional Behavior with Keyword Arguments
- Item 36: Use None and Docstrings to Specify Dynamic Default Arguments
- Item 37: Enforce Clarity with Keyword-Only and Positional-Only Arguments
- Item 38: Define Function Decorators with functools.wraps
- Item 39: Prefer functools.partial over lambda Expressions for Glue Functions
Item 31: Return Dedicated Result Objects Instead of Requiring Function Callers to Unpack More Than Three Variables
One effect of the unpacking syntax (see Item 5: “Prefer Multiple-Assignment Unpacking over Indexing”) is that it allows a Python function to seemingly return more than one value. For example, say that I’m trying to determine various statistics for a population of alligators. Given a list of lengths, I need to calculate the minimum and maximum lengths in the population. Here, I do this in a single function that appears to return two values:
def get_stats(numbers): minimum = min(numbers) maximum = max(numbers) return minimum, maximum lengths = [63, 73, 72, 60, 67, 66, 71, 61, 72, 70] minimum, maximum = get_stats(lengths) # Two return values print(f"Min: {minimum}, Max: {maximum}") >>> Min: 60, Max: 73
The way this works is that multiple values are returned together in a two-item tuple. The calling code then unpacks the returned tuple by assigning two variables. Here, I use an even simpler example to show how an unpacking statement and multiple-return function work the same way:
first, second = 1, 2 assert first == 1 assert second == 2 def my_function(): return 1, 2 first, second = my_function() assert first == 1 assert second == 2
Multiple return values can also be received by starred expressions for catch-all unpacking (see Item 16: “Prefer Catch-All Unpacking over Slicing”). For example, say I need another function that calculates how big each alligator is relative to the population average. This function returns a list of ratios, but I can receive the longest and shortest items individually by using a starred expression for the middle portion of the list:
def get_avg_ratio(numbers): average = sum(numbers) / len(numbers) scaled = [x / average for x in numbers] scaled.sort(reverse=True) return scaled longest, *middle, shortest = get_avg_ratio(lengths) print(f"Longest: {longest:>4.0%}") print(f"Shortest: {shortest:>4.0%}") >>> Longest: 108% Shortest: 89%
Now, imagine that the program’s requirements change, and I need to also determine the average length, median length, and total population size of the alligators. I can do this by expanding the get_stats function to also calculate these statistics and return them in the result tuple that is unpacked by the caller:
def get_median(numbers): count = len(numbers) sorted_numbers = sorted(numbers) middle = count // 2 if count % 2 == 0: lower = sorted_numbers[middle - 1] upper = sorted_numbers[middle] median = (lower + upper) / 2 else: median = sorted_numbers[middle] return median def get_stats_more(numbers): minimum = min(numbers) maximum = max(numbers) count = len(numbers) average = sum(numbers) / count median = get_median(numbers) return minimum, maximum, average, median, count minimum, maximum, average, median, count = ➥get_stats_more(lengths) print(f"Min: {minimum}, Max: {maximum}") print(f"Average: {average}, Median: {median}, Count {count}") >>> Min: 60, Max: 73 Average: 67.5, Median: 68.5, Count 10
There are two problems with this code. First, all of the return values are numeric, so it is all too easy to reorder them accidentally (e.g., swapping average and median), which can cause bugs that are hard to spot later. Using a large number of return values is extremely error prone:
# Correct: minimum, maximum, average, median, count = ➥get_stats_more(lengths) # Oops! Median and average swapped: minimum, maximum, median, average, count = ➥get_stats_more(lengths)
Second, the line that calls the function and unpacks the values is long, and it will likely need to be wrapped in one of a variety of ways (due to PEP 8 style; see Item 2: “Follow the PEP 8 Style Guide”), which hurts readability:
minimum, maximum, average, median, count = get_stats_more( lengths) minimum, maximum, average, median, count = get_stats_more(lengths) (minimum, maximum, average, median, count) = get_stats_more(lengths) (minimum, maximum, average, median, count ) = get_stats_more(lengths)
To avoid these problems, you should never use more than three variables when unpacking the multiple return values from a function. These could be individual values from a three-tuple, two variables and one catch-all starred expression, or anything shorter.
If you need to unpack more return values than that, you’re better off defining a lightweight class (see Item 29: “Compose Classes Instead of Deeply Nesting Dictionaries, Lists, and Tuples” and Item 51: “Prefer dataclasses for Defining Lightweight Classes”) and having your function return an instance of that instead. Here, I write another version of the get_stats function that returns a result object instead of a tuple:
from dataclasses import dataclass @dataclass class Stats: minimum: float maximum: float average: float median: float count: int def get_stats_obj(numbers): return Stats( minimum=min(numbers), maximum=max(numbers), count=len(numbers), average=sum(numbers) / count, median=get_median(numbers), ) result = get_stats_obj(lengths) print(result) >>> Stats(minimum=60, maximum=73, average=67.5, median=68.5, ➥count=10)
The code is clearer, less error prone, and will be easier to refactor later.
Things to Remember
You can have functions return multiple values by putting them in a tuple and having the caller take advantage of Python’s unpacking syntax.
Multiple return values from a function can also be unpacked by catch-all starred expressions.
Unpacking into four or more variables is error prone and should be avoided; instead, return an instance of a lightweight class.