← Home

084. Basic Inheritance

Extend existing classes to add specialized behavior

084. Basic Inheritance

He adds GPU processes to the RAM manager. A GPU process is like a regular Process, but also has vram_mb (video RAM). He does NOT want to duplicate all of Process.

Inheritance: GpuProcess extends Process and adds what it needs.

class Process:
    def __init__(self, name, pid, rss_mb):
        self.name   = name
        self.pid    = pid
        self.rss_mb = rss_mb

    def total_memory(self):
        return self.rss_mb

    def summary(self):
        return f"{self.name}[{self.pid}]: {self.rss_mb} MB RAM"

class GpuProcess(Process):
    def __init__(self, name, pid, rss_mb, vram_mb):
        super().__init__(name, pid, rss_mb)  # initialize the parent
        self.vram_mb = vram_mb               # add GPU-specific attribute

    def total_memory(self):                  # override the parent method
        return self.rss_mb + self.vram_mb

gpu = GpuProcess("cuda_worker", 5678, 200, 4096)
print(gpu.total_memory())  # 4296
print(isinstance(gpu, Process))  # True — a GpuProcess IS-A Process

Without super().__init__(), self.name, self.pid, self.rss_mb are never set and every method call crashes.


💡 Fun fact: Python supports multiple inheritance — a class can inherit from more than one parent simultaneously. To handle the diamond problem (where two parent classes share a grandparent), Python uses the C3 linearization algorithm to determine a consistent Method Resolution Order (MRO). You can inspect it with ClassName.__mro__. Most languages (Java, C#) deliberately forbid multiple inheritance to avoid this complexity.

⚠️ Watch out: Forgetting super().__init__() in a subclass is the single most common inheritance bug. The child’s __init__ completely replaces the parent’s — so if you don’t explicitly call super().__init__(), none of the parent’s attributes get set, and every method that touches them will crash with AttributeError.

🤔 Think about it: Inheritance models an “is-a” relationship: a GpuProcess is-a Process. But Python also supports “has-a” composition — giving GpuProcess a Process object as an attribute instead of inheriting from it. When would you choose composition over inheritance, and what makes one approach cleaner than the other?

Learning objectives

  • Create subclasses that inherit from a base class
  • Call super().init() to initialize the parent
  • Override methods to provide specialized behavior

Key concepts

  • inheritance
  • super()
  • method overriding
  • subclass
  • isinstance

Try it

Concept detail

Inheritance: class Child(Parent): — Child gets all attributes and methods of Parent.

class Circle(Shape):   # Circle IS-A Shape
    ...

super().init(…) calls the parent’s init. ALWAYS call this in a child’s init when the parent has its own init logic — otherwise parent attributes are never set.

class Circle(Shape):
    def __init__(self, radius, color="black"):
        super().__init__(color)   # sets self.color via Shape.__init__
        self.radius = radius      # then add child-specific attributes

Overriding: redefine a method in the child to change its behavior.

class Circle(Shape):
    def area(self):                     # overrides Shape.area()
        return math.pi * self.radius ** 2

The child inherits everything it does NOT override: c = Circle(5) str(c) # uses Shape.str — not overridden in Circle

isinstance(obj, Parent): isinstance(Circle(1), Shape) # True — Circle IS-A Shape isinstance(Circle(1), Circle) # True isinstance(Shape(), Circle) # False — Shape is NOT a Circle

Use inheritance for “is-a” relationships: Circle IS-A Shape ✓ GpuProcess IS-A Process ✓ TaskList IS-A BankAccount ✗ (no relationship)

Solution

import math

class Shape:
    def __init__(self, color="black"):
        self.color = color

    def area(self):
        return 0

    def __str__(self):
        return f"{self.__class__.__name__}(color={self.color}, area={self.area():.2f})"

class Circle(Shape):
    def __init__(self, radius, color="black"):
        super().__init__(color)
        self.radius = radius

    def area(self):
        return math.pi * self.radius ** 2

class Rectangle(Shape):
    def __init__(self, width, height, color="black"):
        super().__init__(color)
        self.width  = width
        self.height = height

    def area(self):
        return self.width * self.height

Tests

def test_circle_area_unit():
    c = Circle(1)
    assert abs(c.area() - math.pi) < 0.0001

def test_circle_area_5():
    c = Circle(5)
    assert abs(c.area() - math.pi * 25) < 0.0001

def test_circle_color_default():
    c = Circle(3)
    assert c.color == "black"

def test_circle_color_custom():
    c = Circle(3, color="red")
    assert c.color == "red"

def test_rectangle_area():
    r = Rectangle(4, 5)
    assert r.area() == 20

def test_rectangle_area_square():
    r = Rectangle(3, 3)
    assert r.area() == 9

def test_rectangle_color():
    r = Rectangle(3, 3, "blue")
    assert r.color == "blue"

def test_isinstance():
    c = Circle(1)
    assert isinstance(c, Shape)
    assert isinstance(c, Circle)

Resources