Common OOP Mistakes in Python

Learn the most common object-oriented programming mistakes in Python, from mutable default arguments to deep inheritance chains, and how to avoid them.

7 min read

Python OOP mistakes are patterns that appear repeatedly in code written by developers at every experience level, and learning to recognize them is one of the fastest ways to improve the quality of your object-oriented code. These mistakes are not syntax errors that Python will catch for you. They are design errors that produce code that runs without exceptions but behaves incorrectly in subtle ways, or code that works today but becomes impossible to change tomorrow. The mutable default argument pitfall, for example, produces no error messages and no warnings. It simply causes data to appear where it should not, and the bug may survive weeks of testing before manifesting in production. Understanding why these mistakes happen and how the standard fixes work is essential for writing classes that behave predictably.

The articles throughout this section have covered the correct patterns for writing Python classes. This article takes the opposite approach: it catalogs the most common mistakes and explains both why they are wrong and how to fix them. Each mistake is paired with its correction, and understanding the correction reinforces the correct pattern. If you have been following this section in order, you have already learned most of the correct patterns. This article consolidates them by showing what happens when they are not followed.

The mistakes covered here are drawn from real code reviews, Stack Overflow questions, and the collective experience of the Python community. They are the mistakes that experienced developers learn to spot at a glance because they have debugged them enough times to recognize the symptoms immediately.

Mutable default arguments

The most famous Python pitfall is also one of the most common in object-oriented code. Default parameter values in function and method definitions are evaluated once, at the time the function is defined, not each time the function is called. When the default value is a mutable object like a list or dictionary, every call that uses the default shares the same object. In a constructor, this means every instance that does not explicitly provide the argument shares the same list or dictionary, and modifications to that list through one instance are visible through all others.

Here is the broken pattern and the correct fix side by side. The broken version uses an empty list as a default, and the fixed version uses None and creates the list inside the method body:

pythonpython
class Broken:
    def __init__(self, items=[]):
        self.items = items
 
class Fixed:
    def __init__(self, items=None):
        self.items = items if items is not None else []

Two instances of Broken created without explicit items will share the same list object. Two instances of Fixed will each receive their own independent empty list. The fix is one extra line in the constructor, and it prevents a category of bugs that can be extraordinarily difficult to trace because the data corruption happens silently and at a distance. This pattern applies to all mutable defaults, including dictionaries, sets, and instances of custom classes.

Deep inheritance chains

Inheritance is a powerful tool, but it is also the most overused feature in object-oriented programming. A typical sign of trouble is a class hierarchy that is four or five levels deep, where understanding the behavior of a class at the bottom requires reading and understanding every class above it. Each level adds complexity, and the interactions between levels, particularly when methods are overridden at multiple levels and each override calls super(), can become impossible to reason about.

The fix for deep inheritance is usually composition. Instead of a class inheriting from a grandparent and parent to acquire behavior, it holds instances of collaborator classes and delegates work to them. The article on composition versus inheritance covers this tradeoff in detail. The guideline is simple: if your inheritance hierarchy is deeper than three levels, stop and consider whether composition would produce a clearer design. Most well-designed Python class hierarchies are two levels deep: an abstract base or a general parent, and concrete children that implement the specifics.

A related mistake is inheriting from a class only to reuse a few utility methods. If a class inherits from a parent and overrides most of the parent's methods, the inheritance relationship is probably not genuine. The child is not truly a specialized version of the parent; it is a different class that happens to share some implementation details. In these cases, extracting the shared logic into standalone functions or a separate utility class used through composition is almost always the better design.

Broken equality and hashing

Overriding the equality method on a class is common and useful, but it comes with a responsibility: if you override equality, you must also consider hashing. Python enforces this by automatically setting the hash to None when equality is overridden, making instances of the class unhashable. The objects cannot be stored in sets or used as dictionary keys. The fix is to implement a hash method that is consistent with equality: equal objects must have equal hashes.

A more subtle mistake is basing equality and hashing on mutable attributes. If a class defines equality based on a name attribute, and the name can change after the object is created, the object's hash changes when the name changes. If the object is stored in a set or used as a dictionary key before the name change, it becomes unfindable afterward because the dictionary looks for it under the old hash. The fix is to base equality and hashing on immutable attributes, or to treat objects with mutable identity as unhashable by design.

The article on object comparison in Python covers the equality and hashing relationship in detail, including the total_ordering decorator and the rules for writing comparison methods that interoperate correctly with other types.

Class variable shadowing

Assigning to a class variable through an instance does not modify the class variable. It creates an instance variable with the same name that shadows the class variable for that specific instance. The class variable remains unchanged, and other instances continue to see it. This behavior is by design, not a bug in Python, but it surprises enough developers that it deserves explicit mention.

The fix is to always access class variables through the class name when you intend to modify them. If the goal is to override a class variable for a specific instance, the instance assignment is correct, but the developer should understand that they are creating a shadow, not modifying shared state. The article on class variables versus instance variables covers this distinction in depth with examples of both correct and incorrect usage.

Writing classes that should be functions

Not every abstraction needs to be a class. A common mistake among developers learning OOP is to wrap every piece of logic in a class, even when a standalone function would be simpler, clearer, and more reusable. A function that accepts input, performs a calculation, and returns a result does not benefit from being wrapped in a class unless the calculation involves state that persists between calls or configuration that is expensive to set up repeatedly.

The test for whether something should be a class is simple: does it have state that persists across multiple method calls? If the answer is no, a function is probably the right choice. If the answer is yes, a class is appropriate. Python is a multi-paradigm language, and not every problem is best solved with object-oriented techniques. The best Python code mixes functions, classes, and modules freely, using each where it fits best.

Rune AI

Rune AI

Key Insights

  • Mutable default arguments are evaluated once at definition time; use None and create the mutable object inside the method.
  • Deep inheritance chains create tight coupling; prefer composition and keep hierarchies shallow.
  • Overriding equality without implementing hash makes objects unhashable; both must be consistent.
  • Assigning to a class variable through an instance creates a shadowing instance variable, not modifying the shared value.
  • Not every collection of functions needs to be a class; standalone functions are often the better choice.
RunePowered by Rune AI

Frequently Asked Questions

What is the most common OOP mistake in Python?

The most common mistake is using a mutable default argument like a list or dictionary in a method or constructor definition. Because default values are evaluated once at definition time, not each time the function is called, every call that uses the default shares the same mutable object. The fix is to use None as the default and create the mutable object inside the method body. This pattern is described in detail in the article on Python constructors.

How do I know if I am overusing inheritance?

Signs of overusing inheritance include class hierarchies deeper than three levels, child classes that override most of the parent's methods, parent classes that exist only to share a few utility methods, and classes that inherit capabilities they do not need. If you find yourself using inheritance primarily to avoid typing, consider whether composition or a standalone function would serve the same purpose with less coupling.

Why do my objects behave strangely when I use them as dictionary keys?

If you override the equality method without also implementing a hash method, Python sets the hash to None, making your objects unhashable. They cannot be used as dictionary keys or stored in sets. Even if you do implement both, the hash must be consistent with equality: equal objects must have equal hashes. A common mistake is to base equality on mutable attributes, which means the hash changes when the attribute changes, causing the object to be lost in the dictionary.

Conclusion

Every Python developer makes OOP mistakes, and the difference between a beginner and an experienced developer is not avoiding all mistakes but recognizing them quickly and knowing the standard fixes. The mutable default pitfall, deep inheritance chains, broken equality and hashing, and class variable shadowing are the most common traps, and understanding why they happen is the first step toward avoiding them in your own code.