Classes and Objects, Part 2

One device class is useful; a family of them is an architecture. Inheritance lets platform-specific classes share a common core - the exact pattern inside Netmiko and NAPALM - and dunder methods make your objects print, compare, and debug like first-class citizens.

TL;DR - only got 10 minutes? This is the lesson. Hit to have it read to you.
  • class CiscoSwitch(NetworkDevice): inherits everything from the base class; add or override only what differs. This is how multivendor libraries are built.
  • A subclass __init__ must call super().__init__(...) FIRST to run the base setup - skip it and base attributes silently never exist.
  • Redefining a method in the subclass overrides it: base backup_command() returns the generic answer, CiscoSwitch's version returns the IOS one.
  • isinstance(sw, NetworkDevice) is True for subclass instances too - code written against the base class accepts the whole family.
  • __str__ is what print() shows (for humans); __repr__ is what the REPL and debuggers show (for engineers - make it look like the constructor call).
  • __eq__ defines what == means: two device objects with the same hostname and ip can count as equal even though they're separate objects.
  • Default printout <NetworkDevice object at 0x10de31d90> isn't an error - it's the absence of __str__/__repr__. Now you can fix it.
  • Prove it: the graded lab at the end runs in your browser - implement the subclass and dunders and make every check green.
In this lesson you will:
  • Derive subclasses that inherit and extend a base class
  • Chain initializers correctly with super().__init__()
  • Override methods for platform-specific behavior
  • Check types with isinstance - and know why it beats exact matching
  • Implement __str__, __repr__, and __eq__ so objects behave like built-ins

Inheritance: the multivendor pattern

Your fleet isn’t one kind of device. Switches, routers, firewalls - mostly alike, each a little different. Copy-pasting NetworkDevice three times creates three maintenance problems; inheritance creates a family instead:

class NetworkDevice:
    def __init__(self, hostname, ip):
        self.hostname = hostname
        self.ip = ip

    def backup_command(self):
        return "show running-config"      # a sane generic answer

class CiscoSwitch(NetworkDevice):
    def __init__(self, hostname, ip, model):
        super().__init__(hostname, ip)    # run the base setup FIRST
        self.model = model                # then add what's new

CiscoSwitch(NetworkDevice) reads as “a CiscoSwitch is a NetworkDevice” - it inherits every attribute and method, then layers on its own. super().__init__() calls the parent’s initializer so hostname and ip get stored exactly as before.

>>> sw = CiscoSwitch("acc-sw01", "10.20.30.11", "C9300-48P-4X")
>>> sw.backup_command()        # inherited - never defined on CiscoSwitch
'show running-config'
>>> sw.model                   # its own addition
'C9300-48P-4X'

Overriding: same question, platform answer

When the family shares a question but not an answer, redefine the method in the subclass:

class JuniperRouter(NetworkDevice):
    def backup_command(self):
        return "show configuration"       # overrides the base version

Now the same line of calling code does the right thing for every device:

for device in fleet:
    print(device.backup_command())        # each answers in its own dialect

That loop is the payoff. Code written against the base class drives the whole family - and isinstance(device, NetworkDevice) stays True for every subclass, which is how libraries check “is this something I can treat as a device?” without caring which kind.

The device family - one base, platform subclasses
Rendering diagram…
View diagram source - it's just text (Mermaid). Diagrams-as-code is how modern network docs work; the flagship course has a free module on it.
classDiagram
  NetworkDevice <|-- CiscoSwitch
  NetworkDevice <|-- JuniperRouter
  class NetworkDevice {
      +hostname
      +ip
      +backup_command()
  }
  class CiscoSwitch {
      +model
      +backup_command() override
  }
  class JuniperRouter {
      +backup_command() override
  }

Dunders: making objects first-class

Print a bare object and you get the dreaded hex address:

>>> print(sw)
<__main__.CiscoSwitch object at 0x10de31d90>

Not an error - just Python admitting you never told it what this thing looks like. Three dunder (double-underscore) methods fix the everyday ergonomics:

class NetworkDevice:
    def __init__(self, hostname, ip):
        self.hostname = hostname
        self.ip = ip

    def __str__(self):                    # for humans: print(), f-strings
        return f"{self.hostname} ({self.ip})"

    def __repr__(self):                   # for engineers: REPL, debuggers
        return f"NetworkDevice(hostname={self.hostname!r}, ip={self.ip!r})"

    def __eq__(self, other):              # what == means for devices
        return self.hostname == other.hostname and self.ip == other.ip
  • __str__ answers print(sw) and f-string interpolation - make it the line you’d want in a report.
  • __repr__ answers the bare REPL echo and debugger displays - the convention is to look like the constructor call, so the repr() discipline you learned in Lesson 1 now pays out on your own types.
  • __eq__ defines ==. Two objects built from the same inventory row are different objects - but with __eq__, they can be equal by value, which is what tests and dedup logic actually want. (You met this idea from the other side in Lesson 4 - dicts and sets compare by value too.)

You already speak dunder: __init__ is one, len(x) calls __len__, in calls __contains__, and + calls __add__. The built-ins feel built-in because they implement the same protocol you just did.

Inheritance and dunders
Try it yourself (Python runs in your browser)
Output appears here. First run downloads the Python runtime (~10 MB), so give it a few seconds.

Prove it (graded lab)

Write each piece in the editor, then Run grader - it runs the lesson’s real checks right here in your browser, nothing to install. Green means the skill is yours, and these are the building blocks the Lesson 10 capstone (netaudit) is assembled from.

The editor hands you the finished Lesson 7 base class; you extend it:

  1. CiscoSwitch(NetworkDevice) - subclass with a model, chained through super()
  2. Override backup_command() with the IOS-specific answer
  3. __str__ on the base - every device prints readably
  4. __repr__ on the base - constructor-shaped, !r quoting
  5. __eq__ on the base - value equality by hostname + ip
Graded lab - 0 checks, run right here
Edit the functions above, then Run grader. First run downloads the Python runtime (~10 MB), so give it a few seconds.
Reveal a reference solution
"""Lesson 8 reference solutions. Read after an honest attempt."""


class NetworkDevice:
    def __init__(self, hostname, ip):
        self.hostname = hostname
        self.ip = ip

    def backup_command(self):
        return "show running-config"

    def __str__(self):
        return f"{self.hostname} ({self.ip})"

    def __repr__(self):
        return f"NetworkDevice(hostname={self.hostname!r}, ip={self.ip!r})"

    def __eq__(self, other):
        return self.hostname == other.hostname and self.ip == other.ip


class CiscoSwitch(NetworkDevice):
    def __init__(self, hostname, ip, model):
        super().__init__(hostname, ip)
        self.model = model

    def backup_command(self):
        return "show running-config view full"

Read it, close it, then re-type your own version from memory. Wrong turns are where the learning is.

Check your understanding

CiscoSwitch defines __init__ but skips super().__init__(hostname, ip). What happens?

1 / 3

Summary

Inheritance turns one class into a family: subclasses share the base core via super().__init__() (skip it and base attributes silently never exist), override methods where platforms disagree, and remain isinstance-compatible so one loop drives every vendor - the literal architecture inside Netmiko and NAPALM. Dunders finish the job of making your types feel native: __str__ for reports, __repr__ shaped like the constructor for debugging, __eq__ for value equality. Next lesson steps out of the language and into the ecosystem: pip, virtual environments done properly, and the libraries that do the heavy lifting.