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:
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
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.
Frequently Asked Questions
What is the most common OOP mistake in Python?
How do I know if I am overusing inheritance?
Why do my objects behave strangely when I use them as dictionary keys?
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.
More in this topic
Create Custom Iterators in Python
Learn how to build your own iterators in Python by implementing the __iter__() and __next__() methods, with practical examples and reusable patterns.
Python Iterator Protocol Explained
Learn how Python's iterator protocol works with the __iter__() and __next__() methods, and understand the contract every iterator must follow.
Python Iterables and Iterators Explained
Learn what iterables and iterators are in Python, how they power for loops under the hood, and why understanding them unlocks cleaner data processing patterns.