Common Python Anti-Patterns to watch out for

Koushik Thota
6 min readApr 22, 2022

Anti-patterns which will make you more mindful while writing code.

Photo by Markus Spiske on Unsplash

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 = True
if number == None or flag == True:
print("This works, but is not the preferred PEP 8 pattern")

Good

number = None 
flag = True
if 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)

References

--

--

Koushik Thota

Ex - child | Data Engineer | Loves Tech and Finance