Avoiding the 3 most common python pitfalls

Table of contents

Python, a versatile and widely used programming language in software development, security, cloud computing, and AI, offers great power and flexibility. However, even experienced developers can stumble upon common pitfalls that introduce bugs, lead to unexpected behavior, or impact performance. By properly understanding these issues, you can prevent a future headache today.

Mutable default arguments

One of the subtle traps in Python arises when using mutable objects as default arguments in function definitions. For instance:

def append_to_list(item, my_list=[]):
    my_list.append(item)
    return my_list

result1 = append_to_list(1)
result2 = append_to_list(2)
print(result1)  # Output: [1, 2]

In this case, the default argument my_list=[] is evaluated only once during function definition, leading to a shared object across multiple calls. To avoid unexpected behavior, it is best to initialize the default argument as None and create a new list inside the function if needed:

def append_to_list(item, my_list=None):
    if my_list is None:
        my_list = []
    my_list.append(item)
    return my_list

This ensures that each function call receives its own independent list.

Confusing == and is

Comparing Mutable Objects with == Instead of 'is' Another common pitfall occurs when comparing mutable objects, such as lists or dictionaries, using the == operator instead of is. Consider the following example:

list1 = [1, 2, 3]
list2 = [1, 2, 3]
print(list1 == list2)  # Output: True
print(list1 is list2)  # Output: False

Although the lists have the same content, they are separate objects with different memory references. To check if they refer to the same instance, use the is operator:

list1 = [1, 2, 3]
list2 = [1, 2, 3]
print(list1 is list2)  # Output: False

By utilizing is when comparing mutable objects, you can ensure the desired behavior. This is also true for the None type: comparing with == will always be false, use is to check if a variable's value is None.

Modifying a List While Iterating over it

Modifying a list while iterating over it can lead to unexpected behavior or runtime errors. Consider the following example:

my_list = [1, 2, 3, 4, 5]
for item in my_list:
 if item % 2 == 0:
   my_list.remove(item)

In this scenario, elements may be skipped or the loop may terminate prematurely due to the modification of the list during iteration. To avoid such issues, iterate over a copy of the list or use list comprehensions:

my_list = [1, 2, 3, 4, 5]
my_list = [item for item in my_list if item % 2 != 0]

By creating a new list or using a list comprehension, you ensure that the original list remains unmodified during iteration.

Bonus: Boolean trickery

Despite python supporting booleans natively through True and False, they are actually integers under the hood. False is 0 and True is 1.

This leads to some unexpected edge cases, like being able to use booleans in calculations and comparisons:

True > 2 # True
False == 0 # True
True > 2 # False
False - 6 # -6
True * 8 # 8

Additionally, when casting to booleans using bool(), only None, 0 and empty strings/lists/dicts/tuples/sets will evaluate to False, while any integer other than 0 will be True, including negative values:

bool(set()) # False
bool("") # False
bool(0) # False
bool(4) # True
bool(-9) # True


More articles

Demystifying /dev: Understanding Linux Device Files

Understanding device files and their uses in linux

Cracking a password-protected zip file with john

A quick guide to crack password-protected zip (and other) archive files with john the ripper and a wordlist

Common pitfalls running docker in production

Avoiding the mistakes many make when embracing containerized deployments

Modern linux networking basics

Getting started with systemd-networkd, NetworkManager and the iproute2 suite

Understanding how RAID 5 works

Striking a balance between fault tolerance, speed and usable disk space