Common Python Anti-Patterns to watch out for
--
Anti-patterns which will make you more mindful while writing code.
In software, anti-pattern is a term that describes how NOT to solve recurring problems in your code. Anti-patterns are considered bad software design, and are usually ineffective or obscure fixes.
Below are a few Python anti-patterns that I have encountered/rectified upon. Learning about these anti-patterns will help you to avoid them in your own code and make you a better programmer (hopefully).
Not Using Context Managers for Files
This is the most common anti pattern I have observed in numerous code reviews. Context managers in python help to facilitate proper handling of resources, to control what to do when objects are created or destroyed. This removes the overhead of handling the creation and deletion of objects.
Bad
file_obj = open('abc.txt', 'r')
data = file_obj.read()
file_obj.close()
Good
with open('abc.txt', 'r') as file_obj:
data = file_obj.read()
Using type() to compare types
isinstance()
is usually the preferred way to compare types. It’s not only faster but also considers inheritance, which is often the desired behavior. In Python, you usually want to check if a given object behaves like a string or a list, not necessarily if it’s exactly a string. So instead of checking for string and all it’s custom subclasses, you can just use isinstance
.
Bad
name = 'Alex'if type(name)== str:
print('It is a string')
else:
print('It is not a string')
Good
name = 'Alex'if isinstance(name,str):
print('It is a string')
else:
print('It is not a string')
Use of exec
The exec
statement enables you to dynamically execute arbitrary Python code which is stored in literal strings. Building a complex string of Python code and then passing that code to exec
results in code that is hard to read and hard to test. Anytime the Use of exec
error is encountered, you should go back to the code and check if there is a clearer, more direct way to accomplish the task.
Bad
s = "print(\"Hello, World!\")"exec s
Good
def print_word():
print("Hello, World!")print_word()
No exception type(s) specified
Not specifying an exception type might not only hide the error but also leads to losing information about the error itself. So its better to handle the situation with the appropriate error rather than the generic exception otherwise as shown below.
Bad
try:
5 / 0
except:
print("Exception")
Good
try:
5 / 0
except ZeroDivisionError as e:
print("ZeroDivisionError")
except Exception as e:
print("Exception")
else:
print("No errors")
finally:
print('Good day')
With this pattern, you are able to handle exceptions based on their actual exception-type. The first exception type that matches the current error is handled first. Thus, it is recommended to handle specific exception types first (e.g., ZeroDivisionError) and generic error types (e.g., Exception) towards the end of the try-except block.
Using map() or filter() instead of list comprehension
For simple transformations that can be expressed as a list comprehension, its better to use list comprehensions over map()
or filter()
as map and filter are used for expressions that are too long or complicated to express with a list comprehension. Although a map()
or filter()
expression may be functionally equivalent to a list comprehension, the list comprehension is generally more concise and easier to read.
Bad
values = [1, 2, 3]doubles = map(lambda num: num * 2, values)
Good
values = [1, 2, 3]doubles = [num * 2 for num in values]
Not using else where appropriate in a for loop
Python provides a built-in else clause for for loops. If a for loop completes without being prematurely interrupted by a break or return statement, then the else clause of the loop is executed.
Bad
numbers = [1, 2, 3]
n = 4
found = False
for num in numbers:
if num == n:
found = True
print(“Number found”)
break
if not found:
print(“Number not found”)
Good
numbers = [1, 2, 3]
n= 4
for num in numbers:
if num == n:
print(“Number found”)
break
else:
print(“Number not found”)
Using single letter variable names
Bad
l = [2,3,4,5]
Good
numbers = [2,3,4,5]
Asking for permission instead of forgiveness
The Python community uses an EAFP (easier to ask for forgiveness than permission) coding style. This coding style assumes that needed variables, files, etc. exist. Any problems are caught as exceptions. This results in a generally clean and concise style containing a lot of try
and except
statements.
Bad
import os
my_file = "/path/to/my/abc.txt"
if os.access(my_file, os.R_OK):
with open(my_file) as f:
print(f.read())
else:
print("File can't be accessed")
Good
import os
my_file = "/path/to/my/abc.txt"try:
f = open(my_file)
except IOError as e:
print("File can't be accessed")
else:
with f:
print(f.read())
Comparing with None/Boolean Values in the wrong way
Bad
number = None
flag = Trueif number == None or flag == True:
print("This works, but is not the preferred PEP 8 pattern")
Good
number = None
flag = Trueif number is None or flag is True:
print("PEP 8 Style Guide prefers this pattern")
Using wildcard imports
Importing every public name from a module using a wildcard (from mymodule import *
) is a bad idea because:
- It could lead to conflicts between names defined locally and the ones imported.
- It reduces code readability as developers will have a hard time knowing where names come from.
- It clutters the local namespace, which makes debugging more difficult.
Instead Replace it with import mymodule
and access module members as mymodule.myfunction
. If the module name is too long, alias it to a shorter name. Example: import pandas as pd
Bad
from collections import *
Good
from collections import Counter
Not using zip() to iterate over a pair of lists
Bad
numbers = [1, 2, 3]
letters = ["A", "B", "C"] for index in range(len(numbers)):
print(numbers[index], letters[index])
Good
numbers = [1, 2, 3]
letters = ["A", "B", "C"] for numbers_value, letters_value in zip(numbers, letters):
print(numbers_value, letters_value)
Using key in list to check if key is contained in list
Using key in list
to iterate through a list can potentially take n
iterations to complete, where n
is the number of items in the list.
If possible, you should change the list to a set or dictionary instead, because Python can search for items in a set or dictionary by attempting to directly accessing them without iterations, which is much more efficient (Since set in python is implemented as a hash table and time complexity of accessing an element is O(1)).
This is not applicable if your list has duplicates and you want to find all occurrences of the element.
Bad
l = [1, 2, 3, 4] if 4 in l:
print("The number 4 is in the list.")
else:
print("The number 4 is NOT in the list.")
Good
s = {1, 2, 3, 4}if 4 in s:
print("The number 4 is in the list.")
else:
print("The number 4 is NOT in the list.")
Not using get()
to return default values from a dictionary
Frequently you will see code create a variable, assign a default value to the variable, and then check a dictionary for a certain key. If the key exists, then the value of the key is copied into the value for the variable. While there is nothing wrong this, it is more concise to use the built-in method dict.get(key[, default])
from the Python Standard Library. If the key exists in the dictionary, then the value for that key is returned. If it does not exist, then the default value specified as the second argument to get()
is returned.
Bad
stocks = {'AMZ': 'Amazon', 'AAPL': 'Apple'}
if 'AMC' in stocks:
stock_name = stocks['AMC']
else:
stock_name = 'undefined'
Good
stocks = {'AMZ': 'Amazon', 'AAPL': 'Apple'}stock_name = stocks.get('AMC', 'undefined')
Not Using explicit unpacking
Unpacking is more concise and less error prone as manually assigning multiple variables to the elements of a list is more verbose and tedious to write.
Bad
cars = ['BMW', 'Mazda', 'Tesla']
car_1 = cars[0]
car_2 = cars[1]
car_3 = cars[2]
Good
cars = ['BMW', 'Mazda', 'Tesla']
car_1,car_2,car_3 = cars
Not Using enumerate() in loops
Creating a loop that uses an incrementing index to access each element of a list within the loop construct is not the preferred style for accessing each element in a list. The preferred style is to use enumerate()
to simultaneously retrieve the index and list element.
Bad
cars = ['BMW', 'Mazda', 'Tesla']for ind in range(len(cars)):
print(ind,cars[ind])
Good
cars = ['BMW', 'Mazda', 'Tesla']for ind,car in enumerate(cars):
print(ind,car)