What are common Python anti-patterns to avoid in production code?

7 minintermediatebest-practicesanti-patternscode-quality

Quick Answer

Common ones: mutable default arguments (shared state leak across calls), bare `except:` clauses (swallow everything including `SystemExit`/`KeyboardInterrupt`), using `pickle`/`eval` on untrusted data, wildcard imports (`from module import *`, polluting the namespace and hiding where names come from), catching exceptions just to `pass` silently, and using a mutable class attribute where an instance attribute was intended.

Detailed Answer

1. Mutable default arguments

# BAD -- shared across every call that omits the argument
def add_item(item, bucket=[]):
    bucket.append(item)
    return bucket

# GOOD
def add_item(item, bucket=None):
    if bucket is None:
        bucket = []
    bucket.append(item)
    return bucket

Covered in depth in the Fundamentals topic — worth repeating here as one of the most common real-world bugs traced back to a single anti-pattern.

2. Bare except: (or overly broad exception handling)

# BAD
try:
    risky_operation()
except:
    pass    # swallows EVERYTHING, including typos (NameError) and Ctrl+C

Silently swallowing all exceptions hides real bugs and makes production issues nearly impossible to diagnose — always catch specific exceptions, and if you must log-and-continue at a boundary, log the actual exception (logger.exception(...)), don't discard it.

3. Mutable class attributes intended as instance attributes

# BAD -- shared across every instance!
class ShoppingCart:
    items = []
    def add(self, item):
        self.items.append(item)

cart1 = ShoppingCart()
cart2 = ShoppingCart()
cart1.add("apple")
cart2.items   # ['apple'] -- BUG: cart2 sees cart1's item!

# GOOD -- instance attribute, set per-object in __init__
class ShoppingCart:
    def __init__(self):
        self.items = []

items = [] at class scope creates one list shared by every instance; each instance needs its own list created in __init__.

4. Wildcard imports

# BAD -- pollutes the namespace, unclear where names come from
from mymodule import *

value = some_function()   # which module defined this? impossible to tell by reading

# GOOD -- explicit imports, or a namespaced import
from mymodule import some_function
import mymodule
mymodule.some_function()

import * makes it impossible to tell, just by reading the code, which module a given name came from — it also risks silently shadowing existing names, and static analysis tools/IDEs can't reliably follow it.

5. Catching an exception just to silence it

# BAD -- hides real failures, produces confusing downstream behavior
try:
    result = fetch_data()
except Exception:
    result = None   # caller now has no idea WHY this is None

# GOOD -- handle it meaningfully, or let it propagate
try:
    result = fetch_data()
except ConnectionError:
    logger.warning("fetch_data failed, using cached value")
    result = get_cached_value()

Swallowing an exception into a generic fallback value without logging or distinguishing why it failed turns a diagnosable failure into a mysterious downstream symptom.

6. Using type() instead of isinstance() for type checks

# BAD -- breaks for subclasses
if type(obj) == list:
    ...

# GOOD -- respects inheritance/polymorphism
if isinstance(obj, list):
    ...

type(obj) == list fails for any subclass of list, defeating polymorphism; isinstance (which also accepts a tuple of types) is almost always what's actually intended.

7. String concatenation in a loop instead of str.join

# BAD -- O(n^2): each += creates a new string, copying everything so far
result = ""
for item in items:
    result += str(item)

# GOOD -- O(n): join builds the final string in one pass
result = "".join(str(item) for item in items)

Since strings are immutable, repeated += in a loop recreates the entire string on every iteration — quadratic behavior that str.join avoids entirely.

Interview-ready summary: Most Python anti-patterns share a common thread — a subtle mismatch between what the code visually appears to do and what actually happens under the hood (mutable defaults/class attributes shared unexpectedly, bare except: hiding real failures, type() breaking polymorphism). Recognizing and avoiding this small, well-known set of patterns eliminates a large share of real-world Python bugs.