AlgoMaster Logo

Anti-Patterns

Low Priority25 min readUpdated September 21, 2026
Listen to this chapter
Unlock Audio

An anti-pattern is a habit that looks reasonable, runs fine on small inputs, and breaks things later: a hidden bug, a security hole, an O(n^2) cliff, or code nobody else can read. Most Python anti-patterns come from one of two places: a feature whose behavior is more subtle than it looks (mutable defaults, is versus ==, bare except) and a habit imported from another language that fights Python's grain (manual indexing, type checks, god classes). This lesson walks through the common ones, shows what goes wrong, and gives the idiomatic fix for each.

Mutable Default Arguments

The default value for a function parameter is evaluated once, when the function is defined, not each time the function is called. For an immutable default like 0 or "unknown", this is fine. For a mutable default like [] or {}, every call that doesn't pass an explicit value shares the same list.

Consider this code:

Three different calls were supposed to produce three different carts. Instead, they all share the same list, because the [] default was created once at definition time and reused on every call.

Fix: use None as a sentinel and create a fresh list inside the function.

The pattern is the same for dict, set, and any other mutable default. The rule: a default argument should be immutable, or it should be None and the function creates the value on entry.

The bug doesn't show up in single-call tests. It only appears when the same function is called more than once. Linters (ruff, pylint) catch this; turn the rule on.

Using is to Compare Values

is checks identity (whether two names point to the same object). == checks equality (whether two objects have the same value). For most types they look like the same thing on small examples, because Python caches small integers and interned strings, but once the values get bigger, is starts returning False on equal values.

Both x and y hold the value 1000, but they're separate int objects in memory. x == y compares the values, returns True. x is y checks whether they're literally the same object, which they aren't.

The same issue hits strings beyond a certain length, lists, tuples, and any object that isn't a singleton.

Use `is` only for: None, True, False, and other genuine singletons (like sentinel objects you define yourself). Use == for everything else.

The string version sometimes works because CPython interns short identifier-like strings, but once the string comes from input, a file, or concatenation, the identity check fails. The rule is mechanical: is for singletons, == for value equality.

Catching Bare except:

A bare except: catches every exception that inherits from BaseException, including KeyboardInterrupt (Ctrl-C) and SystemExit (which sys.exit() raises). That makes it nearly impossible to stop the program cleanly, and it hides real bugs by discarding them.

The pass here turns every error into silence: a KeyError from a missing field, a ZeroDivisionError from a bad calculation, a KeyboardInterrupt from the user trying to quit. The program limps on, the bug stays, and nobody notices until something downstream breaks.

Fix 1: catch what you expect.

Fix 2: if you need a catch-all (for example, in a top-level supervisor that logs unexpected errors and keeps the service up), use except Exception:. That excludes KeyboardInterrupt and SystemExit, which is usually the desired behavior.

Fix 3: if you do want to catch absolutely everything (rare, only justified in a final crash-handler), use except BaseException: explicitly so the reader can see the intent.

The hierarchy that makes this work:

KeyboardInterrupt and SystemExit sit beside Exception, not under it. This is why except Exception: lets Ctrl-C through.

Modifying a List During Iteration

A for loop over a list keeps an internal index. When you remove items inside the loop, the index keeps advancing while the list gets shorter, so some items get skipped.

Consider this code:

14.99 was removed correctly, but 9.99 and 4.99 should have been removed too. After removing 14.99, the list shifts down, but the loop's index moves to the next slot, skipping the new occupant.

Fix: build a new list instead of mutating during iteration.

If other code holds a reference to the original list and needs to see the change, use slice assignment to mutate in place while still iterating over a copy:

The slice on the left rebinds the contents of the existing list object, so any other reference to prices sees the new contents too.

The same issue appears with dicts. for key in some_dict: iterates over a live view; deleting keys inside the loop raises RuntimeError: dictionary changed size during iteration. Iterate over list(some_dict) if you need to delete entries.

eval and exec on Untrusted Input

eval runs a string as a Python expression. exec runs a string as a Python statement. On untrusted input, they're a remote code execution hole; on trusted input, they're almost always overkill.

A user typing __import__('os').system('rm -rf /') runs that immediately. There's no sandboxing in eval. The rule for general code is: don't eval user input. Most legitimate uses of eval have a safer alternative.

Fix: parse the input with a real parser.

ast.literal_eval evaluates only Python literals (numbers, strings, tuples, lists, dicts, booleans, None), so a malicious payload raises ValueError instead of running. For actual math expressions from users, build a small evaluator with a whitelist of allowed operations, or use a library like simpleeval.

Global Mutable State

Global variables that change over time are hard to test, hard to reason about, and a constant source of "why did that change?" debugging sessions. Two functions that look pure (they only read their parameters) behave differently because some other code modified the global between calls.

The bug doesn't show until two pieces of code disagree about whether the rate has been updated yet. Tests pass in isolation and fail in combination. Threads see partial updates.

Fix: pass the state explicitly, or wrap it in a class.

The first form is a pure function: the same inputs always produce the same output. The second form scopes the state inside an object, so two TaxCalculator instances can coexist without stepping on each other.

Module-level constants (UPPER_SNAKE_CASE, set once at import) aren't this anti-pattern. The pattern is mutable state that any code can change at any time.

God Classes

A god class tries to do everything: parse input, run business logic, format output, save to a database, send email, and update metrics, all in one type with two thousand lines. They're hard to test, hard to change without breaking unrelated things, and hard to read in one sitting.

Any change to email logic risks the inventory code, the audit log, or the response format. Tests have to mock half the world to exercise a single behavior.

Fix: split the responsibilities. Each class or module does one thing.

Each piece can be tested without the others. Replacing the payment provider is one class. Adding a new audit destination is one class. The orchestration function reads top-to-bottom in business order.

The rough rule of thumb: a class has one responsibility, and you can describe what it does in one sentence without using "and." If you find yourself saying "this class manages orders and sends emails and writes to the audit log," that's three classes wearing one name.

Premature Optimization

The opposite of "make it work, then make it fast" is "write it complex from the start because you guessed it would need to be fast." Both code paths fail: the first wastes time on the hot path, the second wastes time on the wrong code.

This is reinventing dict in Python. The author thought a custom hash table would be faster than the built-in. It isn't. dict is implemented in C, optimized for the common cases, and it grows automatically. The "optimization" is now slower, has more bugs, and is harder to read.

Fix: use dict. Measure if performance becomes an issue.

The same anti-pattern shows up in micro-tricks like avoiding function calls "for speed," caching things that don't need caching, and writing C extensions for code that's never been profiled. The rule: write clear code first, profile if it's too slow, optimize the spot the profiler points at.

Silent Exception Handling

Catching an exception and doing nothing with it is worse than not catching it at all. The error happened, the code continues as if it didn't, and the bug is invisible until something downstream produces a wrong result.

If save_order raises, the order is lost. Nothing logs it. Nothing alerts. The system reports success.

Fix: at minimum, log the exception. Ideally, decide what should happen on failure.

log.exception includes the full traceback, which is what you want for debugging. If the caller should handle the failure, re-raise. If the failure is recoverable in place, do the recovery and log what happened. The pattern that's never right is except: pass with no other behavior.

A subtler version of the same anti-pattern is catching Exception at a high level and reporting "something went wrong" without any context. The customer sees a generic error, the on-call engineer has nothing to investigate, and the bug remains.

Overusing isinstance Branches

When code does different things for different types using a chain of isinstance checks, it's reinventing polymorphism the hard way. The dispatch is inverted: the calling code knows about every type, instead of each type knowing about its own behavior.

Adding a new notification type means editing render_notification. The behavior is split across two places: the type and the function that dispatches on it. The list grows, the chain gets longer, and the function becomes the bottleneck for every change.

Fix: put the behavior on the type.

Adding a new type means writing one new class with a render method. Nothing else changes. This is plain polymorphism: each type owns the behavior that varies, and the calling code doesn't need to know which type it has.

The exception is when the types are out of your control (built-in types, types from a third-party library) and you can't add methods to them. Then isinstance dispatch is the price you pay, but it should still be one function in one place, not a check scattered across the codebase.

Reinventing the Standard Library

Python's standard library is large and well-tested. A surprising fraction of the "utility" code in any codebase is a hand-rolled, slower version of something that already ships with Python.

A short list of things that get rewritten by hand and shouldn't be:

Hand-rolledStandard library equivalent
Manual frequency countercollections.Counter
Sorted list with frequent insertsbisect.insort
Manual if key in d for dict-of-listscollections.defaultdict(list)
Hand-rolled date parsingdatetime.strptime or datetime.fromisoformat
Manual URL buildingurllib.parse.urljoin, urlencode
Manual path concatenationpathlib.Path
Manual JSON parsingjson module
Manual config file parsingconfigparser, tomllib
Manual command-line argument parsingargparse
Custom retry loops with sleepstenacity (third-party) or hand-rolled with time.sleep and a clear policy

A quick example. The non-Pythonic counter:

The standard-library version:

Same result, one line, faster, harder to get wrong. Before writing a utility, search the standard library. Often, it's there.

Wildcard Imports

from module import * pulls every public name from the module into the current namespace. The downsides are real:

  • The reader has no way to tell which name came from which module.
  • Two star imports can overwrite each other without warning.
  • Linters and IDEs lose track of what's defined.
  • Importing a module's internals breaks every consumer when the module reorganizes.

If both modules define handle_order, the second import wins and the first becomes unreachable. If a future version of email_service adds a process function, every other module's process gets shadowed without a warning.

Fix: import what you need by name.

Or import the module and use qualified names:

The only place star imports are tolerable is inside an __init__.py that re-exports a curated __all__ list, and even there an explicit list is clearer. In application code, they're noise.

Long Parameter Lists

A function with eight positional parameters is a function that's hard to call correctly. The caller has to remember the order, and a small mistake (swap email and phone, swap width and height) compiles fine but produces incorrect output.

Two strings in a row mean nothing at the call site. The next reader can't tell "221B Baker St" from "USD" without scrolling up.

Fix 1: require keyword arguments past a certain point. The * marker forces every argument after it to be passed by name.

Fix 2: group related parameters into a dataclass or TypedDict.

The dataclass adds documentation, default values, and a typed shape that IDEs can complete and type checkers can verify. It also makes the function signature stable: adding a new field is one line in the dataclass, not a breaking change to every caller of create_order.

The rough guidance: three or four positional parameters is fine, five gets awkward, six is a signal to either force keyword arguments or group them. Keyword-only arguments are nearly free to add and they make the call site self-documenting.

A Decision Map

The anti-patterns above fall into three categories. Knowing which category a code smell belongs to helps pick the fix.

The categories overlap. A god class is hard to read, often slow, and often buggy. But the rough triage above is enough to pick a starting point: if the symptom is "wrong output," start with the behavior anti-patterns. If it's "too slow," start with the performance ones. If it's "I can't follow this code," start with the structure ones.

Quiz

Anti-Patterns Quiz

10 quizzes