Python Pitfalls for Beginners and Mid-Level Developers — Part One

Python is celebrated for its readability and ease of learning, but these same features hide subtle pitfalls that frequently trap beginners and mid-level developers. Even simple-looking code can behave unexpectedly due to quirks in Pythons handling of objects, functions, loops, and variable scope. These issues often remain undetected until code scales or is integrated with larger systems. Understanding these Python pitfalls is essential for writing maintainable, predictable, and robust code. This article covers the first set of pitfalls with detailed examples, code analysis, and practical recommendations to prevent common mistakes.

Mutable Default Arguments — Hidden State Across Calls

Mutable default arguments, such as lists or dictionaries, are one of Pythons most infamous traps. Developers often assume that a default argument is initialized anew for each function call, but Python actually evaluates default arguments once at function definition. Any modifications persist across calls, introducing hidden state that can break programs subtly.

Bad Example

Consider a function intended to append items to a list. A mutable default argument can produce unexpected accumulation:


def append_item(item, collection=[]):
    collection.append(item)
    print(f"Adding {item} -> {collection}")
    return collection

print(append_item(1))  # [1]
print(append_item(2))  # [1, 2] — unexpected
print(append_item(3))  # [1, 2, 3]
print(append_item(4))  # [1, 2, 3, 4]
print(append_item(5))  # [1, 2, 3, 4, 5]

Good Example

To avoid this trap, use None as a placeholder and initialize the mutable object inside the function:


def append_item(item, collection=None):
    if collection is None:
        collection = []
    collection.append(item)
    print(f"Adding {item} -> {collection}")
    return collection

print(append_item(1))  # [1]
print(append_item(2))  # [2] — isolated
print(append_item(3))  # [3]
print(append_item(4))  # [4]
print(append_item(5))  # [5]

Analysis and Recommendations

The bad example accumulates items across calls because the default list is shared. The good example isolates each call by creating a new list if no argument is provided. For advanced cases, using immutable types or dataclasses with default_factory ensures predictable behavior. Avoid attempting to reset mutable defaults inside the function without checking for None, as it may overwrite user-provided objects. Always think about state persistence when designing utility functions.

Identity vs Equality — Misusing is and ==

Beginners often confuse identity and value equality. The is operator checks whether two variables reference the same object, while == checks if the values are equal. Python caches small immutable objects, like integers in the range -5 to 256 and short strings, which can make identity comparisons appear correct sometimes but fail in other cases.

Bad Example

Identity checks can produce inconsistent results, misleading developers:


x = 1000
y = 1000
print(x is y)  # False — integers are distinct objects

a = "hello"
b = "hello"
print(a is b)  # True — cached string, misleading

Good Example

Always use == for value comparisons. Reserve is for identity checks, such as verifying None:


x = 1000
y = 1000
print(x == y)  # True — value comparison

a = "hello"
b = "hello"
print(a == b)  # True — value comparison

Analysis and Recommendations

Using is for value comparison introduces subtle bugs. The bad example demonstrates inconsistent behavior caused by object caching. Use == for numeric and string comparisons to avoid hidden errors. Only use is for singletons or when object identity has semantic importance. Understanding the difference improves code predictability and prevents logical mistakes in conditionals and assertions.

Lambda Late Binding — Closures Capture Variables by Reference

Lambdas and inner functions defined inside loops often capture variables by reference. This leads to all closures pointing to the last value of the loop variable. Such behavior can break callbacks, deferred computations, or event-driven code in subtle ways, making debugging difficult.

Bad Example

All lambdas share the same loop variable, producing identical results:


funcs = [lambda: i for i in range(5)]
print([f() for f in funcs])  # [4, 4, 4, 4, 4]

Good Example

Binding the current loop value via a default argument ensures each lambda retains its intended value:


funcs = [lambda i=i: i for i in range(5)]
print([f() for f in funcs])  # [0, 1, 2, 3, 4]

Analysis and Recommendations

In the bad example, all lambdas reference the same variable i after the loop ends. Using default arguments captures the current value, ensuring predictable behavior. Other alternatives include helper functions or functools.partial to bind values explicitly. Always verify closure bindings in asynchronous or event-driven code to prevent hidden bugs.

Summary of Key Practices for Part One

Understanding these pitfalls is critical for beginners and mid-level Python developers. Key takeaways include:

  • Avoid mutable default arguments; initialize internally using None or default_factory.
  • Use == for value comparisons and reserve is for identity checks like None.
  • Be cautious with lambda and closure bindings inside loops; bind values explicitly to avoid late-binding traps.
  • Always test utility functions with edge cases to detect hidden state or reference issues early.

Iterating Over Changing Lists — Skipping and Missing Items

Modifying a list while iterating over it is a subtle Python pitfall that frequently causes skipped elements or inconsistent results. Pythons for loop maintains an internal index, and altering the list size shifts remaining elements, often resulting in skipped items or unexpected behavior. This is particularly common when filtering or removing elements from a list during iteration.

Bad Example

Attempting to remove even numbers directly while iterating leads to skipped elements:


numbers = [1, 2, 3, 4, 5, 6, 7, 8]
for n in numbers:
    if n % 2 == 0:
        numbers.remove(n)
print(numbers)  # [1, 3, 5, 7] — skipped some even numbers

Good Example

Iterating over a shallow copy ensures the original list can be modified safely:


numbers = [1, 2, 3, 4, 5, 6, 7, 8]
for n in numbers[:]:  # shallow copy
    if n % 2 == 0:
        numbers.remove(n)
print(numbers)  # [1, 3, 5, 7] — correct

Analysis and Recommendations

In the bad example, modifying the list while iterating changes indices, causing some elements to be skipped. The good example separates iteration from modification using a shallow copy. Alternative strategies include using **list comprehensions** to filter elements: numbers = [n for n in numbers if n % 2 != 0]. For large datasets where copying is expensive, consider generator expressions or filter to avoid unnecessary memory overhead. Always test with edge cases like empty lists, single-element lists, or consecutive elements meeting the filter condition.

Global Variables and Scope — UnboundLocalError Trap

Global variables are another frequent source of confusion. Python treats variables assigned inside a function as local unless explicitly declared as global. Failing to declare a global variable before assignment triggers an UnboundLocalError. Many beginners assume they can freely read and write global variables inside functions, but Python requires explicit declaration for modification.

Bad Example

Attempting to increment a global counter without declaring it as global:


counter = 0
def increment():
    counter += 1  # UnboundLocalError
increment()

Good Example

Declaring the variable as global allows modification:


counter = 0
def increment():
    global counter
    counter += 1
increment()
print(counter)  # 1 — correct

Analysis and Recommendations

While the good example fixes the immediate error, relying heavily on global variables is discouraged. They introduce hidden state, reduce code modularity, and complicate testing. Alternatives include encapsulating state inside classes or passing variables explicitly as function arguments. Always strive for stateless functions where possible and document any global usage clearly to avoid subtle bugs in larger codebases.

Floating Point Precision — Approximate Comparisons

Python uses binary floating-point numbers that cannot exactly represent many decimal values. Comparing floats with == often fails due to tiny precision errors. Beginners relying on direct equality checks are frequently surprised when calculations do not behave as expected, particularly in financial, scientific, or simulation code.

Bad Example

Direct comparison of a sum of floats fails:


a = 0.1 + 0.2
print(a == 0.3)  # False — unexpected

Good Example

Use math.isclose or specify a tolerance for reliable comparisons:


import math
a = 0.1 + 0.2
print(math.isclose(a, 0.3))  # True

Analysis and Recommendations

The bad example fails because 0.1 and 0.2 cannot be represented exactly in binary floating-point. The good example checks approximate equality within a tolerance. For critical applications, consider the decimal module, which provides arbitrary precision arithmetic. Always document tolerance values when using approximate comparisons to maintain clarity and prevent subtle bugs.

Dictionary Modification During Iteration — RuntimeError Trap

Modifying dictionaries while iterating is another frequent Python trap. Adding or deleting keys during iteration raises a RuntimeError, because the dictionarys internal structure changes. While the syntax may appear valid, Python enforces consistency to prevent subtle bugs.

Bad Example

Deleting keys while iterating over the dictionary:


my_dict = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
for k in my_dict:
    if my_dict[k] % 2 == 0:
        del my_dict[k]  # RuntimeError

Good Example

Iterate over a copy of keys to safely modify the dictionary:


my_dict = {'a': 1, 'b': 2, 'c': 3, 'd': 4}
for k in list(my_dict.keys()):
    if my_dict[k] % 2 == 0:
        del my_dict[k]
print(my_dict)  # {'a': 1, 'c': 3} — correct

Analysis and Recommendations

The bad example crashes because deleting keys changes the dictionary size mid-iteration. Iterating over a copy prevents runtime errors. For complex transformations, consider dictionary comprehensions to construct new dictionaries safely: {k:v for k,v in my_dict.items() if v % 2 != 0}. Avoid relying on iteration order when modifying dictionaries, and test thoroughly to ensure predictable behavior in larger applications.

Summary of Key Practices — Part Two

This second part covers iteration pitfalls, global variables, floating-point precision, and dictionary modifications. Key recommendations include:

  • Never modify lists or dictionaries while iterating; use copies or comprehensions.
  • Declare global variables explicitly or encapsulate state in classes to avoid UnboundLocalError.
  • Use math.isclose or decimal for floating-point comparisons to prevent subtle errors.
  • Test functions and loops with edge cases such as empty collections, single elements, or consecutive items to catch hidden issues early.

By mastering these patterns, Python developers can write code that behaves predictably and scales reliably. Awareness of these traps prevents wasted time debugging and encourages robust, maintainable design practices.

Additional Python Facts — Essential Insights for Beginners and Mid-Level Developers

Even after mastering the main Python pitfalls, there are several subtle behaviors and language features that developers often overlook. Understanding these facts ensures maintainable Python code, predictable Python behavior, and fewer hidden bugs.

Variable Scope in Nested Functions

Variables inside nested functions follow the LEGB rule: Local, Enclosing, Global, Built-in. Beginners often misunderstand how inner functions access outer variables. Assigning to a variable without declaring it in the correct scope can create unexpected results or raise UnboundLocalError. Using nonlocal for enclosing variables allows modifications in nested functions without affecting global state.

Immutability and Object Sharing

Immutable objects, such as strings, tuples, and numbers, are often shared internally by Python to optimize memory. While improves performance, it can confuse developers when identity checks using is behave inconsistently. Recognizing which types are immutable and understanding caching mechanisms prevents subtle identity vs value errors.

Exception Handling Nuances

Pythons exception handling allows multiple except blocks, but catching overly broad exceptions can hide bugs. Beginners sometimes catch Exception or even BaseException, masking real errors. Best practices include catching specific exceptions and using finally for cleanup to ensure maintainable Python code.

Iterable Unpacking and Star Expressions

Python allows flexible unpacking of iterables using star expressions. While convenient, improper unpacking can silently discard data or raise errors when the iterable length does not match expectations. Understanding how to safely unpack elements improves predictable Python behavior in functions and loops.

Context Managers and Resource Management

Using context managers with with ensures proper resource handling. Beginners often forget to close files or network connections explicitly, which can lead to resource leaks. Context managers not only simplify syntax but also guarantee cleanup, critical for maintainable Python code.

Function Argument Unpacking

Python supports both positional (*args) and keyword (**kwargs) argument unpacking. Misusing them can cause unexpected errors, especially when passing arguments through multiple layers of functions. Understanding argument unpacking enhances code flexibility and reduces hidden bugs.

Built-in Functions and Lazy Evaluation

Many Python built-ins, like map, filter, and zip, return iterators that evaluate lazily. Beginners expecting immediate results may encounter unexpected behavior when consuming these objects multiple times. Awareness of lazy evaluation helps write predictable Python behavior and avoid subtle logic errors.

Summary of Additional Facts

These additional insights complement the main Python pitfalls. By understanding variable scoping, immutability, exception handling, iterable unpacking, context managers, argument unpacking, and lazy evaluation, beginners and mid-level developers can write cleaner, more maintainable Python code. Incorporating these practices alongside previously covered pitfalls ensures robust, predictable Python behavior and fewer hidden errors in real-world projects.

Written by: