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.
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__answersprint(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 therepr()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.
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:
CiscoSwitch(NetworkDevice)- subclass with amodel, chained throughsuper()- Override
backup_command()with the IOS-specific answer __str__on the base - every device prints readably__repr__on the base - constructor-shaped,!rquoting__eq__on the base - value equality by hostname + ip
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.
CiscoSwitch defines __init__ but skips super().__init__(hostname, ip). What happens?
print(sw) shows <__main__.CiscoSwitch object at 0x10de31d90>. What is this?
Why prefer isinstance(device, NetworkDevice) over type(device) == NetworkDevice?
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.