Anti Patterns
Overview
Anti-patterns are solutions that appear reasonable but create more problems than they solve. Python’s flexibility and dynamic nature make certain anti-patterns particularly tempting, especially for developers coming from statically-typed languages. Recognizing these patterns helps you avoid common traps.
Data Structure Anti-Patterns
Mutable Default Arguments
Python evaluates default arguments once when defining the function, not each time you call it. Mutable defaults persist across calls, creating shared state.
Why it’s problematic:
- Default list/dict shared across all calls
- Creates hidden coupling between calls
- Violates principle of least surprise
- Difficult to debug
Example:
def add_item(item, items=[]):
items.append(item)
return items
print(add_item(1)) # [1]
print(add_item(2)) # [1, 2] - Unexpected!
print(add_item(3)) # [1, 2, 3] - List persists
def add_item(item, items=None):
if items is None:
items = []
items.append(item)
return items
print(add_item(1)) # [1]
print(add_item(2)) # [2] - Fresh list each time
print(add_item(3)) # [3]
def add_item(item, items=None):
items = items if items is not None else []
items.append(item)
return itemsWhen mutable defaults are okay:
def create_logger(name, _cache={}):
"""Creates or returns cached logger.
Note: _cache persists across calls by design.
"""
if name not in _cache:
_cache[name] = setup_logger(name)
return _cache[name]Using Lists for Frequent Lookups
Lists require O(n) linear search for membership checks. For frequent lookups, this becomes a performance bottleneck.
Why it’s problematic:
- O(n) for membership checks
- Scales poorly with data size
- Wastes CPU on repeated searches
- Set provides O(1) lookups
Example:
ADMIN_USERS = ['alice', 'bob', 'charlie', 'david']
def is_admin(username):
return username in ADMIN_USERS # O(n) search
ADMIN_USERS = {'alice', 'bob', 'charlie', 'david'}
def is_admin(username):
return username in ADMIN_USERS # O(1) lookup
ADMIN_USERS = frozenset(['alice', 'bob', 'charlie', 'david'])Performance comparison:
import timeit
users_list = list(range(1000))
print(timeit.timeit(lambda: 999 in users_list, number=100000)) # ~0.5s
users_set = set(range(1000))
print(timeit.timeit(lambda: 999 in users_set, number=100000)) # ~0.005sNot Using Comprehensions
Writing loops to build lists when comprehensions would be clearer and faster.
Why it’s problematic:
- More verbose than necessary
- Slower than comprehensions
- Less clear intent
- More opportunities for bugs
Example:
squares = []
for x in range(10):
squares.append(x ** 2)
evens = []
for x in range(20):
if x % 2 == 0:
evens.append(x)
squares = [x ** 2 for x in range(10)]
evens = [x for x in range(20) if x % 2 == 0]
words = ['hello', 'world', 'python']
upper_words = [w.upper() for w in words]
word_lengths = {word: len(word) for word in words}
sum_of_squares = sum(x ** 2 for x in range(1_000_000))When loops are better:
results = []
for item in items:
try:
processed = complex_processing(item)
if validate(processed):
results.append(transform(processed))
except ProcessingError:
log_error(item)
continueException Handling Anti-Patterns
Bare Except Clauses
Catching all exceptions without specifying which ones hides bugs and makes debugging impossible.
Why it’s problematic:
- Catches unexpected exceptions (KeyboardInterrupt, SystemExit)
- Makes debugging nearly impossible
- Hides programming errors
- Violates “errors should never pass silently”
Example:
def load_config():
try:
with open('config.json') as f:
return json.load(f)
except: # Catches KeyboardInterrupt, SystemExit, etc.
return {} # Silently fails
def load_config():
try:
with open('config.json') as f:
return json.load(f)
except FileNotFoundError:
return {} # Expected case
except json.JSONDecodeError as e:
logger.error(f"Invalid JSON in config: {e}")
raise # Re-raise to surface the error
def load_config():
try:
with open('config.json') as f:
return json.load(f)
except Exception as e: # Doesn't catch system exceptions
logger.error(f"Failed to load config: {e}")
return {}Swallowing Exceptions
Catching exceptions but doing nothing with them hides problems and makes debugging difficult.
Why it’s problematic:
- Lost error context
- Silent failures corrupt state
- Debugging becomes guesswork
- Violates fail-fast principle
Example:
def fetch_user(user_id):
try:
return database.query(f"SELECT * FROM users WHERE id = {user_id}")
except:
pass # Silent failure - caller gets None
user = fetch_user(123)
print(user.name) # AttributeError: 'NoneType' has no attribute 'name'
def fetch_user(user_id):
return database.query(f"SELECT * FROM users WHERE id = {user_id}")
# Caller handles exceptions or lets them propagate
def fetch_user(user_id):
try:
return database.query(f"SELECT * FROM users WHERE id = {user_id}")
except DatabaseError as e:
logger.error(f"Failed to fetch user {user_id}: {e}")
raise # Re-raise with context logged
from typing import Optional
def fetch_user(user_id) -> Optional[User]:
try:
return database.query(f"SELECT * FROM users WHERE id = {user_id}")
except UserNotFoundError:
return None # Expected case, not an errorUsing Exceptions for Flow Control
Exceptions should represent exceptional conditions, not normal program flow.
Why it’s problematic:
- Exceptions are slow (stack trace creation)
- Unclear what’s error vs normal flow
- Violates principle of least surprise
- Difficult to optimize
Example:
def get_user_age(users, user_id):
try:
return users[user_id].age
except KeyError:
return None # Normal case, not exceptional
try:
age = get_user_age(users, 123)
except KeyError:
age = None
from typing import Optional
def get_user_age(users, user_id) -> Optional[int]:
user = users.get(user_id)
return user.age if user else None
age = get_user_age(users, 123)
if age is not None:
print(f"Age: {age}")
def get_user_age(users, user_id):
try:
return users[user_id].age
except KeyError:
raise ValueError(f"User {user_id} not found") # Exceptional conditionResource Management Anti-Patterns
Not Using Context Managers
Manually managing resources with try/finally when context managers provide automatic cleanup.
Why it’s problematic:
- Easy to forget cleanup
- Verbose and error-prone
- Suppressed exceptions in finally
- Context managers are cleaner and safer
Example:
def process_file(filename):
f = open(filename)
try:
data = f.read()
return process(data)
finally:
f.close() # Easy to forget
def copy_file(src, dest):
f1 = open(src, 'rb')
try:
f2 = open(dest, 'wb')
try:
f2.write(f1.read())
finally:
f2.close()
finally:
f1.close()
def process_file(filename):
with open(filename) as f:
data = f.read()
return process(data)
def copy_file(src, dest):
with open(src, 'rb') as source, open(dest, 'wb') as target:
target.write(source.read())
from contextlib import contextmanager
@contextmanager
def database_transaction(connection):
try:
yield connection
connection.commit()
except Exception:
connection.rollback()
raise
with database_transaction(conn) as db:
db.execute("INSERT INTO users ...")Manually Closing Resources
Forgetting to close files, connections, or other resources leads to leaks.
Why it’s problematic:
- Resource leaks (file handles, connections)
- Unexpected behavior (buffering issues)
- System limits (max open files)
- Memory leaks for long-running processes
Example:
def read_config():
f = open('config.json')
return json.load(f) # File never closed!
for i in range(10000):
config = read_config() # Eventually runs out of file handles
def read_config():
with open('config.json') as f:
return json.load(f)
from pathlib import Path
def read_config():
return json.loads(Path('config.json').read_text())Import Anti-Patterns
Circular Imports
Modules that import each other create initialization problems and tight coupling.
Why it’s problematic:
- Import errors at runtime
- Order-dependent behavior
- Tight coupling between modules
- Indicates poor separation of concerns
Example:
from module_b import function_b
def function_a():
return function_b()
from module_a import function_a # ImportError!
def function_b():
return function_a()
def shared_function():
return "shared"
from shared import shared_function
def function_a():
return shared_function()
from shared import shared_function
def function_b():
return shared_function()
def function_a(dependency):
return dependency()
def function_b():
return "result"
from module_a import function_a
from module_b import function_b
result = function_a(function_b) # Inject dependencyWildcard Imports
Using from module import * pollutes namespace and makes code unclear.
Why it’s problematic:
- Unclear where names come from
- Name collisions
- Makes refactoring difficult
- Breaks static analysis tools
Example:
from math import *
from statistics import *
result = mean([1, 2, 3]) # Which mean? math.mean or statistics.mean?
from statistics import mean, median
from math import sqrt, pi
result = mean([1, 2, 3]) # Clear origin
import math
result = math.sqrt(math.pi) # Clear namespace
import statistics as stats
result = stats.mean([1, 2, 3])String Anti-Patterns
String Concatenation in Loops
Building strings with += in loops creates many intermediate strings.
Why it’s problematic:
- O(n²) time complexity (strings are immutable)
- Creates garbage for each concatenation
- Slow for large strings
- List join is O(n)
Example:
result = ""
for i in range(1000):
result += str(i) + "," # Creates 1000 intermediate strings
parts = []
for i in range(1000):
parts.append(str(i))
result = ",".join(parts)
result = ",".join(str(i) for i in range(1000))
from io import StringIO
output = StringIO()
for i in range(1000):
output.write(str(i))
output.write(",")
result = output.getvalue()Performance comparison:
import timeit
print(timeit.timeit(
'".".join(str(i) for i in range(1000))',
number=1000
)) # ~0.15s
print(timeit.timeit(
'result = ""; [result := result + str(i) for i in range(1000)]',
number=1000
)) # ~0.50sNot Using F-Strings
Using old-style string formatting when f-strings are clearer and faster.
Why it’s problematic:
- Less readable than f-strings
- Slower than f-strings
- More error-prone
- Harder to maintain
Example:
name = "Alice"
age = 30
msg = "Hello %s, you are %d years old" % (name, age)
msg = "Hello {}, you are {} years old".format(name, age)
msg = "Hello " + name + ", you are " + str(age) + " years old"
msg = f"Hello {name}, you are {age} years old"
msg = f"In 5 years: {age + 5}"
price = 19.99
msg = f"Price: ${price:.2f}"
print(f"{name=}, {age=}") # name='Alice', age=30Class Design Anti-Patterns
God Classes
Classes that do too much violate Single Responsibility Principle and become unmaintainable.
Why it’s problematic:
- Difficult to understand
- Difficult to test
- High coupling
- Changes ripple through codebase
Example:
class UserManager:
def create_user(self, data): ...
def validate_email(self, email): ...
def hash_password(self, password): ...
def send_welcome_email(self, user): ...
def save_to_database(self, user): ...
def generate_report(self, users): ...
def export_to_csv(self, users): ...
def import_from_json(self, data): ...
def authenticate(self, email, password): ...
# ... 20 more methods
class UserValidator:
def validate_email(self, email): ...
def validate_password(self, password): ...
class PasswordHasher:
def hash_password(self, password): ...
def verify_password(self, password, hash): ...
class UserRepository:
def save(self, user): ...
def find_by_email(self, email): ...
class EmailService:
def send_welcome_email(self, user): ...
class UserService:
def __init__(self, validator, hasher, repository, email_service):
self.validator = validator
self.hasher = hasher
self.repository = repository
self.email_service = email_service
def create_user(self, data):
self.validator.validate_email(data['email'])
hashed = self.hasher.hash_password(data['password'])
user = User(data['email'], hashed)
self.repository.save(user)
self.email_service.send_welcome_email(user)
return userOverusing Classes
Creating classes when functions or simpler structures would suffice (Java-itis).
Why it’s problematic:
- Unnecessary complexity
- More boilerplate to maintain
- Obscures simple logic
- Python is multi-paradigm, not just OOP
Example:
class StringUtils:
@staticmethod
def reverse(s):
return s[::-1]
@staticmethod
def capitalize_words(s):
return s.title()
result = StringUtils.reverse("hello")
def reverse_string(s):
return s[::-1]
def capitalize_words(s):
return s.title()
result = reverse_string("hello")
class DiscountCalculator:
def __init__(self, rate):
self.rate = rate
def calculate(self, amount):
return amount * (1 - self.rate)
calc = DiscountCalculator(0.1)
result = calc.calculate(100)
def calculate_discount(amount, rate):
return amount * (1 - rate)
result = calculate_discount(100, 0.1)When classes make sense:
- State management across operations
- Encapsulation with data + behavior
- Inheritance or protocols needed
- Context managers or iterators
Performance Anti-Patterns
Premature Optimization
Optimizing code before profiling shows it’s a bottleneck.
Why it’s problematic:
- Makes code complex prematurely
- Optimizes wrong parts
- Wastes development time
- Reduces readability
Example:
def calculate_total(items):
# "Optimized" with caching and pre-allocation
_cache = {}
result = [0] * len(items)
for i, item in enumerate(items):
key = (item['price'], item['qty'])
if key not in _cache:
_cache[key] = item['price'] * item['qty']
result[i] = _cache[key]
return sum(result)
def calculate_total(items):
return sum(item['price'] * item['qty'] for item in items)
#Optimization workflow:
- Write simple, correct code
- Profile to find actual bottlenecks
- Optimize bottlenecks with benchmarks
- Keep readable code in non-critical paths
Not Using Built-in Functions
Reimplementing functionality that exists in standard library.
Why it’s problematic:
- Slower than optimized built-ins
- More code to maintain
- Likely has bugs
- Misses edge cases
Example:
def calculate_sum(numbers):
total = 0
for num in numbers:
total += num
return total
total = sum(numbers)
def find_maximum(numbers):
max_val = numbers[0]
for num in numbers[1:]:
if num > max_val:
max_val = num
return max_val
max_val = max(numbers)
def get_evens(numbers):
result = []
for num in numbers:
if num % 2 == 0:
result.append(num)
return result
evens = [num for num in numbers if num % 2 == 0]
evens = list(filter(lambda x: x % 2 == 0, numbers))Summary
Python’s flexibility makes certain anti-patterns particularly tempting, but recognizing them helps you write more maintainable code. Mutable default arguments create hidden state that persists across function calls - use None as a sentinel instead. Using lists for frequent lookups wastes CPU on linear searches when sets provide constant-time membership checks. Comprehensions aren’t just shorter than equivalent loops - they express intent more clearly and run faster.
Exception handling requires care to avoid masking problems. Bare except clauses catch system exceptions like KeyboardInterrupt that should propagate. Swallowing exceptions silently hides failures that corrupt state and make debugging impossible. Using exceptions for control flow confuses error conditions with normal program logic - return Optional types for expected cases instead.
Context managers automatically clean up resources even when exceptions occur. Manual try/finally blocks require discipline to close files, connections, and locks reliably. Circular imports indicate tight coupling and poor separation of concerns - extract shared code to a third module or use dependency injection. Wildcard imports pollute namespaces and make code impossible to trace - import explicitly or import the module itself.
String concatenation in loops creates O(n²) behavior because strings are immutable. Building strings through intermediate list items and joining them once provides O(n) performance. F-strings make string interpolation readable and fast compared to % formatting or .format() - use them as your default choice.
God classes that handle too many responsibilities become unmaintainable tangles of coupled code. Split classes by responsibility to enable focused testing and independent evolution. Not every operation needs a class - Python supports multiple paradigms, and simple functions often express simple operations more clearly than classes with static methods.
Premature optimization wastes effort on code that isn’t actually slow. Write simple, correct code first, profile to find real bottlenecks, then optimize with benchmarks to validate improvements. The standard library provides optimized implementations of common operations - prefer built-in functions over custom implementations that are slower and more likely to have bugs.
These anti-patterns share a common theme: they trade clarity or correctness for perceived benefits that don’t materialize. Focus on writing clear, correct code that follows Python’s idioms. When optimization matters, let profiling guide you to actual bottlenecks worth improving.