Eliminating indentation by returning early
June 28, 2019Returning 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
The 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.
In programming-speak, returning early is known as guard or guard-code. Thanks to reddit user novel_yet_trivial for pointing this out.
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 else
clause.
That’s a lot of indentation up there. 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 return
s. The important thing is to halt the execution inside these clauses.
Conclusion
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.