Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                

Refactoring long boolean expressions PREMIUM

Trey Hunner smiling in a t-shirt against a yellow wall
Trey Hunner
4 min. read 3 min. video Python 3.9—3.13
Python Morsels
Watch as video
03:09

How can you improve the readability of long Boolean expressions in Python?

Breaking up long expressions

Here's a fairly long Boolean expression:

from datetime import datetime

event = {"name": "Intro", "date": datetime(2030, 3, 6), "full": False}
user = {"name": "jill", "verified": True, "role": "admin", "perms": ["edit"]}

if user["verified"] and event["date"] > datetime.now() and not event["full"]:
    print("Here's the event signup form...")

We could make this a little bit more readable by splitting our code up over multiple lines (thanks to implicit line continuations), with each line starting with a Boolean operator:

from datetime import datetime

event = {"name": "Intro", "date": datetime(2030, 3, 6), "full": False}
user = {"name": "jill", "verified": True, "role": "admin", "perms": ["edit"]}

if (user["verified"]
        and event["date"] > datetime.now()
        and not event["full"]):
    print("Here's the event signup form...")

Or we could put the Boolean operators at the end of each line, if we prefer:

from datetime import datetime

event = {"name": "Intro", "date": datetime(2030, 3, 6), "full": False}
user = {"name": "jill", "verified": True, "role": "admin", "perms": ["edit"]}

if (user["verified"] and
        event["date"] > datetime.now() and
        not event["full"]):
    print("Here's the event signup form...")

But PEP8 (the official Python style guide) recommends putting binary operators (operators that go in between two values, like and and or) at the beginning of each line, for the sake of readability.

That way it's a little bit easier to see at a glance how we're joining our sub-expressions together:

from datetime import datetime

event = {"name": "Intro", "date": datetime(2030, 3, 6), "full": False}
user = {"name": "jill", "verified": True, "role": "admin", "perms": ["edit"]}

if (user["verified"]
        and event["date"] > datetime.now()
        and not event["full"]):
    print("Here's the event signup form...")

Naming sub-expressions with variables

We could also try using variables to name each part of our expression:

from datetime import datetime

event = {"name": "Intro", "date": datetime(2030, 3, 6), "full": False}
user = {"name": "jill", "verified": True, "role": "admin", "perms": ["edit"]}

user_is_verified = user["verified"]
event_in_future = event["date"] > datetime.now()
event_not_full = not event["full"]

if user_is_verified and event_in_future and event_not_full:
    print("Here's the event signup form...")

This allows us to quickly understand what the whole expression is meant to do, before we dive into any particular part of the expression.

Naming operations with functions

While a variable can give a name to the result of an expression, we could instead use a function to name the operation that's being checked by the expression:

from datetime import datetime

event = {"name": "Intro", "date": datetime(2030, 3, 6), "full": False}
user = {"name": "jill", "verified": True, "role": "admin", "perms": ["edit"]}

def is_verified(user): return user["is_verified"]
def in_future(event): return event["date"] > datetime.now()
def not_full(event): return not event["is_full"]

if is_verified(user) and in_future(event) and not_full(event):
    print("Here's the event signup form...")

Using functions for sub-expressions instead of using variables can be helpful if you need to rely on short-circuiting to make sure that some of your sub-expressions will not be run in certain cases.

Using De Morgan's Law

Naming things is a great way to improve readability.

But it's sometimes also helpful to rewrite your Boolean logic to something that's a little bit easier to read.

There's a computer science concept that can help us with this; it's called De Morgan's Law.

De Morgan's Law states that these two lines are equivalent:

>>> neither_one = not (a or b)
>>> neither_one = (not a) and (not b)

It also says that these two are equivalent:

>>> never_both = not (a and b)
>>> never_both = (not a) or (not b)

The rule with De Morgan's Law is that you can distribute the negation of a Boolean operation into each sub-expression, as long as you also flip all the Boolean operators. Meaning, and must becomes or, and or must become and.

Here we have a Boolean expression:

def has_edit_permission(user):
    return not (
        user["role"] == "admin"
        or "edit" in user["permissions"]
    )

We can distribute that not operator into each sub-expression, as long as we also flip the or to an and:

def has_edit_permission(user):
    return (
        not (user["role"] == "admin")
        and not ("edit" in user["permissions"])
    )

Now this code might not seem much more readable, until we realize that we could actually switch the operators we're using to remove those not statements:

def has_edit_permission(user):
    return (
        user["role"] != "admin"
        and "edit" not in user["permissions"]
    )

I find this final expression a lot more readable than what we started with. We started with not(user["role"] == "admin" or "edit" in user["permissions"], and we ended up with return(user["role"] != "admin" and "edit" not in user["permissions"].

Even if you don't memorize De Morgan's Law, keep in mind that you can rewrite your logic, and sometimes the rewrite is easier to read than what you started with.

Make your Boolean expression more readable

The next time you end up with an unwieldy Boolean expression, try splitting up your code over multiple lines, or try embracing variables or function calls to give names to each of your sub-expressions.

You could also try out using De Morgan's Law to see if distributing or un-distributing a negation might make your expression a little bit easier to read.

Series: Conditionals

Conditionals statements (if statements) are useful for making a branch in our Python code. If a particular condition is met, we run one block of code, and if not then we run another block.

To track your progress on this Python Morsels topic trail, sign in or sign up.

0%
Python Morsels
Watch as video
03:09
This is a free preview of a premium screencast. You have 2 previews remaining.