OOP Deep Dive

Python Intermediate
Course 2 ยท Chapter 2 ยท OOP Deep Dive

๐Ÿ”— OOP Deep Dive

Chapter 1 covered defining a single class. This chapter covers three ways classes work together and with the rest of the language: inheritance (building a class on top of another), polymorphism (different classes responding to the same method call), and magic/dunder methods โ€” the hooks that let your own objects work with 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.

โš  Forgetting super().__init__() Silently Skips Parent Setup

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 __eq__, == Compares Identity, Not Values

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

FeatureGoKotlinPython
InheritanceNo classes/inheritance โ€” struct embedding approximates itClasses are final by default; must mark open class to allow subclassingEvery class is inheritable by default, no keyword needed
Custom string formImplement the Stringer interface's String() methodOverride toString()Define __str__
Custom equalityManual field-by-field comparison (or reflect.DeepEqual)data class generates equals() automaticallyDefine __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.

โ†’ Solution

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().

โ†’ Solution

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.

โ†’ Solution

๐ŸŽฏ What's Next

Next chapter: Exception Handling โ€” try/except/else/finally, raising exceptions, and custom exceptions.