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.

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("den-acc-sw01", "10.20.30.11", "C9200L-48P-4X")
>>> sw.backup_command()        # inherited — never defined on CiscoSwitch
'show running-config'
>>> sw.model                   # its own addition
'C9200L-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.

Exercises (graded)

cd labs/python-foundations/lesson08
pytest -q

exercises.py 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
✅ 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.