In case you're trying to read this through your email, be warned: This post is large and may get clipped by services like Gmail and others.
This isn't just an article or a typical post: it is a Python crash course delivered to your inbox. However, since services like Gmail often clip larger emails, I strongly recommend opening it in your browser instead.
Also, make sure to check “Part 1” before proceeding with this edition!
10. Testing and Validating OOP Code in Practice
By now, you've written Python classes like NetworkDevice, Router, Switch, and Interface. You've added logic, composition, and even special methods like __str__() to improve readability. But how do you know if your code works?
In the real world, especially when building automation modules that touch real networks, validating object behavior through tests is crucial. This applies not just for correctness, but also to help future-proof your code as it grows or changes.
Let’s walk through how to set up a lightweight but effective test and validation setup for your OOP code.
Why Test OOP Code?
Without testing, you risk:
Devices being instantiated incorrectly
Methods returning unexpected results
Interface states being mishandled
Future refactors silently breaking things
Testing helps:
✅ Validate logic
✅ Prevent regressions
✅ Encourage modularity
✅ Make future maintenance easier
✅ Build trust in automation
Step 1: Create a Basic Test Environment
We’re going to simulate a small network and run behaviors on it.
Let’s assume you have the following classes already defined:
NetworkDeviceRouter(NetworkDevice)Switch(NetworkDevice)Interface
If you don't already have these classes from previous exercises, let's create them from scratch and then manually set up test instances.
from pprint import pprint
class NetworkDevice:
def __init__(self, hostname, ip_address, vendor):
self.hostname = hostname
self.ip_address = ip_address
self.vendor = vendor
self.interfaces = []
def add_interface(self, interface):
self.interfaces.append(interface)
def __repr__(self):
return f"{self.vendor} {self.hostname} ({self.ip_address})"
class Router(NetworkDevice):
def generate_config_snippet(self):
return f"hostname {self.hostname}\ninterface Gig0/0\n ip address {self.ip_address}\n"
def check_bgp_peers(self):
# Simulate dummy peer state
return ["10.10.10.1 (Established)", "10.10.10.2 (Idle)"]
class Switch(NetworkDevice):
def generate_config_snippet(self):
return f"hostname {self.hostname}\nswitchport mode access\ninterface Eth1\n ip address {self.ip_address}\n"
class Interface:
def __init__(self, name, status, speed, is_uplink):
self.name = name
self.status = status
self.speed = speed
self.is_uplink = is_uplink
def __repr__(self):
return f"Interface {self.name}: {self.status}, {self.speed}, Uplink: {self.is_uplink}"
# Create devices
r1 = Router("R1", "10.0.0.1", "Cisco")
sw1 = Switch("SW1", "10.0.0.10", "Arista")
# Add interfaces
r1.add_interface(Interface("Gig0/0", status="up", speed="10G", is_uplink=True))
r1.add_interface(Interface("Gig0/1", status="down", speed="1G", is_uplink=False))
sw1.add_interface(Interface("Eth1", status="up", speed="10G", is_uplink=True))
sw1.add_interface(Interface("Eth2", status="up", speed="1G", is_uplink=False))
# Print device summaries
print(r1)
print(sw1)
# Show interface status
for iface in r1.interfaces:
print(iface)Output:
Cisco R1 (10.0.0.1)
Arista SW1 (10.0.0.10)
Interface Gig0/0: up, 10G, Uplink: True
Interface Gig0/1: down, 1G, Uplink: FalseAt this stage, you can verify visually if everything looks correct. But let’s go deeper.
Step 2: Assert Expected Behaviors
Now let’s assert that things work as expected, which is the first step toward unit testing.
assert r1.hostname == "R1"
assert len(r1.interfaces) == 2
# Check uplink status
assert r1.interfaces[0].is_uplink is True
assert r1.interfaces[0].status == "up"
assert r1.interfaces[1].status == "down"
# Validate a method
assert r1.generate_config_snippet().startswith("hostname R1")If anything fails, Python raises an AssertionError. You don’t need a full testing framework yet; just validating assumptions is incredibly useful.
Step 3: Write a Simple Health Check Simulator
Let’s define a function to simulate a basic audit of devices:
def check_interface_health(device):
print(f"Checking {device.hostname} interfaces...")
for iface in device.interfaces:
if iface.is_uplink and iface.status != "up":
print(f"[ALERT] {device.hostname} - {iface.name} is a DOWN uplink!")
elif not iface.is_uplink and iface.status != "up":
print(f"[WARN] {device.hostname} - {iface.name} is down (non-uplink)")
else:
print(f"[OK] {device.hostname} - {iface.name} is {iface.status}")
# Run it
check_interface_health(r1)
check_interface_health(sw1)Sample Output (focusing on the health check simulator code snippet above):
Checking SW1 interfaces...
[OK] SW1 - Eth1 is up
[OK] SW1 - Eth2 is upThis mimics real-world logic you might embed in a CI pipeline or an audit script.
Step 4: Validate Inheritance Behavior
Suppose your Router class has a custom method (which should have, if you pasted it earlier):
class Router(NetworkDevice):
def check_bgp_peers(self):
# Simulate dummy peer state
return ["10.10.10.1 (Established)", "10.10.10.2 (Idle)"]Let’s assert its output (make sure to add this to your code):
peers = r1.check_bgp_peers()
assert "10.10.10.1 (Established)" in peersThis confirms that class-specific behaviors are accessible and return expected results.
Step 5: Wrap It in a Reusable Validation Script
Here's a consolidated example. Ensure to add it to your code:
def test_device(device):
print(f"\nDevice Summary: {device}")
assert device.hostname is not None
assert isinstance(device.interfaces, list)
for iface in device.interfaces:
print(f"Interface: {iface}")
assert iface.name.startswith("Gig") or iface.name.startswith("Eth")
assert iface.status in ["up", "down"]
if isinstance(device, Router):
bgp_peers = device.check_bgp_peers()
print("BGP Peers:", bgp_peers)
assert len(bgp_peers) > 0
# Run against all test devices
test_device(r1)
test_device(sw1)Output (focusing on the snippet above):
Device Summary: Cisco R1 (10.0.0.1)
Interface: Interface Gig0/0: up, 10G, Uplink: True
Interface: Interface Gig0/1: down, 1G, Uplink: False
BGP Peers: ['10.10.10.1 (Established)', '10.10.10.2 (Idle)']
Device Summary: Arista SW1 (10.0.0.10)
Interface: Interface Eth1: up, 10G, Uplink: True
Interface: Interface Eth2: up, 1G, Uplink: FalseThis helps catch malformed interfaces, bad states, or failed method overrides, without a test framework!
Bonus: Quick-and-Dirty Simulation of Link Traversal
Modify the classes NetworkDevice and Interface to reflect the following:
class NetworkDevice:
def __init__(self, hostname, ip_address, vendor):
self.hostname = hostname
self.ip_address = ip_address
self.vendor = vendor
self.interfaces = []
def add_interface(self, interface):
interface.device = self # Set back-reference to this device
self.interfaces.append(interface)
def __repr__(self):
return f"{self.vendor} {self.hostname} ({self.ip_address})"class Interface:
def __init__(self, name, status, speed, is_uplink):
self.name = name
self.status = status
self.speed = speed
self.is_uplink = is_uplink
self.connected_to = None
self.device = None # Back-reference to parent device
def __repr__(self):
return f"Interface {self.name}: {self.status}, {self.speed}, Uplink: {self.is_uplink}"Then, let's add this quick-and-dirty simulation to the code:
# Link interfaces manually
r1.interfaces[0].connected_to = sw1.interfaces[0]
sw1.interfaces[0].connected_to = r1.interfaces[0]
# Traverse the link
link_peer = r1.interfaces[0].connected_to
print(f"{r1.hostname}:{r1.interfaces[0].name} -> {link_peer.device.hostname}:{link_peer.name}")Output (focusing on the quick-and-dirty simulation part of the code):
R1:Gig0/0 -> SW1:Eth1Testing this kind of topology linkage is essential in automation systems that deal with multi-device path traversal or link validations.
Some Thoughts
Concept | Purpose |
|---|---|
| Confirm code behavior manually |
Interface health checks | Simulate operational validation |
Inheritance testing | Validate subclass logic correctness |
Object graph traversal | Model and test topology navigation |
Manual simulation | Catch bugs before touching real networks |
Next Step: Test Automation at Scale
While we’ve shown manual and semi-automated testing, this naturally leads to more formal tools like:
pytest(for real unit testing)doctest(for validating docstring examples)CI pipelines (GitHub Actions, GitLab CI, etc.)
Mocking tools for simulating devices
But you don’t need those to start. Even simple functions like the ones above help network engineers write better, safer code right now.
11. Solution Example: Build a Device Inventory and Health Reporter
This is your capstone moment for this article. You’ve learned about Python classes, inheritance, composition, methods, and how to validate behavior. Now it’s time to put it all to work in a real-world scenario that mirrors something you might build into a monitoring, CMDB, or asset inventory system in a NetDevOps stack.
The Scenario
Your team is responsible for a mix of network devices, including routers, switches, and firewalls, across multiple sites. Each device has a unique hostname, vendor, and management IP address. Some interfaces are up, some are down. Some devices don’t respond to ping().
You’ve been asked to build a report that summarizes this inventory and health state using Python OOP.
What You’re Asked To Do
Model the following with Python classes:
Devices using inheritance:
Router,Switch,Firewall, all extendingNetworkDevice.Interfaces using composition: Each device has one or more
Interfaceobjects.Add methods like
ping()andis_healthy()to simulate behavior.Build a reporting function that:
Groups devices by type (Router, Switch, etc.)
Shows device attributes
Flags devices that fail to respond
Lists interfaces that are down
Step-by-Step Code Walkthrough (Full Example)
Let’s write the entire script, broken into sections, so you can follow along and adapt it for your environment.
1. Define Your Base Classes
class Interface:
def __init__(self, name, status="down", speed="1G", is_uplink=False):
self.name = name
self.status = status
self.speed = speed
self.is_uplink = is_uplink
def __str__(self):
return f"{self.name} [{self.status}, {self.speed}, uplink={self.is_uplink}]"
class NetworkDevice:
def __init__(self, hostname, ip_address, vendor):
self.hostname = hostname
self.ip_address = ip_address
self.vendor = vendor
self.interfaces = []
def add_interface(self, interface):
self.interfaces.append(interface)
def ping(self):
# Simulate a ping (you can randomize this for dynamic effect)
return "reachable" not in self.hostname.lower() # Fake simulation
def get_down_interfaces(self):
return [iface for iface in self.interfaces if iface.status != "up"]
def __str__(self):
return f"{self.hostname} ({self.vendor}) - {self.ip_address}"2. Add Subclasses for Specific Device Types
class Router(NetworkDevice):
def get_type(self):
return "Router"
class Switch(NetworkDevice):
def get_type(self):
return "Switch"
class Firewall(NetworkDevice):
def get_type(self):
return "Firewall"3. Create Your Sample Inventory
inventory = []
# Routers
r1 = Router("R1-reachable", "10.0.0.1", "Cisco")
r1.add_interface(Interface("Gig0/0", "up", "10G", True))
r1.add_interface(Interface("Gig0/1", "down", "1G"))
inventory.append(r1)
r2 = Router("R2-unreachable", "10.0.0.2", "Juniper")
r2.add_interface(Interface("ge-0/0/0", "down", "1G", True))
inventory.append(r2)
# Switches
sw1 = Switch("SW1-reachable", "10.0.1.1", "Arista")
sw1.add_interface(Interface("Eth1", "up", "10G", True))
sw1.add_interface(Interface("Eth2", "up", "1G"))
inventory.append(sw1)
# Firewall
fw1 = Firewall("FW1-reachable", "10.0.2.1", "Palo Alto")
fw1.add_interface(Interface("eth0", "down", "1G", True))
fw1.add_interface(Interface("eth1", "down", "1G"))
inventory.append(fw1)
4. Generate the Health Report
from collections import defaultdict
def generate_report(devices):
grouped = defaultdict(list)
for device in devices:
grouped[device.get_type()].append(device)
for dtype, dev_list in grouped.items():
print(f"\n=== {dtype.upper()} ===")
for dev in dev_list:
print(f"\n{dev}")
if not dev.ping():
print(" ⚠️ Ping failed!")
down_ifaces = dev.get_down_interfaces()
if down_ifaces:
print(" 🔻 Down interfaces:")
for iface in down_ifaces:
print(f" - {iface}")
else:
print(" ✅ All interfaces are UP")
5. Run the Report
generate_report(inventory)Expected Output
=== ROUTER ===
R1-reachable (Cisco) - 10.0.0.1
🔻 Down interfaces:
- Gig0/1 [down, 1G, uplink=False]
R2-unreachable (Juniper) - 10.0.0.2
⚠️ Ping failed!
🔻 Down interfaces:
- ge-0/0/0 [down, 1G, uplink=True]
=== SWITCH ===
SW1-reachable (Arista) - 10.0.1.1
✅ All interfaces are UP
=== FIREWALL ===
FW1-reachable (Palo Alto) - 10.0.2.1
🔻 Down interfaces:
- eth0 [down, 1G, uplink=True]
- eth1 [down, 1G, uplink=False]
Why This Exercise Matters
This isn’t just an academic example. This structure enables:
Scalable device inventory modeling
Clear separation of logic per device type
Per-interface tracking and filtering
Ping + status validation before change control
Reusability across tools and scripts
You’ve just built the foundations for an in-memory CMDB and health checker, something you could later integrate with APIs, NMSs, or inventory tools like NetBox.
Try This on Your Own:
Add interface descriptions
Color code output
Store data to JSON
Add VLAN or BGP neighbor logic per device type
Tie this into your real lab’s SNMP or CLI outputs
12. Why You Should Embrace OOP in NetDevOps Workflows
The networks we build and operate today aren’t static collections of devices and commands. They’re dynamic ecosystems of interconnected systems that need to be modeled, reasoned about, and automated with precision. That’s why Object-Oriented Programming (OOP) isn’t just a nice-to-have in the modern NetDevOps toolbox but rather a foundational skill.
Let’s be clear: embracing OOP doesn’t mean you stop being a network engineer. You don’t give up CLI mastery, protocol fluency, or your deep troubleshooting instincts. But it does mean you learn to abstract, scale, and structure your engineering logic in ways that go far beyond brittle scripts and one-off automations.
Why OOP Fits the Networking Mental Model
If you really think about it, OOP maps beautifully to how we conceptualize real networks:
Devices are objects; they have properties (IP, vendor, hostname) and behaviors (ping, push config, check health).
Interfaces are components of devices, with their own attributes and states.
Topologies are relationships between objects; devices connect via links, have neighbors, and form protocols.
States change over time: interface status flaps, routes come and go, policies apply conditionally.
Instead of managing all of this through flat lists, nested dictionaries, or repetitive logic, OOP lets you create logical, reusable, extensible building blocks. You stop thinking in terms of “a bunch of IPs and configs” and start thinking like a systems designer.
That’s when the real magic happens.
OOP Is the Foundation for All Scalable Automation
As your network grows in size, complexity, or criticality, you’ll hit a point where:
Manually iterating through YAML or JSON files isn’t fast enough
Bash or
expectscripts become brittle and unreadablePulling and parsing CLI output feels like duct tape, not engineering
Your logic becomes hard to test, hard to reuse, and hard to trust
But with well-structured Python classes, methods, and models, you can:
Represent your inventory dynamically
Build health checks as object behaviors
Compose objects into topologies
Integrate smoothly with APIs or config renderers
Unit test and reuse logic across dozens of use cases
This is the NetDevOps way: building programmatic views of your infrastructure that empower you to move faster with confidence.
From Scripts to Systems
When you master OOP, your automation work stops being a patchwork of disconnected scripts. Instead, you start building:
Reusable libraries
Policy engines
Dynamic inventory explorers
Intelligent validators
Declarative automation loops
You go from “writing Python that configures BGP” to designing software that understands BGP, interfaces, devices, and the rules of your organization.
That is a massive shift in career trajectory. It’s how you elevate from script-runner to network architect with code skills.
Your Next Step
So now what?
Practice modeling real network elements: Take your lab, a production network snapshot, or your IPAM data and turn it into objects.
Refactor old scripts using OOP: See where classes can replace dicts, where inheritance avoids duplication, and where methods clarify intent.
Start testing your logic: Use dummy inputs, write
__str__()outputs, and simulate results.
And if you’ve followed along this far, congratulations. You’ve crossed a line most network engineers never do: you're now thinking like a developer without losing your engineering soul.
See you all in the next chapter of this series!
Leonardo Furtado

