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