Table of Contents Link to heading
- Inheritance Fundamentals
- Single Inheritance
- Using super() to Call Parent Methods
- Method Overriding
- Multiple Inheritance
- Method Resolution Order (MRO)
- Abstract Base Classes
- Checking Inheritance Relationships
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}
__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 usesuper() - 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'>,)