# Common OOP Mistakes
# what goes wrong and what to do instead
# redhorndev.com

# ─────────────────────────────────────────────
# 1. Forgetting self
# ─────────────────────────────────────────────

class Animal:
    def __init__(self, name):
        name = name             # wrong — local variable, disappears after __init__

# correct:
class Animal:
    def __init__(self, name):
        self.name = name        # stored on the object

# ─────────────────────────────────────────────
# 2. Not calling super().__init__()
# ─────────────────────────────────────────────

class Animal:
    def __init__(self, name, age):
        self.name = name
        self.age  = age

class Dog(Animal):
    def __init__(self, name, age, breed):
        # super().__init__(name, age) — missing
        self.breed = breed

# dog = Dog("Rex", 4, "Husky")
# print(dog.name)               # AttributeError — name was never set

# correct:
class Dog(Animal):
    def __init__(self, name, age, breed):
        super().__init__(name, age)     # runs Animal.__init__ first
        self.breed = breed

dog = Dog("Rex", 4, "Husky")
print(dog.name)                 # Rex

# ─────────────────────────────────────────────
# 3. Mutable default arguments
# ─────────────────────────────────────────────

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 affected too

# correct:
class Shelter:
    def __init__(self, animals=None):
        self.animals = animals if animals is not None else []

s1 = Shelter()
s2 = Shelter()
s1.animals.append("Lassie")
print(s2.animals)               # [] — independent

# ─────────────────────────────────────────────
# 4. Modifying class attributes through an instance
# ─────────────────────────────────────────────

class Animal:
    count = 0                   # class attribute

a = Animal()
a.count = 5                     # creates instance attribute — doesn't change class attribute
print(Animal.count)             # 0 — unchanged
print(a.count)                  # 5 — instance attribute shadows class attribute

# correct — modify through the class:
Animal.count = 5
print(Animal.count)             # 5

# ─────────────────────────────────────────────
# 5. Too much in one class
# ─────────────────────────────────────────────

# wrong:
class Animal:
    def describe(self): ...
    def save_to_file(self): ...
    def send_email(self): ...
    def generate_pdf(self): ...
    def connect_to_db(self): ...

# correct — one responsibility per class:
class Animal:
    def describe(self): ...

class AnimalFileHandler:
    def save(self, animal): ...
    def load(self): ...

# ─────────────────────────────────────────────
# 6. Bypassing private attributes
# ─────────────────────────────────────────────

class Animal:
    def __init__(self, name, health):
        self.name    = name
        self._health = health

    def set_health(self, health):
        if health in ["healthy", "ill"]:
            self._health = health
        else:
            print("Invalid health status.")

lassie = Animal("Lassie", "healthy")
lassie._health = "missing"          # bypasses validation — wrong

lassie.set_health("ill")            # correct — validation runs
lassie.set_health("missing")        # Invalid health status.

# ─────────────────────────────────────────────
# 7. Calling methods on the class, not an instance
# ─────────────────────────────────────────────

class Animal:
    def __init__(self, name):
        self.name = name

    def describe(self):
        print(f"I am {self.name}.")

# Animal.describe()             # TypeError — no self value
Animal("Lassie").describe()     # correct — instance created, method called

# ─────────────────────────────────────────────
# 8. Not defining __str__
# ─────────────────────────────────────────────

class Animal:
    def __init__(self, name, species):
        self.name    = name
        self.species = species

lassie = Animal("Lassie", "dog")
print(lassie)       # <__main__.Animal object at 0x...> — useless

# correct:
class Animal:
    def __init__(self, name, species):
        self.name    = name
        self.species = species

    def __str__(self):
        return f"{self.name} — {self.species}"

lassie = Animal("Lassie", "dog")
print(lassie)       # Lassie — dog

# ─────────────────────────────────────────────
# Quick reference
# ─────────────────────────────────────────────

# 1. Always self.attr — not attr — in __init__
# 2. Always call super().__init__() in child classes
# 3. Never mutable defaults — use None instead
# 4. Modify class attributes through the class, not an instance
# 5. One class, one responsibility
# 6. Respect the underscore — use setters
# 7. Methods need an instance — not the class directly
# 8. Always define __str__
