The Inheritance Gallery: Parent and Child Classes

 

The Inheritance Gallery: Parent and Child Classes





Timothy's library had grown beyond simple books. He now cataloged audiobooks, ebooks, and rare manuscripts—each with unique attributes. Audiobooks had narrators and durations. Ebooks had file formats and download links. Manuscripts had preservation conditions and historical significance.

His first attempt created separate classes with duplicated code:

class Book:
    def __init__(self, title, author, year):
        self.title = title
        self.author = author
        self.year = year

class Audiobook:
    def __init__(self, title, author, year, narrator, duration_minutes):
        self.title = title        # Duplicated
        self.author = author      # Duplicated
        self.year = year          # Duplicated
        self.narrator = narrator
        self.duration_minutes = duration_minutes

class Ebook:
    def __init__(self, title, author, year, file_format, download_url):
        self.title = title        # Duplicated
        self.author = author      # Duplicated
        self.year = year          # Duplicated
        self.file_format = file_format
        self.download_url = download_url

Margaret found him writing the same initialization code for the third time. "You're repeating yourself," she observed. "Come to the Inheritance Gallery—where specialized blueprints inherit from general ones."

The Parent-Child Relationship

Margaret showed Timothy inheritance:

class Book:
    def __init__(self, title, author, year):
        self.title = title
        self.author = author
        self.year = year

    def get_summary(self):
        return f'"{self.title}" by {self.author} ({self.year})'

class Audiobook(Book):  # Inherits from Book
    def __init__(self, title, author, year, narrator, duration_minutes):
        super().__init__(title, author, year)  # Call parent's __init__
        self.narrator = narrator
        self.duration_minutes = duration_minutes

# Create an audiobook
dune_audio = Audiobook("Dune", "Herbert", 1965, "Scott Brick", 1233)

# Audiobook has Book's attributes and methods
print(dune_audio.title)          # "Dune"
print(dune_audio.narrator)       # "Scott Brick"
print(dune_audio.get_summary())  # "Dune" by Herbert (1965)

"The Audiobook class inherits from Book," Margaret explained. "It gets all of Book's attributes and methods automatically. We only add what's unique to audiobooks."

Understanding super()

Timothy was curious about super():

class Book:
    def __init__(self, title, author, year):
        self.title = title
        self.author = author
        self.year = year

class Audiobook(Book):
    def __init__(self, title, author, year, narrator, duration_minutes):
        # super() finds the parent class and calls its method
        super().__init__(title, author, year)
        # Now add audiobook-specific attributes
        self.narrator = narrator
        self.duration_minutes = duration_minutes

"super() calls the parent class's method," Margaret explained. "It handles the shared initialization, then you add specialized attributes. This eliminates duplication."

Method Overriding

Timothy learned child classes could replace parent methods:

class Book:
    def __init__(self, title, author, year):
        self.title = title
        self.author = author
        self.year = year

    def get_summary(self):
        return f'"{self.title}" by {self.author} ({self.year})'

class Audiobook(Book):
    def __init__(self, title, author, year, narrator, duration_minutes):
        super().__init__(title, author, year)
        self.narrator = narrator
        self.duration_minutes = duration_minutes

    # Override parent's method
    def get_summary(self):
        hours = self.duration_minutes / 60
        return f'"{self.title}" by {self.author} ({self.year}) - Narrated by {self.narrator}, {hours:.1f} hours'

dune = Book("Dune", "Herbert", 1965)
dune_audio = Audiobook("Dune", "Herbert", 1965, "Scott Brick", 1233)

print(dune.get_summary())
# "Dune" by Herbert (1965)

print(dune_audio.get_summary())
# "Dune" by Herbert (1965) - Narrated by Scott Brick, 20.6 hours

"The child class overrides the parent's method," Margaret noted. "When you call get_summary() on an Audiobook, Python uses the audiobook's version, not the book's."

Extending Parent Methods

Timothy discovered he could call the parent's method and then add to it:

class Audiobook(Book):
    def __init__(self, title, author, year, narrator, duration_minutes):
        super().__init__(title, author, year)
        self.narrator = narrator
        self.duration_minutes = duration_minutes

    def get_summary(self):
        # Call parent's method, then extend it
        base_summary = super().get_summary()
        hours = self.duration_minutes / 60
        return f'{base_summary} - Narrated by {self.narrator}, {hours:.1f} hours'

"This is better than duplicating code," Margaret explained. "You reuse the parent's logic and add specialized behavior."

The isinstance Check

Margaret showed Timothy how to check object types:

dune = Book("Dune", "Herbert", 1965, 412)
dune_audio = Audiobook("Dune", "Herbert", 1965, "Scott Brick", 1233)

# isinstance checks the hierarchy
print(isinstance(dune, Book))        # True
print(isinstance(dune_audio, Book))  # True - audiobook IS a book
print(isinstance(dune_audio, Audiobook))  # True
print(isinstance(dune, Audiobook))   # False - book is NOT an audiobook

# type checks exact type only
print(type(dune_audio) == Book)      # False - not exactly a Book
print(type(dune_audio) == Audiobook) # True - exactly an Audiobook

"An Audiobook is a Book," Margaret explained. "Use isinstance() when checking types - it respects inheritance. Use type() only when you need the exact class."

Method Resolution Order: How Python Finds Methods

Timothy wondered how Python decided which method to call when multiple classes were involved. Margaret showed him the Method Resolution Order:

class Book:
    def get_info(self):
        return "Book info"

class Audiobook(Book):
    def get_info(self):
        return "Audiobook info"

audiobook = Audiobook("Dune", "Herbert", 1965, "Scott Brick", 1233)

# Python searches classes in a specific order
print(Audiobook.__mro__)
# (<class 'Audiobook'>, <class 'Book'>, <class 'object'>)

# Order: Check Audiobook first, then Book, then object
# Every class inherits from object

"Python searches left-to-right, starting with the child class," Margaret explained. "When you call a method, Python checks Audiobook first, then Book, then object. This is why child methods override parent methods."

When You Don't Need to Override init

Timothy learned he didn't always need to override initialization:

class SpecialBook(Book):
    # No __init__ override - uses parent's automatically

    def get_special_status(self):
        return f'"{self.title}" is a special edition!'

# Uses Book's __init__ automatically
special = SpecialBook("Dune", "Herbert", 1965, 412)
print(special.get_special_status())  # Works!

"If you don't override __init__, the child class uses the parent's," Margaret noted. "Only override when you need additional attributes."

Real-World Example: Library Catalog System

Margaret demonstrated a practical hierarchy:

class Book:
    def __init__(self, title, author, year, pages):
        self.title = title
        self.author = author
        self.year = year
        self.pages = pages
        self.is_checked_out = False

    def checkout(self):
        if self.is_checked_out:
            return False
        self.is_checked_out = True
        return True

    def return_item(self):
        self.is_checked_out = False

    def get_reading_time(self):
        return self.pages * 2  # 2 minutes per page

class Audiobook(Book):
    def __init__(self, title, author, year, narrator, duration_minutes):
        super().__init__(title, author, year, pages=0)  # Audiobooks have no pages
        self.narrator = narrator
        self.duration_minutes = duration_minutes

    def get_reading_time(self):
        # Override - return listening time instead
        return self.duration_minutes

class Ebook(Book):
    def __init__(self, title, author, year, pages, file_format, file_size_mb):
        super().__init__(title, author, year, pages)
        self.file_format = file_format
        self.file_size_mb = file_size_mb

    def get_download_info(self):
        return f'{self.file_format} format, {self.file_size_mb}MB'

# All types can be checked out
catalog = [
    Book("Dune", "Herbert", 1965, 412),
    Audiobook("1984", "Orwell", 1949, "Simon Prebble", 720),
    Ebook("Foundation", "Asimov", 1951, 255, "EPUB", 2.4)
]

# Polymorphism - treat all items the same way
for item in catalog:
    print(f'{item.title}: {item.get_reading_time()} minutes')
    item.checkout()

When to Use Inheritance

Margaret emphasized when inheritance made sense:

Use inheritance when:

  • There's a clear "is-a" relationship (audiobook IS a book)
  • Child classes share substantial behavior with the parent
  • You want polymorphism (treating different types uniformly)
  • The hierarchy is stable and unlikely to change

Don't use inheritance when:

  • Relationship is "has-a" (car HAS an engine, not car IS an engine)
  • You only need a few methods from another class
  • The hierarchy becomes complex (more than 2-3 levels deep)
  • Composition would be clearer

Composition vs Inheritance

Margaret showed Timothy the alternative to inheritance:

# Inheritance approach
class Audiobook(Book):
    def __init__(self, title, author, year, narrator, duration_minutes):
        super().__init__(title, author, year)
        self.narrator = narrator
        self.duration_minutes = duration_minutes

# Composition approach - "has-a" relationship
class Audiobook:
    def __init__(self, title, author, year, narrator, duration_minutes):
        self.book_info = Book(title, author, year)  # Has a Book
        self.narrator = narrator
        self.duration_minutes = duration_minutes

    def get_summary(self):
        return f'{self.book_info.get_summary()} - Narrated by {self.narrator}'

"Composition means containing another object," Margaret explained. "Favor composition over inheritance when the relationship isn't clearly 'is-a'."

Multiple Inheritance: A Brief Look

Timothy glimpsed a more complex pattern:

class Book:
    def __init__(self, title, author, year, pages=0, **kwargs):
        super().__init__(**kwargs)  # Pass remaining arguments up the chain
        self.title = title
        self.author = author
        self.year = year
        self.pages = pages

class AudioContent:
    def __init__(self, narrator, duration_minutes, **kwargs):
        super().__init__(**kwargs)
        self.narrator = narrator
        self.duration_minutes = duration_minutes

    def get_audio_info(self):
        hours = self.duration_minutes / 60
        return f'Narrated by {self.narrator}, {hours:.1f} hours'

class DigitalContent:
    def __init__(self, file_format, file_size_mb, **kwargs):
        super().__init__(**kwargs)
        self.file_format = file_format
        self.file_size_mb = file_size_mb

    def get_file_info(self):
        return f'{self.file_format}, {self.file_size_mb}MB'

class Audiobook(Book, AudioContent, DigitalContent):
    def __init__(self, title, author, year, narrator, duration_minutes, file_format, file_size_mb):
        super().__init__(
            title=title,
            author=author,
            year=year,
            narrator=narrator,
            duration_minutes=duration_minutes,
            file_format=file_format,
            file_size_mb=file_size_mb
        )

# Has attributes and methods from all parent classes
audiobook = Audiobook("Dune", "Herbert", 1965, "Scott Brick", 1233, "MP3", 450)
print(audiobook.title)           # From Book
print(audiobook.get_audio_info()) # From AudioContent
print(audiobook.get_file_info())  # From DigitalContent

"Multiple inheritance lets a class inherit from multiple parents," Margaret cautioned. "Notice the **kwargs pattern - it's called cooperative inheritance. Each parent's __init__ calls super(), passing remaining arguments up the chain. But this is complex - usually composition is clearer."

Timothy's Inheritance Wisdom

Through exploring the Inheritance Gallery, Timothy learned essential principles:

Inheritance creates "is-a" relationships: Child classes are specialized versions of parent classes.

super() calls parent methods: Eliminates code duplication in initialization and methods.

Method overriding replaces parent behavior: Child classes can provide specialized implementations.

Extending methods reuses parent logic: Call parent method with super(), then add more.

Use isinstance() for type checking: It respects inheritance hierarchy, unlike type().

Method Resolution Order (MRO) determines lookup: Python searches child first, then parents, then object.

Not all methods need overriding: If parent's __init__ is sufficient, don't override it.

Polymorphism treats types uniformly: Different classes can respond to the same method calls.

Favor composition over inheritance: "Has-a" relationships often clearer than "is-a".

Keep hierarchies shallow: More than 2-3 levels becomes hard to maintain.

Multiple inheritance requires cooperation: Use **kwargs pattern with super() for correct behavior.

Multiple inheritance is complex: Composition is usually simpler and clearer.

Inheritance is for shared behavior: Not just for code reuse—composition can do that too.

Timothy had discovered how to build specialized blueprints from general ones—creating hierarchies where audiobooks, ebooks, and manuscripts could all be treated as books while maintaining their unique characteristics. The Inheritance Gallery had revealed the power and the limits of parent-child relationships in object-oriented design.


Aaron Rose is a software engineer and technology writer at tech-reader.blog and the author of Think Like a Genius.

Comments

Popular posts from this blog

The New ChatGPT Reason Feature: What It Is and Why You Should Use It

Raspberry Pi Connect vs. RealVNC: A Comprehensive Comparison

Insight: The Great Minimal OS Showdown—DietPi vs Raspberry Pi OS Lite