Common OOP anti-patterns and code smells — a field reference

Every anti-pattern in this lesson is the specific, real shape a violation of a principle covered earlier in this domain actually takes in production code — this is the field-reference version, so it's recognizable on sight instead of requiring the underlying principle to be re-derived every time.

Advanced

4 min read

Anti-pattern 1: the God Object — one class that knows and does everything

class Application:
    def parse_config(self): ...
    def connect_to_database(self): ...
    def validate_user_input(self): ...
    def render_html(self): ...
    def send_email(self): ...
    def calculate_taxes(self): ...
    # ...forty more methods, spanning every unrelated concern the app has

Covered mechanically in the SOLID lesson's Single Responsibility Principle: a class should have one reason to change, and Application here has dozens — a change to email formatting, tax rules, or HTML rendering all require editing the same class, and any one of them risks breaking the others through shared state. The fix: split by actual responsibility — ConfigParser, Database, Validator, Renderer — each with one real reason to change, composed together rather than merged into one object.

Anti-pattern 2: the Anemic Domain Model — objects that are just data, with logic living elsewhere

class Order:
    def __init__(self):
        self.items = []
        self.status = "pending"
        self.total = 0
    # no methods at all — just a bag of fields
 
def calculate_total(order): ...       # logic lives OUTSIDE the object it operates on
def can_be_cancelled(order): ...       # scattered across the codebase, disconnected from Order itself

An object that's pure data with no behavior of its own pushes every operation on that data out into standalone functions scattered wherever they happened to be needed — the actual rules about what an Order can do live nowhere near the Order class itself, easy to duplicate or apply inconsistently across different call sites. The fix: give the object real methods (order.calculate_total(), order.can_be_cancelled()) — encapsulation, one of the four pillars from this domain's own foundational lesson, means keeping data and the behavior that operates on it together.

Anti-pattern 3: inheriting purely for code reuse, with no real "is-a" relationship

class Stack(list):  # "a Stack IS-A list"? Not really — this exposes list's ENTIRE interface, including
    def push(self, item):  # insert(0, x), sort(), and everything else a Stack was never meant to support
        self.append(item)

Covered mechanically in the composition-over-inheritance lesson: Stack inheriting from list isn't modeling a genuine "is-a" relationship — a stack conceptually only supports push/pop, but this class exposes every one of list's methods too, including ones (sort(), insert(0, x)) that violate what a stack is actually supposed to guarantee. The fix: composition — Stack has-a list internally (self._items = []), exposing only the specific operations a stack should actually have.

Anti-pattern 4: a subclass that breaks its parent's contract (violating Liskov Substitution)

class Rectangle:
    def set_width(self, w): self.width = w
    def set_height(self, h): self.height = h
 
class Square(Rectangle):  # "a Square IS-A Rectangle" — geometrically true, but...
    def set_width(self, w):
        self.width = self.height = w  # setting width SILENTLY changes height too — surprising!
    def set_height(self, h):
        self.width = self.height = h

Square is geometrically a rectangle, but this implementation breaks any code written against Rectangle's contract — a caller that does rect.set_width(5); rect.set_height(10) expecting a 5×10 rectangle gets a 10×10 square instead when rect is actually a Square, a real, silent behavioral surprise. The fix: this is exactly what the Liskov Substitution Principle (from the SOLID lesson) warns against — a subclass must be safely substitutable for its parent without breaking callers' reasonable expectations; when it can't be (as here), the inheritance relationship itself is the wrong model, not just an implementation detail to patch around.

Anti-pattern 5: a Singleton used as a disguised global variable

class ConfigSingleton:
    _instance = None
    def __new__(cls):
        if cls._instance is None:
            cls._instance = super().__new__(cls)
        return cls._instance
 
# Used EVERYWHERE, directly, as implicit shared mutable state —
# any code, anywhere, can silently change it, and tests can't isolate
# themselves from whatever state a previous test left behind

Covered mechanically in the Singleton lesson's own "why it's controversial" discussion: a Singleton used as a convenient way to reach shared state from anywhere is functionally a global variable with extra ceremony — any code anywhere can mutate it, tests become order-dependent because state leaks between them, and dependencies on it are invisible in a class's constructor signature, unlike a dependency that's explicitly passed in. The fix: pass shared state explicitly as a constructor argument (dependency injection) wherever practical, reserving Singleton for the narrow, genuine cases where exactly one instance is a real structural requirement, not just a reachability convenience.

The actual throughline across all five

Every one of these traces back to a principle this domain already covered directly: single responsibility, encapsulation, composition over inheritance, Liskov substitution, and the real cost of implicit shared state. Recognizing an anti-pattern's shape on sight — "this smells like a God Object," "this smells like an anemic model" — is what separates catching a design problem early from discovering it the hard way once the codebase has grown around it.

Further reading

Check your understanding

A quick comprehension check — not tracked, not graded, just for you.

1. Why is a God Object a real problem, beyond just being a large class?

2. What's the actual problem with an Anemic Domain Model (an object that's just data, with all logic living in separate standalone functions)?

3. Why does the Square-extends-Rectangle example violate the Liskov Substitution Principle?