It is no secret that Python is one of the most popular programming languages there is.
The combination of Python being flexible, dynamically typed, and that new programmers have an appeal for it, can result in erroneous, inefficient code.
In this article, I am going to go through 3 Python anti-patterns and how to avoid them so that you won’t repeat the mistakes of other developers.
It is by no means an article directed at the beginner developer, as even experienced developers, might be introduced to a certain anti-pattern that they didn’t think of before.
To contribute to my point that even experienced developers might be introduced to anti-patterns they didn’t think of before, I am going to start with an anti-pattern that I used to fall for up until recently.
1. Check if a list contains an element
When using lists, a common operation is to check whether a certain element is in the list or not, for example
some_list = [1, 2, 3] if 3 in some_list: #do something
Leaving aside my horrible variable naming, this code snippet above is an anti-pattern in terms of performance that stems from the difference in implementation between lists and sets.
Sets are implemented as a hash table, which means that in the average case a search can be performed in O(1) over a set (O(n) in the worst case).
On the other hand, lists are simply contiguous memory cells which means that the search complexity is O(n).
To emphasize how bad this anti-pattern can get, I conducted a small experiment, and even though I expected bad results I was surprised by how extreme they were.
timeit module in python, I measured how much time it takes to perform 1k conditionals that check whether an element is in the list or not, using the below function.
for i in range(1000):
if i in iterable:
To this function, I will pass a set and a list and compare the execution time of each experiment using the below snippet
from timeit import timeit timeit("contains_test(iterable)", setup="from __main__ import contains_test; iterable = set(range(1000))", number=1000) timeit("contains_test(iterable)", setup="from __main__ import contains_test; iterable = list(range(1000))", number=1000)
And here are the shocking results:
- When using a set as an iterable, the execution time was 0.0744 seconds.
- When using a list as an iterable, the execution time was 5.624 seconds.
If you divide both results you get that the execution time of the list test is ~75 times more than the set test execution time.
Important Note: This is not me saying “list bad, set good”, sets and lists are very different in various ways, and far from being interchangeable.
The results I demonstrated above will be correct only for membership testing, not any other functionality.
2. General exception handling
In contrast to the previous anti-pattern in terms of performance, this anti-pattern is related to the correctness of your program.
We have all been there, we try to execute some code, we get a weird error we don’t understand, and we decide to whip out our secret weapon which is a try-except block.
There is nothing wrong with try-except in general unless it is done in a reckless way as featured below
def divide(a, b):
result = a / b
In the above snippet, we can see that just a few lines of code can cause a variety of errors, namely:
- ZeroDivisionError for b=0
- TypeError for a or b that are strings (or any other objects that don’t support the division operator)
While utilizing this general exception handling, we will just return None upon any error that the program comes across, and we basically hide other potential errors using this mechanism.
The best way to deal with this anti-pattern is to be as explicit as possible with exception types. For example, the best practice for the above code would be the following.
def divide(a, b):
result = 0 try:
result = a / b
except Exception as e:
With this pattern, you are handling exceptions based on their type.
Since the first exception that is matched will be handled, it is recommended to handle specific types of exception first (e.g. ZeroDivisionError, TypeError) and then handle generic exception types (e.g. Exception).
3. LBYL vs EAFP
This practice might surprise people that are new to Python coming from other languages such as C.
LBYL stands for “Look Before You Leap” which basically means that the program should check whether something can be completed successfully and proceed only if that’s the case.
For example, LBYL in Python can be expressed in the below snippet
if "key" in some_dict:
In the above snippet, we first checked that there is a key called “key” in our dictionary and only then proceeded to print its value.
A different practice that is an idiomatic Python practice is EAFP which stands for “Easier to Ask for Forgiveness than Permission”.
Using this practice, we basically assume that something will succeed, and if not, we will handle it in a certain way.
For example, EAFP can be expressed in the below snippet using try-except blocks.
In the above snippet, we just assumed “key” is a valid key in our dictionary and tried to access it, if it failed, we will proceed to handle it in a certain way.
EAFP relies on the assumption that most of the time, statements won’t fail and thus all the checks that are done with the LBYL practice might be unnecessary and wasteful in execution time.
Although this article has come to an end, there are many more bad practices and anti-patterns out there.
Make sure you explore them, understand them, and hopefully avoid them at all costs.