Search⌘ K
AI Features

Return Related Tips

Explore Pythonic techniques for function returns to improve clarity and consistency. Understand how to return multiple values as tuples, use yield for generators to produce items lazily, and return functions from other functions. Learn to write functions that always return a consistent type and avoid unnecessary else statements following returns, enhancing code readability and maintainability.

A function is a unit of computation. It’s a tool that we design and sharpen for a specific operation. It can also be reused, both by us and other programmers. Better functions improve the modularity, clarity, and maintainability of the program and increase its value overall. Our program is only as effective as its functions.

In many aspects, Python functions are similar to functions in other popular languages. They take parameters, perform computations, and return computed values to the caller, though there are some differences.

We must use functions in any program longer than a dozen lines.

In this chapter, we’ll learn to use the specific Pythonic function design mechanisms:

  • Multiple returns
  • Optional and keyword parameters generators
  • Anonymous functions
  • Functions creating functions

Make functions always return something

C, C++, Java, and even Fortran either allow functions to return exactly one result or none at all (void functions), while Python requires that every function return precisely one value. If our function doesn’t have a return statement at the end or has a return statement without a value, the function returns None. This function, for example, returns None:

Python 3.8
def aFuncThatReturnsNone():
a=1
aFuncThatReturnsNone() # Nothing displayed
print(aFuncThatReturnsNone())

If a function has a return statement with one value, that value is returned.

Return consistently

It is Pythonic to ensure that a function always returns the value of the same type.

The absence of function prototypes in Python makes it possible to design functions that return different data types, even when called with the arguments of the same data type. Consider the re.search() function for regular expression pattern matching. It returns an _sre.SRE_Match object on success and “nothing”:

Python 3.8
print(re.search('0', 'hello')) # Prints nothing
print(re.search('o', 'hello'))

Thankfully, None has the boolean value of False, so we can decide whether we can use the returned object for further processing:

match = re.search(pattern, 'hello') 
if match:
  # Do something with the match object

A worse example is a function that returns a non-string object on success and an error message as a string otherwise:

Python 3.8
def make_list(n):
if n >= 0:
return list(range(n))
else:
return 'The value of n is negative'
print(make_list(5))
print(make_list(-1))

In this case, a valid result is sometimes logically false (when n==0), but the invalid result is always logically true. The only way to tell success from failure is by explicitly checking the type of the returned value.

We can use one Pythonic technique to return consistently. If we assume that a valid result is as common as a failure, we choose a sentinel with the same type as the valid result but whose value is never valid. For example, if the function is expected to produce a positive number, we should use a negative number to report a failure. This approach is used by str.find():

Python 3.8
print('abc'.find('a'))
print('abc'.find('z')) # Not found!

If we can’t choose a proper sentinel or we’re assured that failures are rare, we let the function signal them by raising an exception.

This approach is used by str.index():

Python 3.8
print('abc'.index('a'))
print('abc'.index('z')) # Not found!

Either way is fine, but it can be argued that if we define two methods that belong to the same class, perform very similar tasks, and use different failure reporting mechanisms, it isn’t Pythonic.

Return many values

What if we want our function or method to return more than one value, let’s say several values separated by commas? It’s worth trying:

Python 3.8
def aFuncThatReturnsValues():
return 1,2,3,4 # Does it really return four values?
result = aFuncThatReturnsValues()
print(result)

The printout looks suspiciously like a tuple.

Let’s run the line of code below to check the type of returning values.

Python 3.8
print(type(result))

It is a tuple. A function that attempts to return several values, in truth, returns one object: a tuple built from the values. Building a tuple by way of comma-separated sequences is called packing. When a function returns what looks like several values, it packs them and returns one tuple.

Conclusion: A function always returns one result.

Omit else after return

The return statement terminates the enclosing function. Even if return is not the last statement of the function, the next statement isn’t executed. We can use this property to optimize complex conditional if statements. Suppose we have a condition in a function that, if true, results in a return. There is no need to create the else branch of the conditional statement because the code simply continues to execute when the condition is false:

Python 3.8
def sillyFunction(parameter):
if parameter is None:
return None
# No need for an 'else' and extra indentation!
result = do_something(parameter)
return result

Eliminating else removes the additional indentation that would follow it and improves readability. Let’s make a function that will accept an integer number as a parameter and create a list of integers with a range of that number.

Python 3.8
def createList(number):
return list(range(number))# returning list
def sillyFunction(number):
if number is None:
return None
# No need for an 'else' and extra indentation!
result = createList(number)
return result
print(sillyFunction(None))# passing None
print(sillyFunction(5))# passing 5 as a number

yield, don’t return

Once we return from a Python function, we can never come back again and continue from the next line after the return statement. This behavior is no cause for concern, except when we want to implement a generator.

A generator is an object that, when asked, lazily produces one item at a time. From a caller’s perspective, it looks like a function with internal memory that remembers where it stopped after returning the previous result and resumes from the next line; seemingly impossible.

We can quickly write generators ourselves. All we need is to replace return with yield. A function with a yield returns a generator object. We can use the object as a parameter to another function or elicit the generated values by applying the built-in functions, next() (one item) or list() (all items):

Python 3.8
def fortune_teller(attempts=2):
for _ in range(attempts):
yield bool(random.randint(0, 1))
return 'Do not call me again!'
oracle = fortune_teller(2) # The generator created
print(next(oracle))
# call it again
print(next(oracle))

Note: Once a generator returns (implicitly or explicitly), it becomes empty.

When we attempt to get another value from an empty generator, the generator raises a StopIteration that we can handle, if necessary (these exceptions can be handled in python using try/except).

Python 3.8
next(oracle)

If we provide a fallback argument to next(), an empty generator returns the fallback value and doesn’t raise the exception.

Python 3.8
print(next(oracle,True))

Remember that if we apply list() to a generator, the function retrieves all items and the generator becomes empty.

Let’s try it now.

Python 3.8
oracle = fortune_teller(2)
print(list(oracle))

To see whether the generator is empty, let’s call list(oracle) again.

Python 3.8
print(list(oracle))

We can use a generator object to synthesize a potentially infinite iterable and organize a potentially infinite for loop:

Python 3.8
def bottomless_mug():
count = 1
while True: #No return
yield f"Coffee #{count}"
count += 1
for coffee in bottomless_mug():
print(coffee)
if '3' in coffee: break # Without this line, the loop never stops!

Generator functions remember their internal state between calls and can produce long (possibly infinite) iterables on demand, one item at a time. The latter is important if we process large amounts of data but have limited memory. When we need either of these features, we should use generators!

Return and apply functions

Unlike C and C++, Python functions are first-class objects, which means they can be created and returned by other functions and passed to other functions as parameters. Simply treat their names as variables and apply the call operator, (), when needed.

For example, here is a function called polynomial_factory(coeffs)that takes a list of polynomial coefficients, from the largest to the smallest degree, and returns a function that evaluates the polynomial:

def polynomial_factory(coeffs): 
  def polynomial(x):
    p=0
    for c in coeffs:
      p=p*x+c 
    return p
  return polynomial

Let’s use the factory to build the evaluator for the polynomial, 4x37x2+2x+54x^{3}-7x^{2}+2x+5, and then use the evaluator to calculate the polynomial values in the select points:

Python 3.8
poly = polynomial_factory([4,-7,2,5])
print([poly(x) for x in range(10)])

The poly() function behaves just like any other function, even though it was generated by the factory and not written by us.

To supplement the polynomial factory, we can create another function that takes the polynomial and evaluates it over a specified range. In other words, it applies a function to a range. In contrast with the factory, the new function doesn’t return a function but takes it as a parameter.

Python 3.8
def apply(func, start, end, step=1):
return [func(x) for x in range(start, end, step)]
print(apply(poly, 0, 10))

The result is identical. The beauty of the applicator function is that it’s universal, applying any function to any range.

Quiz on return and yield

Technical Quiz
1.

Which situation below makes a function return a generator object?

A.

The generator keyword is in the function definition

B.

The absence of the return keyword

C.

The yield keyword is anywhere in the function body

D.

The presence of the next() method


1 / 5