Common Python Pitfalls and How to Avoid Them
This article enumerates frequent Python pitfalls—including UnboundLocalError, mutable default arguments, subtle differences between x+=y and x=x+y, tuple syntax, shared mutable containers, list mutation during iteration, closure late binding, misuse of del, import inconsistencies, version‑specific changes, and the GIL—providing explanations and correct practices to help developers write safer code.
Python is an easy‑to‑learn yet powerful language, but it contains several subtle pitfalls that can surprise developers, especially those transitioning from other languages. This article lists and explains many of these pitfalls, offering correct patterns and work‑arounds.
UnboundLocalError : When a variable is referenced before assignment inside a function, Python raises an UnboundLocalError . Example:
<code>>> a=1
>>> def func():
... a+=1
... print a
>>> func()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<stdin>", line 2, in func
UnboundLocalError: local variable 'a' referenced before assignment</code>A more advanced version occurs when a name is imported conditionally:
<code>import random
def func(ok):
if ok:
a = random.random()
else:
import random
a = random.randint(1, 10)
return a
func(True) # UnboundLocalError: local variable 'random' referenced before assignment</code>These errors are not considered traps because they fail loudly; the real traps are those that appear to work but behave incorrectly.
1. Mutable Objects as Default Arguments
Using a mutable object (e.g., a list) as a default parameter causes the same object to be shared across calls:
<code>>> def f(lst=[]):
... lst.append(1)
... return lst
>>> f()
[1]
>>> f()
[1, 1]</code>Default values are evaluated once when the function is defined. The usual fix is to use None and create a new object inside the function:
<code>>> def report(when=None):
... if when is None:
... when = time.time()
... return when
>>> report()
1500113446.746997
>>> report()
1500113448.552873</code>2. x += y vs x = x + y
For immutable types they are equivalent, but for mutable containers the in‑place += creates a new object while + modifies the existing one, leading to different object identities:
<code>>> x = 1; x += 1; print x
2
>>> x = [1]; x += [2]; print x
[1, 2]
>>> x = [1]; x = x + [2]; print x
[1, 2]</code>Inspecting id(x) shows that += may produce a new list, whereas + rebinds the name to a new object.
3. Tuple Parentheses
Parentheses create a tuple when they contain a comma. A single value inside parentheses is just the value itself; to create a one‑element tuple you need a trailing comma:
<code>>> a = (1, 2)
>>> type(a)
<class 'tuple'>
>>> a = (1)
>>> type(a)
<class 'int'>
>>> a = (1,)
>>> type(a)
<class 'tuple'>
</code>4. Creating a List of Lists
Using multiplication copies references to the same inner list, so modifications affect all rows:
<code>>> a = [[]] * 10
>>> a[0].append(10)
>>> a[0]
[10]
>>> a[1]
[10] # same object
</code>The correct approach is a list comprehension that creates a new list each iteration:
<code>>> a = [[] for _ in range(10)]
>>> a[0].append(10)
>>> a
[[10], [], [], [], [], [], [], [], [], []]
</code>5. Modifying a List While Iterating
Deleting elements while iterating with enumerate can skip items because the list shrinks but the index continues to increase:
<code>def modify_lst(lst):
for idx, elem in enumerate(lst):
if elem % 3 == 0:
del lst[idx]
</code>Using a list comprehension or iterating over a copy avoids this issue.
6. Closures and Lambda Late Binding
Lambda functions capture variables by reference, so all generated functions see the final value of the loop variable:
<code>def create_multipliers():
return [lambda x: i*x for i in range(5)]
for m in create_multipliers():
print(m(2)) # prints 8 five times
</code>Fix by binding the current value as a default argument:
<code>def create_multipliers():
return [lambda x, i=i: i*x for i in range(5)]
</code>7. The del Method and Garbage Collection
Defining a __del__ method can prevent the cyclic‑garbage collector from reclaiming objects involved in reference cycles, potentially causing memory leaks.
8. Importing the Same Module in Different Ways
Importing a module via different paths (e.g., using sys.path.append ) creates distinct module objects with different IDs, leading to unexpected state separation.
9. Python 2 → Python 3 Migration Issues
Key differences include range returning a range object instead of a list, and functions like map , filter , dict.items() returning iterators rather than lists.
10. The ++i / --i Misconception
Python does not support the ++/-- operators; using them in code written for C/C++ simply returns the original value.
11. setattr , getattr , getattribute
These magic methods control attribute access. getattribute and setattr are the low‑level hooks, while getattr is only called when normal lookup fails.
12. The Global Interpreter Lock (GIL)
The GIL is a well‑known limitation of CPython that prevents true parallel execution of Python bytecode in multiple native threads.
Conclusion
Python is beginner‑friendly and highly flexible, but developers must be aware of these common pitfalls to write robust code. The list above is not exhaustive; readers are encouraged to contribute additional examples.
Python Programming Learning Circle
A global community of Chinese Python developers offering technical articles, columns, original video tutorials, and problem sets. Topics include web full‑stack development, web scraping, data analysis, natural language processing, image processing, machine learning, automated testing, DevOps automation, and big data.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.