Featured image

Table of Contents Link to heading

Inheritance Fundamentals Link to heading

Inheritance allows a class (the subclass or child) to acquire the attributes and methods of another class (the superclass or parent), then extend or modify that behaviour. This is the mechanism for code reuse in OOP — common functionality is defined once in a parent class and shared across multiple child classes.

Benefits:

  • Eliminates code duplication for shared behaviour
  • Enables polymorphism — different objects can respond to the same interface
  • Creates a logical classification hierarchy that mirrors real-world relationships

Python supports both single inheritance (one parent) and multiple inheritance (multiple parents).

Single Inheritance Link to heading

class NetworkDevice:
    """Base class for all network devices."""

    def __init__(self, hostname, ip_address, location=""):
        self.hostname = hostname
        self.ip_address = ip_address
        self.location = location
        self.interfaces = {}

    def add_interface(self, name, description=""):
        self.interfaces[name] = {"description": description, "state": "down"}

    def get_summary(self):
        return f"{self.hostname} ({self.ip_address})"

    def __str__(self):
        return self.get_summary()


class Router(NetworkDevice):
    """A router — inherits from NetworkDevice, adds routing-specific features."""

    def __init__(self, hostname, ip_address, routing_protocol="OSPF", location=""):
        super().__init__(hostname, ip_address, location)   # call parent __init__
        self.routing_protocol = routing_protocol
        self.routing_table = []

    def add_route(self, network, next_hop, metric=1):
        self.routing_table.append({
            "network": network,
            "next_hop": next_hop,
            "metric": metric
        })

    def get_summary(self):
        base = super().get_summary()
        return f"{base} [Router, {self.routing_protocol}, {len(self.routing_table)} routes]"


class Switch(NetworkDevice):
    """A switch — inherits from NetworkDevice, adds switching-specific features."""

    def __init__(self, hostname, ip_address, vlan_range=(1, 4094), location=""):
        super().__init__(hostname, ip_address, location)
        self.vlan_min, self.vlan_max = vlan_range
        self.vlans = {}

    def create_vlan(self, vlan_id, name):
        if not (self.vlan_min <= vlan_id <= self.vlan_max):
            raise ValueError(f"VLAN ID {vlan_id} out of range")
        self.vlans[vlan_id] = name

    def get_summary(self):
        base = super().get_summary()
        return f"{base} [Switch, {len(self.vlans)} VLANs]"


# Usage
r = Router("core-router-01", "10.0.0.1", "BGP")
r.add_interface("Gi0/0", "Uplink to ISP")
r.add_route("0.0.0.0/0", "203.0.113.1")

s = Switch("access-sw-01", "10.0.1.1")
s.create_vlan(10, "DATA")
s.create_vlan(20, "VOICE")

print(r)     # core-router-01 (10.0.0.1) [Router, BGP, 1 routes]
print(s)     # access-sw-01 (10.0.1.1) [Switch, 2 VLANs]

# Router inherits NetworkDevice methods
print(r.interfaces)           # {} (inherited from NetworkDevice)
r.add_interface("Gi0/1")     # inherited method works on Router instance

Using super() to Call Parent Methods Link to heading

super() returns a proxy object that delegates method calls to the next class in the MRO. It is the correct way to call a parent class’s method from a child class.

class Firewall(NetworkDevice):
    def __init__(self, hostname, ip_address, policy="default-deny"):
        super().__init__(hostname, ip_address)    # initialise parent
        self.policy = policy
        self.acl_rules = []

    def add_rule(self, action, src, dst, port=None):
        rule = {"action": action, "src": src, "dst": dst}
        if port:
            rule["port"] = port
        self.acl_rules.append(rule)

    def get_summary(self):
        parent_summary = super().get_summary()    # call parent method
        return f"{parent_summary} [Firewall, policy={self.policy}, {len(self.acl_rules)} rules]"

Always use super() rather than calling the parent class by name directly (NetworkDevice.__init__(self, ...)). Direct calls break under multiple inheritance because they bypass the MRO.

Method Overriding Link to heading

A subclass can override a parent method by defining a method with the same name. The subclass’s version is called instead of the parent’s:

class Router(NetworkDevice):
    def add_interface(self, name, description="", ip_address=None):
        """Override parent: also store IP address for routed interfaces."""
        super().add_interface(name, description)          # call parent version
        if ip_address:
            self.interfaces[name]["ip_address"] = ip_address

r = Router("router-01", "10.0.0.1")
r.add_interface("Gi0/0", "WAN uplink", ip_address="203.0.113.2")
print(r.interfaces)
# {'Gi0/0': {'description': 'WAN uplink', 'state': 'down', 'ip_address': '203.0.113.2'}}

Multiple Inheritance Link to heading

Python supports multiple inheritance — a class can inherit from more than one parent:

class Loggable:
    """Mixin that adds logging capability."""

    def log(self, message, level="INFO"):
        print(f"[{level}] {self.__class__.__name__}: {message}")


class Monitorable:
    """Mixin that adds monitoring capability."""

    def __init__(self):
        self._metrics = {}

    def record_metric(self, name, value):
        self._metrics[name] = value

    def get_metrics(self):
        return dict(self._metrics)


class ManagedRouter(Router, Loggable, Monitorable):
    """A router with logging and monitoring capabilities."""

    def __init__(self, hostname, ip_address, routing_protocol="OSPF"):
        Router.__init__(self, hostname, ip_address, routing_protocol)
        Monitorable.__init__(self)

    def add_route(self, network, next_hop, metric=1):
        super().add_route(network, next_hop, metric)
        self.log(f"Added route to {network} via {next_hop}")
        self.record_metric("route_count", len(self.routing_table))


mr = ManagedRouter("managed-router-01", "10.0.0.1", "OSPF")
mr.add_route("192.168.1.0/24", "10.0.0.254")
# [INFO] ManagedRouter: Added route to 192.168.1.0/24 via 10.0.0.254

print(mr.get_metrics())   # {'route_count': 1}
Tip
Use mixins (small, focused classes with no __init__ or a no-argument __init__) to add capabilities to multiple class hierarchies without tight coupling. Mixins should add one well-defined behaviour — logging, serialisation, monitoring — and nothing else.

Method Resolution Order (MRO) Link to heading

When a method is called on an instance, Python searches for it using the C3 linearisation algorithm (also called MRO). The MRO defines the order in which classes are searched.

print(ManagedRouter.__mro__)
# (<class 'ManagedRouter'>, <class 'Router'>, <class 'NetworkDevice'>,
#  <class 'Loggable'>, <class 'Monitorable'>, <class 'object'>)

# Equivalent helper method
print(ManagedRouter.mro())

Python searches classes left to right, depth first, from the most derived class upward. This order determines:

  • Which __init__ is called when you use super()
  • Which method implementation is used when multiple parent classes define the same method name

The “diamond problem” — where two parents share a common base class — is handled correctly by the C3 algorithm, which ensures each class in the hierarchy is visited exactly once:

class A:
    def greet(self): return "Hello from A"

class B(A):
    def greet(self): return "Hello from B"

class C(A):
    def greet(self): return "Hello from C"

class D(B, C):
    pass

d = D()
print(d.greet())          # Hello from B (B comes first in MRO)
print(D.mro())            # [D, B, C, A, object]

Abstract Base Classes Link to heading

Abstract base classes (ABCs) define an interface that subclasses must implement. They cannot be instantiated directly — they exist only to be subclassed.

from abc import ABC, abstractmethod

class NetworkDevice(ABC):
    """Abstract base class for all network devices."""

    def __init__(self, hostname, ip_address):
        self.hostname = hostname
        self.ip_address = ip_address

    @abstractmethod
    def connect(self):
        """Establish a management connection to the device."""
        pass

    @abstractmethod
    def get_running_config(self):
        """Retrieve the current running configuration."""
        pass

    def get_summary(self):
        """Concrete method available to all subclasses."""
        return f"{self.hostname} ({self.ip_address})"


class CiscoRouter(NetworkDevice):
    def connect(self):
        print(f"Connecting to {self.hostname} via SSH...")

    def get_running_config(self):
        return f"! Running config for {self.hostname}\n..."


# Cannot instantiate the abstract class
# d = NetworkDevice("test", "1.1.1.1")  # TypeError: Can't instantiate abstract class

# Must implement all abstract methods to instantiate
r = CiscoRouter("router-01", "10.0.0.1")
r.connect()   # Connecting to router-01 via SSH...

ABCs are the Python mechanism for defining interfaces — they enforce a contract that subclasses must fulfill. This is particularly useful in network automation frameworks where different device drivers must all expose the same operations.

Checking Inheritance Relationships Link to heading

r = Router("router-01", "10.0.0.1")
s = Switch("switch-01", "10.0.1.1")

isinstance(r, Router)           # True
isinstance(r, NetworkDevice)    # True (inheritance chain)
isinstance(r, Switch)           # False

issubclass(Router, NetworkDevice)    # True
issubclass(Switch, Router)           # False

# Check available attributes and methods
print(dir(r))                   # all attributes and methods including inherited
print(r.__class__.__name__)     # 'Router'
print(r.__class__.__bases__)    # (<class 'NetworkDevice'>,)