Building Reusable Components

Inheritance is Python's mechanism for building a new class on top of an existing one. When class Admin(User): declares User as its base, Admin automatically gets every attribute and method User defines, and can add, override, or extend them. The standard library, most web frameworks, and many of your own projects are built on short inheritance chains.

The guiding principle is is-a: a class should inherit from another only if every instance of the subclass truly is a kind of the parent. Admin is a User, Dog is an Animal. If the relationship is better described as has-a (Order has a Customer), prefer composition: hold the other object as an attribute.

Inheritance unlocks two powers at once: code reuse (the base class's methods are available for free) and polymorphism (code written against the base class works with any subclass). Both benefits come with responsibility: changes in the base class ripple through every subclass, so keep the base small and stable.

Python supports multiple inheritance: class C(A, B):. It can be useful for genuine mix-ins but is easy to misuse. Most code is better served by single inheritance plus composition. When multiple inheritance does make sense, understand the method resolution order (C.__mro__) before writing it.

Declaring a subclass and using super()

class Admin(User): pass is already a valid subclass — empty body, identical behavior, new name. Override methods by defining them again in the subclass. Call the parent's version inside your override with super().method(...).

super() returns a proxy that dispatches to the next class in the method resolution order. Typical use: super().__init__(...) at the top of a subclass __init__ to run the parent initialization before adding your own.

Avoiding deep hierarchies

Two levels of inheritance is usually plenty. Beyond that, it is very hard to keep track of which class defines what. Prefer composition (keep the other object as an attribute) and narrow protocols (use typing.Protocol or abc.ABC) over "framework-style" base classes that demand subclassing for every new behavior.

A good rule: subclass to reuse code in behavior that is truly shared, not just similar. Two classes that share an attribute but do different things with it probably shouldn't share a base class.

Inheritance fundamentals.

ToolPurpose
class B(A):
syntax
Declares B as a subclass of A.
super().m(...)
built-in
Calls the parent class's implementation.
Cls.__mro__
attribute
Method resolution order for the class.
isinstance(obj, Base)
built-in
True for instances of subclasses too.
issubclass(C, B)
built-in
Checks subclass relationship.
abc.ABC / @abstractmethod
module
Declare required methods in a base class.
typing.Protocol
class
Structural subtyping without inheritance.
__init_subclass__
hook
Customize behavior for every new subclass.

Building Reusable Components code example

The script builds a User / Admin / Guest hierarchy and shows super() in action.

# Lesson: Building Reusable Components
class User:
    def __init__(self, username: str, email: str) -> None:
        self.username = username
        self.email = email

    def greet(self) -> str:
        return f"hi, {self.username}"

    def __repr__(self) -> str:
        return f"{type(self).__name__}(u={self.username!r})"


class Admin(User):
    def __init__(self, username: str, email: str, level: int = 1) -> None:
        super().__init__(username, email)   # reuse parent init
        self.level = level

    def greet(self) -> str:
        base = super().greet()              # reuse parent behavior
        return f"{base} (admin level {self.level})"


class Guest(User):
    def __init__(self) -> None:
        super().__init__(username="guest", email="guest@example.com")

    def greet(self) -> str:
        return "welcome, guest!"


users: list[User] = [User("ana", "a@b"), Admin("ben", "b@c", level=3), Guest()]
for u in users:
    print(f"{u!r:35} -> {u.greet()}")

print("\nmro:", [c.__name__ for c in Admin.__mro__])
print("Admin is a User?", issubclass(Admin, User))
print("Guest is a User?", isinstance(Guest(), User))

Four ideas to watch:

1) `super().__init__(...)` lets the parent run first; the subclass adds its own fields.
2) `super().greet()` reuses the parent string and adds something specific.
3) `type(self).__name__` in __repr__ gives subclass-aware output automatically.
4) `__mro__` lists the order Python searches for methods.

Practice one level of inheritance.

class Animal:
    def __init__(self, name: str):
        self.name = name
    def sound(self) -> str:
        return ""
    def describe(self) -> str:
        return f"{self.name} says {self.sound() or '...'}"

class Dog(Animal):
    def sound(self) -> str:
        return "woof"

class Cat(Animal):
    def sound(self) -> str:
        return "meow"

for a in (Dog("Rex"), Cat("Mia")):
    print(a.describe())

Verify is-a and method reuse.

class A:
    def f(self): return "A"
class B(A):
    def f(self): return super().f() + "B"
assert B().f() == "AB"
assert isinstance(B(), A)
assert issubclass(B, A)

Running prints:

User(u='ana')                        -> hi, ana
Admin(u='ben')                      -> hi, ben (admin level 3)
Guest(u='guest')                    -> welcome, guest!

mro: ['Admin', 'User', 'object']
Admin is a User? True
Guest is a User? True