OOP Deep Dive

Python Intermediate — 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 Handlingtry/except/else/finally, raising exceptions, and custom exceptions.