OOP Deep Dive
🔗 OOP Deep Dive
print(), ==, and other built-in behavior.
🧬 Inheritance
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:
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:
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.