Skip to content

Python Inheritance

Inheritance is a cornerstone of object-oriented programming (OOP). It provides a mechanism for creating a new class that is based on an existing class. The new class inherits attributes and behaviors (i.e., fields and methods) from the existing class.

Imagine inheritance in OOP as analogous to genetic inheritance in biology: just as you might inherit traits from your parents, a subclass inherits properties and methods from its parent class.

  • Parent Class: The class whose properties and methods are inherited by another class.
  • Child Class: The class that inherits properties and methods from another class.

Inheritance Use Cases:

  • Reusability: Instead of rewriting code, you can reuse attributes and behaviors from existing classes.
  • Extensibility: You can extend functionalities of base classes without modifying their original implementation.
  • Logical Structure: Grouping shared attributes and behaviors in a base class and specific attributes in derived classes creates a logical hierarchy and structure.
  • Override Mechanism: A child class can provide a specific implementation of a method that's already defined in its parent class.

Python Inheritance Example

In Python, inheritance is achieved by defining a new class that is derived from an existing class. Here's a basic example to illustrate:

class NetworkDevice:
    def __init__(self, serial, IP, name):
        self.serial = serial
        self.IP = IP
        self.name = name

    def turn_on_port(self, port_number):
        print(f"Turning on port {port_number} on {self.name}.")

    def __str__(self):
        return f"{self.name} - IP: {self.IP}, Serial: {self.serial}"

class Cisco9400(NetworkDevice):

    def turn_on_port(self, port_number):  # Overriding the parent class method to add specific behavior for Cisco9400
        print(f"Turning on port {port_number} on Cisco9400: {self.name}.")


cisco_device = Cisco9400("ABC123", "192.168.1.1", "Main_Switch")
print(cisco_device)  # Output: Main_Switch - IP: 192.168.1.1, Serial: ABC123
cisco_device.turn_on_port(5)  # Output: Turning on port 5 on Cisco9400.

In this example, the Cisco9400 class inherits from the NetworkDevice class. Hence, it has access to the attributes and methods of the NetworkDevice class. However, the Cisco9400 class also provides its own implementation of the turn_on_port method, which overrides the method from its parent class.

Python Inheritance Example - LAB

Using the below table, develop a parent Class CommandRunner and two child classes that inherit from the parent Class called JuniperCommand and CiscoCommand. The methods (named as in the first column) will print as described below in each respective column.

Warning

In production, these would likely be attributes, but this exercise use methods.

method name CommandRunner CiscoCommand JuniperCommand
show_clock "show clock" "show clock" "show system uptime"
show_history "show history" "show history" "show cli history"
show_version "show version" "show version" "show version"
show_interface "show interface" "show ip interface brief" "show interfaces terse"

Info

Knowing how inheritance works, use your judgement on which methods need to be applied to each class.

The super() Function

In Python, super() is a built-in function used in the with inheritance. It allows you to call methods from the parent class within a derived class. That is to say that the child class can choose to still run the parent class method as well as it's own code. This is especially useful when you want to extend or modify the behavior of a parent class's method in the child class.

In here you can see how super is called on the __init__ method.

class NetworkDevice:
    def __init__(self, serial, IP, name):
        self.serial = serial
        self.IP = IP
        self.name = name

    def turn_on_port(self, port_number):
        print(f"Turning on port {port_number} on {self.name}.")

    def __str__(self):
        return f"{self.name} - IP: {self.IP}, Serial: {self.serial}"

class Cisco9400(NetworkDevice):
    def __init__(self, serial, IP, name, firmware_version):
        super().__init__(serial, IP, name)
        self.firmware_version = firmware_version

    def update_firmware(self, new_version):
        print(f"Updating firmware on {self.name} from version {self.firmware_version} to {new_version}.")
        self.firmware_version = new_version

    def turn_on_port(self, port_number):  # Overriding the parent class method to add specific behavior for Cisco9400
        print(f"Turning on port {port_number} on Cisco9400: {self.name} using firmware version {self.firmware_version}.")

# Testing
cisco_device = Cisco9400("ABC123", "192.168.1.1", "Main_Switch", "3.2.1")
print(cisco_device)  # Output: Main_Switch - IP: 192.168.1.1, Serial: ABC123
cisco_device.turn_on_port(5)  # Output: Turning on port 5 on Cisco9400: Main_Switch using firmware version 3.2.1.
cisco_device.update_firmware("3.3.0")

Be aware the order in which super is ran, may need to be considered, if the parent or child class depends on the other. As an example, if you set a value in the parent class that is referenced in the child class, super should be called first.

The super() Function - LAB

Add to your JuniperCommand class's show_version method to also print the command show hardware after it runs the command show version.

Inheritance in a Dictionary

While we commonly consider inheritance in Python with classes, it is a concept that can be applied across the board.

In Nautobot's config context, this concept is used to create a single structured data from many of them with a specific order.

In this example, we will use an "interface profile" data structure.

Imagine that we have the following common profiles.

port_profiles:
    l3_default_interface:
        admin_status: 'up'
    default_access:
        description: 'Standard Port'
        mode: 'access'
        access_vlan: '1010'
        voice_vlan: '2010'
        routedip: 'disabled'
    voice_access:
        description: 'Voice Only Port'
        mode: 'access'
        access_vlan: '2010'
        voice_vlan: '2010'
        routedip: 'disabled'
    uplink_port:
        description: 'Uplink Port'
        mode: 'trunk'
        routedip: 'disabled'
        vlans: '1-4096'

We can use this to provide summarized data to "compress" our data. Let's take a look at what the complimentary data structure would look like.

interfaces:
  - name: "Loopback0"
    description: "MGMT INTERFACE"
    ip_address: "172.16.255.105"
    subnet_mask: "255.255.255.0"
    port_profile: "l3_default_interface"

  - name: "GigabigEthernet1/1"
    description: "To NYC-RS01 - G1/1"
    port_profile: "uplink_port"

  - name: "GigabigEthernet1/2"
    port_profile: "default_access"

  - name: "GigabigEthernet1/3"
    description: "CIO Port"
    port_profile: "default_access"
    speed: 1000
    duplex: full

  - name: "GigabigEthernet1/4"
    port_profile: "voice_access"

Now, combining these two data points, we can get data that get's expanded like.

  - name: "GigabigEthernet1/3"
    description: "CIO Port"
    port_profile: "default_access"
    speed: 1000
    duplex: full
    mode: 'access'
    access_vlan: '1010'
    voice_vlan: '2010'
    routedip: 'disabled'

That is to say, that the data for the port_profile: default_access get's added to the data.

Inheritance in a Dictionary - LAB

Build the actual data dictionary merging capability to create the expected final data structure.

interfaces = [
  {
    "name": "Loopback0",
    "description": "MGMT INTERFACE",
    "ip_address": "172.16.255.105",
    "subnet_mask": "255.255.255.0",
    "port_profile": "l3_default_interface"
  },
  {
    "name": "GigabigEthernet1/1",
    "description": "To NYC-RS01 - G1/1",
    "port_profile": "uplink_port"
  },
  {
    "name": "GigabigEthernet1/2",
    "port_profile": "default_access"
  },
  {
    "name": "GigabigEthernet1/3",
    "description": "CIO Port",
    "port_profile": "default_access",
    "speed": 1000,
    "duplex": "full"
  },
  {
    "name": "GigabigEthernet1/4",
    "port_profile": "voice_access"
  }
]

port_profiles = {
  "l3_default_interface": {
    "admin_status": "up"
  },
  "default_access": {
    "description": "Standard Port",
    "mode": "access",
    "access_vlan": "1010",
    "voice_vlan": "2010",
    "routedip": "disabled"
  },
  "voice_access": {
    "description": "Voice Only Port",
    "mode": "access",
    "access_vlan": "2010",
    "voice_vlan": "2010",
    "routedip": "disabled"
  },
  "uplink_port": {
    "description": "Uplink Port",
    "mode": "trunk",
    "routedip": "disabled",
    "vlans": "1-4096"
  }
}

def merge_profile(port_profiles, interfaces):
    # Add your code here
    pass

updated_interfaces = merge_profile(port_profiles, interfaces)
print(updated_interfaces)