Python
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Data Types
-
Section 3: Collections
-
39: Set Operations: Union, Intersection, Difference
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Turtle Graphics and Early Practice Projects
-
Section 7: Working with Files and I/O
-
Section 8: Regular Expressions
-
Section 9: Object-Oriented Python
-
Section 10: Error Handling
-
Section 11: Modules and Packages
-
Section 12: Iterators, Generators, and Functional Tools
-
Section 13: Decorators and Metaprogramming
-
Section 14: Concurrency and Parallelism
-
Section 15: Working with Dates, Times, and Numbers
-
Section 16: Standard Library Deep Dive I: Data Structures
-
Section 17: Standard Library Deep Dive II: System and Introspection
-
Section 18: Standard Library Deep Dive III: Security and Encoding
-
Section 19: Standard Library Deep Dive IV: Text and Data Utilities
-
Section 20: Networking and Web Basics
-
Section 21: Working with Databases
-
Section 22: Testing and Quality
-
Section 23: Advanced Typing
-
Section 24: Context Managers and Resource Handling
-
Section 25: Text, Unicode, and Binary Data
-
Section 26: More Functional and Iteration Tools
-
Section 27: Data Validation and Configuration
-
Section 28: Working with Images and Media
-
Section 29: Property-Based and Documentation Testing
-
Section 30: Packaging and Deployment
-
Section 31: Performance and Internals
-
Section 32: Design Patterns in Python
-
Section 33: GUI Programming
-
Section 34: Security Basics
-
Section 35: Data Structures and Algorithms
-
Section 36: Practical Projects
-
Section 37: Capstone Projects
-
Section 38: Interview and Algorithm Practice
-
Section 39: Writing Idiomatic Python
465: Avoiding Common Anti-Patterns
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)
There are no comments for now.