Eliminating indentation by returning early
Returning early is a fairly basic but useful technique and it’s one that I’ve only adopted relatively late in my Python journey. The Zen of Python states that “flat is better than nested” and returning early can definitely make a noticeable difference in this regard.
Consider the following function:
def make_odd_even(number: int) -> int: if number % 2 != 0: return number + 1 else: return number
Given an integer,
make_odd_even() converts an odd number to an even one. It first verifies that it’s odd, and then adds 1 to it, making it even. If the number is already even, it’s returned as is.
Here’s another, shorter way to write it:
def make_odd_even_v2(number: int) -> int: if number % 2 != 0: # check if odd return number + 1 return number
else clause is omitted and becomes implicit because we know that if the number isn’t odd then it’s surely even. There is no third option. Considering that
return always stops any further code from being executed, we also know that if the number is odd, the second return statement is never reached. Same result, shorter code.
Another way to write the function is to flip the
if clause check:
def make_odd_even_v3(number: int) -> int: if number % 2 == 0: # check if even return number return number + 1
It’s hard to see when the code is so trivial, but though they achieve the same result, only
make_odd_even_v3() is an example of a returning early function.
What is returning early?
Returning early is the practice of first checking for one or more “invalid”/terminating conditions, usually at the beginning of the code, and halting the execution if any of these conditions is satisfied.
That’s a mouthful.
Here’s a more involved example: Suppose we want to write a function to download media (image or video) from a tweet and then upload it from our local machine to an FTP server. This function should receive one parameter,
tweet_url, and if all goes well, it should return a URL to the downloaded media file.
Here’s one possible implementation:
The function below calls other helper functions and raises custom exceptions. Their implementations are beside the point of this article and are therefore omitted.
def upload_tweet_media(tweet_url: str) -> str: if check_valid_tweet(tweet_url): local_path = download_media_from_tweet(tweet_url) if local_path: try: url_to_file = upload_to_ftp(local_path) return url_to_file except ftputil.error.FTPOSError: raise FTPError("Couldn't upload to FTP server") else: raise DownloadError("Couldn't download twitter media") else: raise TwitterURLError("URL is invalid")
We first check whether
tweet_url is a valid URL and that it actually points to a tweet. If it does, we then attempt to download the media from this tweet using the helper function
download_media_from_tweet() - this naughty function returns either the
local_path to the downloaded file, or
None if the download failed for any reason. If the download is successful, we then pass the file’s local path to
upload_to_ftp(). Assuming all goes well, the function returns the URL to the uploaded file. For every condition check, we’re also including an
That’s lots of identation here. At the innermost part, we’re three levels deep.
Advantages of returning early
How would the function above look with early-return clauses: what if it checks for the “negative”, falsey, or invalid scenarios first?
def upload_tweet_media(tweet_url: str) -> str: if not check_valid_tweet(tweet_url): raise TwitterURLError("URL is invalid") local_path = download_media_from_tweet(tweet_url) if not local_path: raise DownloadError("Couldn't download twitter media") try: url_to_file = upload_to_ftp(local_path) return url_to_file except ftputil.error.FTPOSError: raise FTPError("Couldn't upload to FTP server")
Here, we — only seemingly — flipped the order by which we check for invalid conditions. In reality, the outer
if clauses are evaluated first anyway - we just changed the way the code is laid out.
The function is now flatter, shorter even with spacing, and cleaner to the eye. As for readability, I think this change only makes our intention clearer: unless the URL is invalid, and then unless the file couldn’t be downloaded, try to upload it to the FTP. An added benefit is that our “happy-path” return value (
url_to_file) is no longer indented three levels deep, and is clearly visible towards the end of the function.
Order (sometimes) matters
In the example above, the order by which we perform the checks matters. It’s obvious: we shouldn’t attempt to download a file if the URL isn’t valid, so we should first check if the URL is invalid, and only then attempt the download.
However, it isn’t always immediately evident that conditions are coupled. When refactoring code to return early, keep in mind to verify that dependent checks are performed in the right order. You’re no longer guided by the mental hints of indentation.
It’s only returning early if you actually return
Also remember that there has to be some kind of terminating statement in your return early clauses. In the example above, these are
raise statements, but they could have been
returns. The important thing is to halt the execution inside these clauses.
It’s not always possible and it doesn’t always make sense to use early-returns. Where it does, they can eliminate multiple levels of indentation, make the code more readable, shorter, and the intention behind it clearer.