Eliminating indentation by returning early

June 28, 2019

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

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 returns. 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.