Skip to Content
Course content

465: Avoiding Common Anti-Patterns

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I've spent a lot of time reviewing pull requests over the years, and there is one specific pattern that pops up constantly, even from developers who are otherwise quite proficient. It usually looks something like this: a function designed to append an item to a list, where the list is provided as a default argument.

def add_to_task_list(task, tasks=[]):
    tasks.append(task)
    return tasks

# First call works as expected
print(add_to_task_list("Write docs")) # ['Write docs']
# Second call... wait, what?
print(add_to_task_list("Fix bug"))    # ['Write docs', 'Fix bug']

The developer thinks they are saying, "If no list is provided, start with a fresh, empty list." But as you can see above, the second call remembers the first call. The list isn't being reset; it's persisting across calls. It feels like a bug in the language, but it's actually a fundamental part of how Python handles function definitions.

The myth that default arguments are re-evaluated

The misconception here is the belief that the expression [] is executed every time the function is called. In reality, Python evaluates default arguments once, at the moment the function is defined. That list is created once and stored in the function object's __defaults__ attribute. Every time you call the function without providing a list, you are interacting with that same, single list object in memory.

I'll be honest: this trips up almost everyone coming from Java or C#. In those languages, you're used to the "default" being a value template. In Python, it's a persistent object. If that object is mutable (like a list, dictionary, or set), you've accidentally created a hidden piece of global state.

Using the None sentinel for mutable defaults

To fix this, we use what I call the "Sentinel Pattern." Instead of providing the mutable object itself, we use None—which is immutable—and then assign the fresh list inside the function body.

def add_to_task_list(task, tasks=None):
    if tasks is None:
        tasks = []
    tasks.append(task)
    return tasks

print(add_to_task_list("Write docs")) # ['Write docs']
print(add_to_task_list("Fix bug"))    # ['Fix bug']

Now, the list is created inside the local scope of the function call. It's a bit more verbose, but it's the only way to ensure your function doesn't have a "memory" of previous executions.

Manual indexing with range(len())

Another pattern I see frequently is the "C-style" loop. Someone wants to iterate through a list and needs the index of the current item, so they write this:

for i in range(len(my_list)):
    print(f"Item {i} is {my_list[i]}")

It works, sure. But it's clunky. You're forcing Python to look up the length of the list, create a range object, and then manually index back into the list on every single iteration. It's noisy and increases the chance of "off-by-one" errors if you start doing math with i.

Letting enumerate do the heavy lifting

The Pythonic way to handle this is enumerate(). It yields both the index and the item simultaneously. It's cleaner, faster, and much more readable.

for i, item in enumerate(my_list):
    print(f"Item {i} is {item}")

I always tell my juniors: if you find yourself typing range(len(...)), stop and ask if enumerate or zip could do the job instead. Nine times out of ten, the answer is yes.

The "Catch-All" safety net

Finally, let's talk about error handling. I often see code wrapped in a massive try...except block that looks like this:

try:
    process_data(data)
except:
    print("Something went wrong")

This is a dangerous anti-pattern. A "bare except" doesn't just catch the ValueError or TypeError you're expecting; it catches everything. This includes SystemExit and KeyboardInterrupt. If you use a bare except, you might find that hitting Ctrl+C to stop your program suddenly stops working because your code is "handling" the exit signal and just printing "Something went wrong."

Targeted exception handling

Always be explicit about what you are catching. If you truly don't know what might happen but want to avoid crashing the system, at least catch Exception. This excludes the system-level signals that allow you to actually kill the process.

try:
    process_data(data)
except ValueError as e:
    print(f"Invalid data: {e}")
except Exception as e:
    print(f"Unexpected error: {e}")

By naming the exception, you document your assumptions about where the code might fail. It makes the code easier to debug because you aren't masking legitimate crashes that you should be seeing during development.




📋 Practical Task

Refactoring the Legacy Order Processor

You've inherited a piece of code for an order processing system that is riddled with the anti-patterns we just discussed. Your task is to refactor the OrderManager class to be "Pythonic."

The Goal: Fix the mutable default argument bug, replace the manual indexing loop with enumerate, and correct the bare exception handler.

class OrderManager:
    def __init__(self):
        self.orders = []

    # BUG: This list persists across different calls if not provided
    def add_order_batch(self, new_orders, batch_log=[]):
        for order in new_orders:
            self.orders.append(order)
            batch_log.append(order)
        return batch_log

    def print_order_summary(self):
        # ANTI-PATTERN: Manual indexing
        for i in range(len(self.orders)):
            print(f"Order #{i}: {self.orders[i]}")

    def process_payment(self, order_id):
        try:
            # Imagine a complex payment function here that could fail
            raise ConnectionError("Payment gateway down")
        except:
            # ANTI-PATTERN: Bare except
            print("Payment failed.")

# Test your fix here:
manager = OrderManager()
print(manager.add_order_batch(["Order_A", "Order_B"])) # Should be ['Order_A', 'Order_B']
print(manager.add_order_batch(["Order_C"]))           # Should be ['Order_C'], NOT ['Order_A', 'Order_B', 'Order_C']
manager.print_order_summary()
manager.process_payment(1)
Rating
0 0

There are no comments for now.

to be the first to leave a comment.