Python Refactorings - Part 8

Six more examples of ways to refactor your Python code, and why they are improvements

Date

Feb 22, 2022

Python code
Photo by Chris Ried on Unsplash

Writing clean, Pythonic code is all about making it as understandable, yet concise, as possible. This is the eighth part of a series on Python refactorings, based on those that can be done automatically by Sourcery. Here are parts 1, 2, 3, 4, 5, 6, and 7.

The focus of this series is on why these changes are good ideas, not just on how to do them.

Merge Repeated Ifs

Don’t Repeat Yourself (DRY) is a core tenet of programming best practices. Often we think about it for large sections of code, but we also want want to avoid repeating conditionals when possible. For instance - if we’re dealing with a couple of variables that are modified when the same condition holds we might have code structured like:

if wardrobe.hats:
    self.happiness += 1
else:
    self.happiness -= 1

if wardrobe.hats:
    self.stylishness += 1
else:
    self.stylishness -= 1

Here we’re repeating both the if wardrobe.hats: as well as the else: and can easily group together the variable changes so we only have a single conditional check.

if wardrobe.hats:
    self.happiness += 1
    self.stylishness += 1
else:
    self.happiness -= 1
    self.stylishness -= 1

We’ve now cut down on the duplication and we’ve significantly shortened and simplified the code we’re dealing with. An important note is that we can only simplify our code like this if the statements inside the first if statement do not impact the condition itself.

Merge Dictionary Assignments

A pattern that we often see when initialising a dictionary is to declare an empty dictionary and then add items into it line by line.

hats_i_own = {}
hats_i_own["panama"] = 1
hats_i_own["baseball_cap"] = 2
hats_i_own["bowler"] = 23

This winds up taking up many more lines than necessary. Instead you can merge the whole assignment process together in a single line - declaring the dictionary alongside the items in it

hats_i_own = {"panama": 1, "baseball_cap": 2, "bowler": 23}

Now while reading the code, if we are not interested in the individual elements the initialisation no longer uses up so much screen real estate, but they are still there if we need to read them.

Removing unnecessary calls to dict.items()

Sticking with dictionaries for the moment - if we are using both keys and values from a dictionary we need to call dict.items() . But, if we’re only calling for the keys then this call is unnecessary. For instance:

for name, age in people.items():
    print("Hi, my name is", name)

In this example we’re grabbing the name of each person in people, but we’re only using the dictionary key and never the value (their age) so we don’t actually need the .items(). Instead we can simplify the code down to:

for name in people:
    print("Hi, my name is", name)

This is a bit nicer to read because we no longer have the unnecessary dict.items call and we also are able to remove the unused variable age.

If we were calling the key and the value of each item in the dictionary we would need to maintain the dict.items().

Remove str() from Calls to print()

When we’re printing something in Python we don’t need to explicitly call str() to print non-strings - it’s done automatically. So if we have something like:

print(str(1))

We can simplify it down to

print(1)

Cutting out the str() call is a nice, easy way to simplify print statements, that is also a slight performance improvement.

Flatten Nested Try

Nested try-except statements don’t create the same complexity issues as what we can run into from traditional nested conditionals. But, they’re still best to avoid from an overall readability perspective. For example:

def testConnection(db, credentials):
    try:
        try:
            db.connect(credentials)
        except InvalidCredentials:
            return "Check your credentials"
        except ConnectionError:
            return "Error while trying to connect"
    finally:
        print("Connection attempt finished")
    return "Connection Successful"

Has a nested try at the top that doesn’t need to be included this is because try can contain both a series of except clauses and a finally. We can therefore cut it down to:

def testConnection(db, credentials):
    try:
        db.connect(credentials)
    except InvalidCredentials:
        return "Check your credentials"
    except ConnectionError:
        return "Error while trying to connect"
    finally:
        print("Connection attempt finished")
    return "Connection Successful"

The reduction in nesting has made this code a bit clearer and easier to read.

Convert Any to In

When we’re dealing with a list we’ll often want to check whether or not a certain value exists in that list. One common way to handle that is using any.

if any(hat == "bowler" for hat in hats):
    shout("I have a bowler hat!")

This works, but Python’s built in in operator can make it even easier to handle this check:

if "bowler" in hats:
    shout("I have a bowler hat!")

Now we’re able to do the exact same check, but in a much clearer way. At a quick glance we can see that we’re checking whether an item is in a list.

Conclusion

As mentioned, each of these is a refactoring that Sourcery can automatically perform for you. We're planning on expanding this blog series out and linking them in as additional documentation, with the aim of turning Sourcery into a great resource for learning how to improve your Python skills. You can read the next part in the series here.

If you have any thoughts on how to improve Sourcery or its documentation please do email us or hit me up on Twitter