Embercode
✕
ConditionalsLesson 6 of 14

40 min read

Guard clauses and early return in Python

Why this matters

A function often has to check several things before it does its real work. A bank withdrawal must check the card, the amount and the balance. A form must check each field. Beginners usually write each check as an if around the next check, so the code moves further to the right with every rule. By the fifth rule, the line that does the real work sits inside five levels of indentation, and the reader has to hold all five conditions in their head to understand it.

A guard clause is an if that handles a bad case and leaves the function at once. Once the bad cases are handled at the top, the rest of the function can assume that the input is good. This lesson shows how to turn nested if blocks into a flat list of checks. It also covers the mistakes that appear when you do it: a missing return, code that can never run, and a check that is written the wrong way round.

Python shows the missing return mistake differently from some other languages. A function that reaches its end without a return does not stop the program when you save it. It gives back None, which is Python's word for "no value", and the error shows up later, where the value is used.

The next lesson, on De Morgan rewrites, shows how to flip a negated condition into an equal one.

The problem: code that moves to the right

Here is a withdrawal function written in the natural first way. Each rule is a new if inside the previous one. A function is a named block of code that you call with values, such as withdraw_nested(100, 30, True). True and False are the two truth values, and Python writes them with capital letters.

Python · editable

Output:

approved, new balance 70
declined: amount must be positive
declined: not enough money
declined: card is not active

The function works, but the rule that matters most, "the withdrawal is approved", sits at the deepest level. To find it, you must read down through three if lines and their else partners. The indentation makes the function look like an arrow that points to the right. Programmers call this the pyramid or the arrowhead shape.

The idea: handle the bad cases first and leave

A guard clause turns the question around. Instead of "if everything is good, do the work", it asks "if something is wrong, stop now".

Think of a hotel check-in desk. The clerk does not walk you through a long series of checks and then hand you a key at the end of them. The clerk looks at the first problem, such as no booking, and says "no" at once. Each check that fails ends the process. Only a guest who passes every check gets the key.

In Python, "stop now" means return. When Python reaches a return, it leaves the function immediately and hands back the value after the word. Any lines below the return do not run. This is what makes a guard clause work. The check ends the function, so the code below it does not need an else to protect it.

Here is the same withdrawal written with guard clauses:

Python · editable

Output:

approved, new balance 70
declined: amount must be positive
declined: not enough money
declined: card is not active

The output is the same as before. The code is different in three ways.

First, every line has the same indentation. The function has no arrow shape.

Second, each check is one short if with one return. You can read the function from top to bottom and treat each check as a separate rule. Each rule removes one kind of bad input.

Third, the happy path is at the bottom. The last line, return f"approved, new balance ...", is the one that runs when every check has passed. It is not hidden inside three levels.

Notice also that the checks are written with the bad case in the condition. The first check asks "is the card not active?" with not card_active (not means "the opposite of", and it is the Python word for the ! that some other languages use), not "is the card active?". This is the core habit of a guard clause. You pick the condition that means "stop here", and you write that condition as the if.

Step by step: rewriting nested ifs

You can turn most nested if blocks into guard clauses by repeating four steps.

  1. Find the outer if and write down what its condition checks. Ask yourself what the if is for. In the withdrawal example, the outer check is "is the card active?"
  2. Write the opposite of that condition, which is the bad case. "Card is active" becomes "card is not active", written as not card_active.
  3. Put that bad-case condition in a new if with the return of the bad result inside its block. Place it before the main work.
  4. Remove the else, and move the code that followed the old if back one level of indentation. That code now sits at the same level as the guard. Repeat from step 1 with the next nested if.

The else goes away because it is no longer needed. After a return, the function has already stopped, so nothing that follows can run when the guard fires. Writing else after a return is not wrong, but it adds a level of indentation for no reason.

A second example: returning early from a loop

Early return also works inside a loop. A for loop runs its indented block once for each item in a list. enumerate gives each item together with its position, which starts at 0. So for i, name in enumerate(names) gives i the position and name the item. This is a common place where beginners write a flag variable that they never need.

The function below searches a list of names and returns the position of the first match. If the name is not there, it returns -1. The value -1 is a common choice for "not found", because no valid position in a list can be negative.

Python · editable

Output:

1
-1

The loop returns as soon as it finds the name. It does not need to keep looking, so it does not need a variable such as found to remember what happened. The return -1 at the bottom runs only when the loop finishes without a match.

Python lists also have an index method (a function attached to a list, called with a dot), which returns the position of an item. It raises an error when the item is missing, so the loop above is the safer choice when a missing item is normal:

names = ["ana", "ben", "chris"]
print(names.index("zoe"))

The program stops, and the last line of the error message is:

ValueError: list.index(x): x not in list

A version with a flag does the same job, but it is longer:

def find_index_with_flag(names, target):
    answer = -1
    found = False
    for i, name in enumerate(names):
        if not found and name == target:
            answer = i
            found = True
    return answer

This version has two variables, the found flag and the answer. The loop keeps going after the match, and the not found test stops it from changing the answer again. A reader has to follow both variables to see what the function returns. The early return version has fewer parts, and each part does one thing.

A third example: a list of checks with messages

The last example has several checks, and each check gives its own message. This is the kind of function you write when you validate user input.

The rules are: the name must have at least 3 characters, no more than 12, and only letters and digits. The function returns a short message that names the first rule that failed, or ok if all rules pass.

Python · editable

Output:

too short
too long
only letters and digits allowed
ok

len(name) gives the number of characters in the text. A for loop over a string visits one character at a time, and ch.isalnum() is true when the character is a letter or a digit. The underscore in sam_01 is neither, so it fails the check.

The order of the checks matters. The length checks come first because they are quick. They do not look at each character. The loop that checks each character comes last, because it does more work. A bad length stops the function before the loop starts. The order also decides which message the user sees. A name of 20 characters with an underscore gets "too long", not the character message, because the length check is first.

Each return inside the loop ends the function, so the loop does not need its own flag. The line return "ok" runs only when every check has passed.

A fourth example: checks first, then the work

Many functions do more than return a message. They check the input, then calculate a value from it. The guard clauses still go at the top, so the calculation runs only on values that are known to be good.

The function below calculates a delivery fee. A parcel must weigh more than 0 kg and no more than 30 kg. The base fee is 5.00 plus 0.50 for each kilogram. Express delivery doubles the fee, and a rural address adds 4.00.

Python · editable

Output:

fee 6.0
fee 16.0
invalid weight
too heavy, call us
fee 20.0

Follow the last call by hand. The weight 10 passes both guards. The base fee is 5.00 + 10 * 0.50, which is 10.0. The express value is True, so the fee doubles to 20.0. The rural value is False, so the fee stays the same. The function returns fee 20.0.

The two if blocks in the middle are not guards. Neither one ends the function. Each changes the fee and lets the function continue. A guard ends the function with return, and an ordinary if changes a value. Telling the two apart is the skill this example teaches. When you read a function, a return inside an if tells you the function might stop there. An if with no return tells you that a value might change.

When not to use a guard clause

A guard clause is a tool, not a rule for every if. Use one when the bad case should stop the function. Use an ordinary if and else when both cases produce a value that the rest of the function needs.

The function below picks a price. Members pay 8.0 and everyone else pays 10.0. Both branches set the same variable, and the function returns it at the end. A guard here would need a second return and would not read better. The second function shows the one-line form, which Python calls a conditional expression. It is useful for a short choice like this one. A longer choice with several lines in each branch reads better as an if block.

Python · editable

Output:

8.0
10.0
8.0
10.0

A function with many return lines can also be hard to scan, because a reader has to check every exit to know what the function does. When a function has more exits than fit on one screen, consider splitting its checks into a second function. This is a judgment call, and it depends on how the code reads to the people who work on it.

Guard clauses in a function that returns nothing

A function with no return value still gives back something. When it reaches its end, it returns None. A return written on its own, with no value after it, ends the function early and also gives back None. Some other languages have a separate kind of function for this case, but in Python it is the same kind of function.

This is where beginners make the most serious mistake in this lesson. Here is a function that prints a message for people who are too young. The first version forgets the return.

Python · editable

Output:

Too young
Welcome
Too young
Too young
None

The first function prints "Welcome" even for a 12-year-old. The if only controls the line inside its block. Without a return, the function goes on to the next line. The second function stops after the bad message, so "Welcome" is not printed. The last line shows the value that the call gives back. The function's job is to print, so it returns None, and print shows None.

Common mistakes

Forgetting the return inside a guard

This is the mistake shown above. A guard clause that does not return is only an if, and the code after it runs anyway. Python does not warn you, because the code is valid. The program simply gives the wrong answer. When a function prints two messages that should be one, check each guard for a return.

A missing return gives None

In some languages, a function that returns a value must end every path with a return, or the program does not compile. Python does not check this. A function that reaches its end without a return gives back None:

Python · editable

Output:

pass
None

The mistake usually shows up later, at the place where the value is used. This line joins the result with text:

def grade(score):
    if score >= 50:
        return "pass"
 
 
print("Result: " + grade(30))

The program stops with an error. The last line of the error message is:

TypeError: can only concatenate str (not "NoneType") to str

Read the last line first. It says that a text value was joined with None, which has no text to join.

The fix is to add a return for the other path. The if handles the first case, and the final return handles everything else:

def grade(score):
    if score >= 50:
        return "pass"
    return "fail"

Code after a return never runs

In some languages, a line placed after a return in the same block is an error. Python allows it, and the line never runs:

Python · editable

Output:

fail

Python does not report the second line, and the program does not stop. A code checker or an editor may flag it. If you see a line like this, look for a line that you forgot to move above the return, or a return that you put in the wrong place.

Writing the check the wrong way round

A guard clause checks for the bad case. Beginners sometimes write the good case instead. For example, to stop a withdrawal of zero or less, you need amount <= 0, not amount > 0. Writing if amount > 0: return "declined: amount must be positive" would decline every good withdrawal and allow the bad ones. Read each guard aloud and check that the condition describes the thing you want to refuse.

Keeping an else after a return

Writing else after a return runs correctly, but it adds an indentation level for no reason. In the nested withdrawal, the else after return "declined: amount must be positive" is not needed, because that return already stops the function. The guard form has no else at all.

Practice

Exercise 1

Rewrite this function so that it uses guard clauses and has no else:

def discount(age, member):
    if age >= 65:
        if member:
            result = "30 percent"
        else:
            result = "15 percent"
    else:
        result = "none"
    return result

Show the answer

def discount(age, member):
    if age < 65:
        return "none"
    if member:
        return "30 percent"
    return "15 percent"

The first guard handles everyone under 65. The second guard handles members of 65 or older. The last line handles non-members of 65 or older. The function returns the same results as the nested version for all inputs.

Exercise 2

Write a function first_negative(values) that returns the position of the first value below zero, or -1 if there is none. Use a loop and an early return. Do not use a flag variable.

Show the answer

def first_negative(values):
    for i, value in enumerate(values):
        if value < 0:
            return i
    return -1
 
 
print(first_negative([4, 9, -2, -7]))
print(first_negative([1, 2]))

The first call prints 2, and the second call prints -1.

Exercise 3

Rewrite this function so that it returns from inside the loop as soon as it finds a zero:

def contains_zero(values):
    found = False
    for value in values:
        if value == 0:
            found = True
    return found

Show the answer

def contains_zero(values):
    for value in values:
        if value == 0:
            return True
    return False
 
 
print(contains_zero([3, 0, 5]))
print(contains_zero([3, 5]))

The loop returns True at the first zero. If the loop ends without finding one, the final line returns False. The function prints True for [3, 0, 5] and False for [3, 5]. Python also has a shorter test for this, 0 in values, which is True when the value is in the list.

Exercise 4

The function below is valid Python, so Python does not complain about it. Predict what size(150) prints, then check.

def size(n):
    if n < 10:
        return "small"
    elif n < 100:
        return "medium"

Then add a final return so that size(150) returns "large", without changing the results for numbers under 100.

Show the answer

Python prints None. For 100 or more, neither if matches, the function reaches its end, and it gives back None. The fix is to add a final return for the remaining case:

def size(n):
    if n < 10:
        return "small"
    elif n < 100:
        return "medium"
    return "large"

The elif after the first return is allowed here because the if and elif lines are one decision with three outcomes. The final return handles every value that the first two did not catch.

Exercise 5

Predict the output of this program, then check it. Does the second call still run?

def report(temperature):
    if temperature > 30:
        print("Hot")
        return
    print("Not hot")
 
 
report(35)
report(20)

Show the answer

Hot
Not hot

Both lines of the program run. The return inside the guard ends report only for the call with 35. When the call returns, the program continues to the second call. A return ends the function it is in, not the whole program.

Summary

  • A guard clause is an if that checks for a bad case and ends the function with return.
  • Replacing nested if blocks with guards keeps the main path at one indentation level and puts the real work at the bottom.
  • Write the guard's condition as the bad case, and check it by reading it aloud.
  • Every return ends the function at once, so code after a return in the same block can never run.
  • A return with no value gives back None, and so does a function that reaches its end. Python reports that mistake only where the value is used.
Practice this topic: coding problems for Conditionals →

Stuck? Ask the tutor

Ask about anything in this lesson. Answers are short and use simple words.

Sign in to ask the tutor.

← Module assignment: Ferry booking desk in Python