Untangling Heavily Nested Python Code

Written by andrew.smykovv | Published 2023/03/28
Tech Story Tags: python | code-refactoring | programming | clean-code | code-smells | python-tips | python-development | python-tutorials

TLDRNested code is considered to be hard to read and analyze. Such code requires more mental effort to understand what it does. The more nested code is, the higher its [cyclomatic complexity] It’s often thought of as an **anti-pattern** that should be avoided. One of the **refactoring techniques** is using guard clauses.via the TL;DR App

Heavily nested code is considered to be hard to read and analyze. It stems from the fact that such code requires more mental effort to understand what it does. The more nested code is, the higher its cyclomatic complexity, which makes it harder to comprehend and test. That’s why deeply nested code is often thought of as an anti-pattern that should be avoided.

One of the refactoring techniques to reduce nested structuring is using guard clauses, also called guard statements. They are chunks of code at the top of the function serving the following purposes:

  • checking passed-in parameters for suitability
  • checking the object’s state to decide whether to continue
  • catching unexpected situations before executing the main body

In this way, we can reduce the indentation level and make the code’s structure much simpler.

Extra nesting level due to the passed-in parameters check

We’ll look at the application of guard clauses in terms of checking passed-in parameters. Let’s look at two examples from the practice where functions’ bodies were placed inside a single if statement. I’ve changed the examples a bit but preserved the main idea.

def get_characters(file_path: Path) -> List[str]:
    result = []

    if file_path.exists():
        with open(file_path, 'r') as file:
            for line in file:
                if not is_character_line(line):
                    result.append(line)

    return result

At the top of the function, we check whether the file exists. If it does, we go into the if statement where the function’s main body gets executed. We read the file line by line adding appropriate lines to the result list. At the end of the function, we return the result. We can simplify the function and get rid of the excessive nesting by introducing a guard clause at the top of it.

Let’s look at another example where the first line introduces a nesting block for the whole function.

@dataclass
class AtmMachine:
    code: str
    name: str
    city: str

def print_atm_statistics(atms: List[AtmMachine]) -> None:
    if len(atms) > 0:
        basic_atm = atms[0]
        print_atms_in_city(basic_atm.city)        
        process_atms_codes(atms)
        process_atms_names(atms)
        print_summary(atms)

The code even seems weird-looking because the entire function is placed inside an if statement. The dataclass defines an ATM which has three parameters describing it. The function print_atm_statistics receives a list of atms as a parameter to process them somehow. In the case of a valid parameter, the whole processing is executed inside a single conditional.

Placing the entire function inside a single conditional is a frequent pattern encountered in my work. Functions start with if conditional representing parameters checks of some sort.

Introducing the Guard Clause

We could improve the examples by adding the guard clause that would check for unsuitable parameters at the top of the function, reducing one redundant nesting level.

The first example transforms into:

def get_characters(file_path: Path) -> List[str]:
    if not file_path.exists():
        return []

    result = []
    with open(file_path, 'r') as file:
        for line in file:
            if not is_character_line(line):
                result.append(line)
    return result

The introduction of the guard clause has allowed us to remove one indentation level and thus improve the readability. It’s reasonable to immediately return if any of the parameters doesn’t suit for any reason. In this case, if the file doesn’t exist, we instantly return meaning we have nothing to process.

As for the second example with ATMs:

def print_atm_statistics(atms: List[AtmMachine]) -> None:
    if not len(atms):
        return

    basic_atm = atms[0]
    print_atms_in_city(basic_atm.city)
    process_atms_codes(atms)
    process_atms_names(atms)
    print_summary(atms)

The guard has also helped to remove an extra level of nesting inside the function. We’ve inverted the conditional for the parameters check, moving it to the top of the function. When the list is empty, we return at once because there is nothing to process.

Generally, you can use the following pattern to remove an extra indentation level.

# before
def func():
    if guard:
        # do something
        # do something
        # do something

# after
def func():
    if not guard:
        return
    # do something
    # do something
    # do something

Nested code often naturally occurs during development. Most of the time, the first code’s draft is not the best in terms of readability and perception. That’s why after finishing work with a particular piece of code, it makes sense to revise it.

Guard clause is a valuable refactoring technique helping in code improvement by making it much flatter and more readable.

Occasionally, a function may start with a conditional that checks parameters for suitability. As a result, the entire function may end up inside a single conditional, which makes it artificially complicated. By inverting the condition, we can reduce an additional indentation level making the code clearer.


Written by andrew.smykovv | Passionate Software Developer.
Published by HackerNoon on 2023/03/28