Quick Python Refactoring Tips (Part 2)

Quick Python Refactoring Tips (Part 2)

8 quick Python refactoring tips for cleaner and more pythonic code.

In this tutorial I show you another 8 quick Python refactoring tips for cleaner and more pythonic code. This is part 2 of my refactoring code series.

Part 1 can be found here: Refactoring Part 1

1. Merge append into list declaration

Let’s start with a simple one. Instead of declaring an empty list and then appending to it, just initialize the list directly with all elements. This shortens the code and makes the intent more explicit. It is also slightly more performant since it avoids the function calls to append().

players = []
players.append("Patrick")
players.append("Max")
players.append("Jessi")

# -> refactor
players = ["Patrick", "Max", "Jessi"]

The same holds true for filling up other collection types like sets and dictionaries.

2. Use items() to directly unpack dictionary values

When iterating over a dictionary and you need both the key and the value, then don’t access the values manually. Instead iterate over dictionary.items() which gives you both the keys and values at the same time.

This saves us the line that we used to assign to players, and the code now reads more naturally, with a touch less duplication.

teams_by_color = {"blue": ["Patrick", "Jessi"]}

for team_color in teams_by_color:
    players = teams_by_color[team_color]
    if is_winning(team_color):
        advance_level(players)

# -> refactor
for team_color, players in teams_by_color.items():
    if is_winning(team_color):
        advance_level(players)

3. Replace range(len) with enumerate

If we need to iterate over a list and need to track both the index and the current item, use the built-in enumerate() function instead of range(len). This returns both the current index and the current item as a tuple. So we can directly check the value here and also access the item with the index.

for i in range(len(players)):
    print(i, players[i])

# -> refactor
for i, player in enumerate(players):
    print(i, player)

Enumerate also comes with an optional start argument. If you use it, the counter starts at this value. But be aware that the items still start at the very first one.

for i, player in enumerate(players, start=1):
    print(i, player)

4. Replace a manual loop counter with a call to enumerate

This is very similar to before. Sometimes I see code where iteration is performed over the items directly - which is not bad by itself - but then a counter is needed and this gets manually incremented inside the loop. Again here you can simply use the enumerate function. This is simpler and also faster.

i = 0
for player in players:
    print(i, player)
    i += 1

# -> refactor
for i, player in enumerate(players):
    print(i, player)

4.1 Don’t manually update counter

If you just need to count the number of items, also don’t iterate over the loop and manually count all items. Instead, simply use the len() function to get the number of elements in the list.

num_players = 0
for player in players:
    num_players += 1

# -> refactor
num_players = len(players)

5. Simplify conditional into return statement

When we reach the end of a method and want to return True or False, a common way of doing this is like so. If the condition is True, we return True. And otherwise we return False at the end. However, it’s neater just to return the result directly:

def function():
    if isinstance(a, b) or issubclass(b, a):
        return True
    return False

# -> refactor
def function():
    return isinstance(a, b) or issubclass(b, a)

One thing we should be aware of here is that this can only be done if the expression evaluates to a boolean. Both isinstance() and issubclass() are functions that return a boolean, so this is fine. But in the next example the first expression pythonistas is a list and not a boolean value.

If pythonistas is a valid non-empty list, this would return the list instead of an expected boolean and then potentially be a bug in your application. So to make sure we’re returning a boolean here, we can wrap the return in a call to the bool() function.

def any_pythonistas():
    pythonistas = [coder for coder in coders if is_good_in_python(coder)]
    if pythonistas or self.is_pythonista():
        return True
    return False

# -> refactor
def any_hats():
    pythonistas = [coder for coder in coders if is_good_in_python(coder)]
    return bool(pythonistas or self.is_pythonista())

6. Merge duplicate blocks in conditional

We should always be searching for opportunities to remove duplicated code. A good place to do so is where there are multiple identical blocks inside an if …elif chain.

In this example both the if and the elif lead to the same performed function. So we can combine the first two blocks using or to remove the duplicated call to the function. Now if we need to change the process_standard_payment() line we can do it in one place instead of two.

def process_payment(payment, currency):
    if currency == "USD":
        process_standard_payment(payment)
    elif currency == "EUR":
        process_standard_payment(payment)
    else:
        process_international_payment(payment)

# -> refactor
def process_payment(payment, currency):
    if currency == "USD" or currency == "EUR":
        process_standard_payment(payment)
    else:
        process_international_payment(payment)

7. Replace multiple comparisons of same variable with in operator

We can refactor the previous code even one step further. Since we repeatedly check the same variable against multiple values, we can shorten this by using the in operator. If the currency value is in the defined list, we do the dedicated action.

def process_payment(payment, currency):
    if currency == "USD" or currency == "EUR":
        process_standard_payment(payment)
    else:
        process_international_payment(payment)

# -> refactor
def process_payment(payment, currency):
    if currency in ["USD", "EUR"]:
        process_standard_payment(payment)
    else:
        process_international_payment(payment)

And to improve this once more, we should use a set here. Looking up values in a set is faster, and we want unique elements here anyway, so a set is the better choice.

# -> refactor
def process_payment(payment, currency):
    if currency in {"USD", "EUR"}:
        process_standard_payment(payment)
    else:
        process_international_payment(payment)

8. Replace yield inside for loop with yield from

This is an advanced tip if you are already familiar with generators. One little trick that often gets missed is that Python’s yield keyword has a corresponding yield from for iterables.

If you have an iterable like a list, instead of saying for item in iterable: yield item, you can simply say yield from iterable. This is shorter and removes the manual looping over the iterable, which can also result in an improved performance.

def get_content(entry):
    for block in entry.get_blocks():
        yield block

# -> refactor
def get_content(entry):
    yield from entry.get_blocks()

Bonus: Free VS Code/PyCharm Refactoring Extension:

There exists an extension for VS Code and PyCharm that helps you identify these refactoring patterns. This extension is Sourcery. Sourcery is a free extension that you can easily install and which then gives you helpful refactoring hints. You can install and test it here: Sourcery.ai *

* Note: That's an affiliate link 😉

Resources