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.
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.
Exercises (graded)
cd labs/python-foundations/lesson08
pytest -q
exercises.py 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
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.