Common OOP Mistakes
OOP introduces new ways to think about code. It also introduces new ways to get it wrong. These are the most common mistakes — and what to do instead.
1. Forgetting self
class Animal:
def __init__(self, name):
name = name # wrong — stored as local variable, not on the object
def describe(self):
print(self.name) # AttributeError — self.name doesn't exist
Every attribute must be stored with self.. Without it, the value is a local variable that disappears when __init__ finishes.
class Animal:
def __init__(self, name):
self.name = name # correct
2. Not calling super().__init__()
class Dog(Animal):
def __init__(self, name, breed):
self.breed = breed # Animal.__init__ never runs
# self.name, self.age don't exist
dog = Dog("Rex", "Husky")
print(dog.name) # AttributeError
class Dog(Animal):
def __init__(self, name, breed):
super().__init__(name) # runs Animal.__init__ first
self.breed = breed # then adds Dog's own attribute
3. Using mutable default arguments in __init__
class Shelter:
def __init__(self, animals=[]): # wrong — shared across all instances
self.animals = animals
s1 = Shelter()
s2 = Shelter()
s1.animals.append("Lassie")
print(s2.animals) # ['Lassie'] — s2 is affected too
Mutable defaults — lists, dicts — are created once and shared by all instances that don't pass their own value. Always use None as default and create the object inside __init__:
class Shelter:
def __init__(self, animals=None):
self.animals = animals if animals is not None else []
4. Modifying class attributes through an instance
class Animal:
count = 0 # class attribute — shared
a = Animal()
a.count = 5 # creates an instance attribute — doesn't change the class attribute
print(Animal.count) # 0 — unchanged
print(a.count) # 5 — instance attribute shadows the class attribute
If you want to modify a class attribute, do it through the class — not an instance:
Animal.count = 5 # correct — modifies the class attribute
print(Animal.count) # 5
5. Putting too much in one class
class Animal:
def __init__(self, name):
self.name = name
def describe(self): ...
def save_to_file(self): ...
def load_from_file(self): ...
def send_email_report(self): ...
def generate_pdf(self): ...
def connect_to_database(self): ...
A class that does everything is a class that does nothing well. Each class should have one clear responsibility. File handling, email, PDF generation — those belong in separate classes or modules. Animal should know about animals, not about email servers.
6. Accessing private attributes directly
class Animal:
def __init__(self, name, health):
self.name = name
self._health = health # private by convention
def set_health(self, health):
if health in ["healthy", "ill"]:
self._health = health
lassie = Animal("Lassie", "healthy")
lassie._health = "missing" # bypasses validation — wrong
The underscore is a signal — not a lock. Respect it. Use the setter.
7. Confusing the class and the instance
class Animal:
def describe(self):
print("I am an animal.")
Animal.describe() # TypeError — no instance, self has no value
Animal().describe() # correct — creates an instance, then calls the method
Methods live on instances, not on the class directly. You need an object to call a method on.
8. Not defining __str__
lassie = Animal("Lassie", "dog", 4)
print(lassie) # <__main__.Animal object at 0x10b3c2d50>
Always define __str__. It costs one method. It pays off every time you print, log, or debug.
What you should understand now
- Always use
self.attr— not justattr— in__init__ - Always call
super().__init__()in child classes - Never use mutable objects as default arguments — use
None - Modify class attributes through the class, not an instance
- One class, one responsibility
- Respect the underscore convention — use setters
- Always define
__str__