OOP Deep Dive
๐ OOP Deep Dive
print(), ==, and other built-in behavior.
๐งฌ Inheritance
class Animal: def __init__(self, name): self.name = name def speak(self): return f"{self.name} makes a sound" class Dog(Animal): # Dog inherits from Animal def __init__(self, name, breed): super().__init__(name) # call Animal's __init__ first self.breed = breed def speak(self): # overriding Animal's speak() return f"{self.name} says Woof!" rex = Dog("Rex", "Labrador") print(rex.speak()) # Rex says Woof! โ Dog's own version
class Dog(Animal): makes Dog a subclass of Animal, inheriting its attributes and methods. super().__init__(name) calls the parent class's constructor, so Dog doesn't have to duplicate the logic for setting self.name.
If Dog.__init__ sets self.breed but never calls super().__init__(name), the object never gets a self.name attribute at all โ calling rex.name later raises an AttributeError, and the failure happens far away from the actual mistake, making it a confusing bug to track down. Whenever a subclass defines its own __init__, call super().__init__(...) first unless you deliberately want to skip the parent's setup.
๐ญ Polymorphism
Different classes can implement the same method name, and calling code can treat them uniformly without checking which specific class it's dealing with:
class Cat(Animal): def speak(self): return f"{self.name} says Meow!" animals = [Dog("Rex", "Labrador"), Cat("Whiskers")] for animal in animals: print(animal.speak()) # each object uses its OWN speak(), automatically # Rex says Woof! # Whiskers says Meow!
The loop doesn't need an if isinstance(animal, Dog) check anywhere โ calling .speak() automatically runs whichever version belongs to that object's actual class. This is polymorphism: one interface (.speak()), many implementations.
โจ Magic / Dunder Methods
"Dunder" (double underscore) methods let your class hook into Python's built-in behavior. __init__ is one you already know โ here are two more of the most common:
class Point: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f"({self.x}, {self.y})" def __eq__(self, other): return self.x == other.x and self.y == other.y p1 = Point(1, 2) p2 = Point(1, 2) print(p1) # (1, 2) โ uses __str__, not "<__main__.Point object at 0x...>" print(p1 == p2) # True โ uses __eq__, compares VALUES not identity
Without a custom __eq__, p1 == p2 would be False even with identical x/y values โ the default == checks whether both names point to the exact same object in memory, the same identity-vs-equality distinction that matters for strings and numbers too, but is easy to forget once you're writing your own classes.
Similarly, without __str__, print(p1) would show something unhelpful like <__main__.Point object at 0x000001A2B3C4D5E6> โ the default representation, which just identifies the object's type and memory address rather than anything about its actual data.
Inheritance & Object Representation: Go vs Kotlin vs Python
| Feature | Go | Kotlin | Python |
|---|---|---|---|
| Inheritance | No classes/inheritance โ struct embedding approximates it | Classes are final by default; must mark open class to allow subclassing | Every class is inheritable by default, no keyword needed |
| Custom string form | Implement the Stringer interface's String() method | Override toString() | Define __str__ |
| Custom equality | Manual field-by-field comparison (or reflect.DeepEqual) | data class generates equals() automatically | Define __eq__ manually |
Kotlin's open class requirement is a real, easy-to-forget difference coming from Python โ Python never blocks subclassing by default, where Kotlin does unless you opt in.
Inheritance
class Child(Parent):, with super() to call the parent's methods.
Overriding
Redefining a parent's method in the subclass to change its behavior.
Polymorphism
Calling code uses one method name; each object's own class decides what actually runs.
Dunder methods
__str__ for print(), __eq__ for == โ hooks into built-in behavior.
๐ป Coding Challenges
Challenge 1: Shape Hierarchy
Write a base class Shape with a method area() that returns 0, then subclasses Square(side) and Circle(radius) that each override area() with their correct formula. Loop over a list of one of each and print each shape's area.
Goal: Practice inheritance and overriding together, plus polymorphic iteration.
Challenge 2: A Printable Fraction Class
Write a Fraction class with numerator and denominator instance attributes, and a __str__ method that prints it as "numerator/denominator" (e.g. "3/4").
Goal: Practice writing a __str__ method and seeing it kick in automatically with print().
Challenge 3: Value-Based Equality
Add an __eq__ method to the Fraction class from Challenge 2, so that two Fraction objects with the same numerator and denominator compare as equal with ==. Prove it works by comparing two separately created but equal fractions.
Goal: Practice writing __eq__ and understand why it's needed for value-based comparison.
๐ฏ What's Next
Next chapter: Exception Handling โ try/except/else/finally, raising exceptions, and custom exceptions.