Advanced Python Automation for Network Engineers
Dataclasses, Type Hints, and Custom Exceptions for Automation Results
What you'll learn
- How @dataclass replaces an ad hoc result dict with a real type
- How type hints document what a function actually expects and returns
- How a custom exception hierarchy separates operational failures from programming bugs
- That this generalizes Reliable Automation's status strings, not repeats them
Listen to a guided explanation of this lesson.
Spoken narration isn't supported in this browser.
The dict every capstone has used
{"hostname": "SW2", "status": "changed", "detail": "..."} — this exact shape, or something close to it, has appeared in every JSON report since Phase 2.6. It works, but Python enforces nothing about it: a typo in a key name, a missing field, an inconsistent value — none of it is caught until something downstream breaks.
from dataclasses import dataclass, field
from enum import Enum
class Status(Enum):
ALREADY_COMPLIANT = "already-compliant"
CHANGED = "changed"
UNREACHABLE = "unreachable"
AUTH_FAILED = "auth-failed"
VALIDATION_FAILED = "validation-failed"
@dataclass
class Vlan:
id: int
name: str
@dataclass
class Device:
hostname: str
required_vlans: list[Vlan] = field(default_factory=list)
@dataclass
class AutomationResult:
hostname: str
status: Status
detail: str@dataclass generates __init__, __repr__, and equality comparison automatically -- Status is a small Enum, replacing bare status strings with a fixed, checkable set of values.
Real, verified construction
d = Device(hostname="SW1", required_vlans=[Vlan(id=10, name="USERS"), Vlan(id=20, name="VOICE")])
print(d)
r = AutomationResult(hostname="SW1", status=Status.ALREADY_COMPLIANT, detail="no change needed")
print(r)
print(r.status.value)type hints on every field (hostname: str, id: int) document what's expected -- not enforced at runtime by Python itself, but readable by every editor and by the next engineer.
Device(hostname='SW1', required_vlans=[Vlan(id=10, name='USERS'), Vlan(id=20, name='VOICE')])
AutomationResult(hostname='SW1', status=<Status.ALREADY_COMPLIANT: 'already-compliant'>, detail='no change needed')
already-compliantReal, verified output. r.status.value unwraps the Enum back to its plain string, e.g. for writing into a JSON report.
A real exception hierarchy
Reliable Automation classified outcomes with status strings compared by hand (if status == "unreachable"). This lesson gives those same failure categories real Python types.
class AutomationError(Exception):
"""Base class for expected, operational automation failures."""
class DeviceUnreachableError(AutomationError):
"""The device could not be reached at all."""
class AuthenticationError(AutomationError):
"""The device rejected the credentials."""
class ValidationError(AutomationError):
"""The device responded, but its state doesn't match what's required."""A flat hierarchy -- one base class, three direct subclasses. Not deep, not academic: each one names exactly one of the failure categories Reliable Automation already established.
What this hierarchy deliberately does NOT catch
AutomationError and its subclasses represent expected, operational failures -- a device being down is not a bug. A real programming mistake (a KeyError from a typo, a TypeError from a wrong argument) should never be caught as an AutomationError -- it should surface as itself, so it gets fixed instead of silently logged as "the device's fault."
Inventory, upgraded
With real types in place, load_inventory() from Lesson 2 gets one small upgrade — returning Device objects instead of plain dicts:
import csv
from network_automation.models import Device, Vlan
REQUIRED_VLANS = {
"SW1": [Vlan(id=10, name="USERS"), Vlan(id=20, name="VOICE")],
"SW2": [Vlan(id=10, name="USERS")],
"SW3": [Vlan(id=10, name="USERS"), Vlan(id=30, name="MGMT")],
"SW4": [Vlan(id=10, name="USERS")],
"SW5": [Vlan(id=10, name="USERS")],
}
def load_inventory(path):
with open(path) as f:
rows = list(csv.DictReader(f))
devices = []
for row in rows:
hostname = row.get("hostname")
if not hostname:
continue
devices.append(Device(hostname=hostname, required_vlans=REQUIRED_VLANS.get(hostname, [])))
return devicesReal, verified. Same devices.csv, same five hostnames — now returning a list of real Device objects instead of dicts, so a typo'd field name fails immediately instead of silently producing None somewhere downstream.
Key takeaways
- @dataclass generates a working __init__, __repr__, and equality comparison from a field list — verified directly, not assumed magic.
- A small Status Enum replaces bare status strings with a fixed, checkable set of values.
- A flat exception hierarchy (AutomationError → DeviceUnreachableError/AuthenticationError/ValidationError) gives Reliable Automation's failure categories real types instead of compared strings.
- The hierarchy deliberately does not catch real bugs (KeyError, TypeError) — only the named operational failure categories.
- load_inventory() now returns real Device objects — the same devices.csv, a stricter, self-checking result shape.