Advanced Python Automation for Network Engineers
Classes for Connections and Devices: Composition, Context Managers, and a Retry Decorator
What you'll learn
- When a class earns its place over a plain function or dataclass
- How a context manager guarantees cleanup even when something fails
- How to turn a retry loop into a reusable decorator
- That this reuses Reliable Automation's exact retry policy, not a new one
Listen to a guided explanation of this lesson.
Spoken narration isn't supported in this browser.
When a class earns its place
A dataclass holds data. A class earns its place when something needs to hold state and behavior together — a connection is a good example: it has a session that needs to be created, used across multiple calls, and reliably closed.
import requests
from network_automation.exceptions import AuthenticationError, DeviceUnreachableError
class Connection:
def __init__(self, base_url, token):
self.base_url = base_url
self.token = token
self._session = None
def __enter__(self):
self._session = requests.Session()
self._session.headers["Authorization"] = f"Bearer {self.token}"
return self
def __exit__(self, exc_type, exc_value, traceback):
if self._session is not None:
self._session.close()
return False # never swallow an exception
def get_state(self, hostname, timeout=2):
try:
response = self._session.get(f"{self.base_url}/devices/{hostname}/vlans", timeout=timeout)
except requests.exceptions.ConnectionError:
raise DeviceUnreachableError(f"{hostname} did not respond")
except requests.exceptions.Timeout:
raise DeviceUnreachableError(f"{hostname} timed out")
if response.status_code == 401:
raise AuthenticationError(f"{hostname} rejected the credentials")
if response.status_code == 503:
raise DeviceUnreachableError(f"{hostname} is temporarily unavailable")
response.raise_for_status()
return response.json()["vlans"]__enter__/__exit__ make this usable with Python's with statement. get_state() translates requests' own exceptions and status codes into this module's exception hierarchy from Lesson 3 -- one place where that translation happens, not repeated per call site.
Cleanup that actually happens
with Connection("http://127.0.0.1:8767", token="demo-token") as conn:
vlans = conn.get_state("SW1")
print("SW1 vlans:", vlans)with Connection(...) as conn: calls __enter__ on the way in and __exit__ on the way out -- guaranteed, even if get_state() raises. Real, verified output.
SW1 vlans: [{'id': 10, 'name': 'USERS'}, {'id': 20, 'name': 'VOICE'}]Real, verified output against this module's mock backend.
The retry loop, as a decorator
Reliable Automation wrote a bounded, selective retry loop by hand. Here it is, expressed once, reusable anywhere:
import functools
import time
def retry(max_attempts=3, delay=2):
def decorator(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
for attempt in range(1, max_attempts + 1):
try:
return func(*args, **kwargs)
except DeviceUnreachableError:
if attempt == max_attempts:
raise
time.sleep(delay)
return wrapper
return decorator
@retry(max_attempts=3, delay=0.5)
def get_state_with_retry(connection, hostname):
return connection.get_state(hostname)The exact same policy Reliable Automation taught -- bounded attempts, retry only on DeviceUnreachableError, never on AuthenticationError -- now written once and applied with @retry(...) instead of copy-pasted into every function that needs it.
Same policy, reused -- not a new one
This is not new retry logic. It's Reliable Automation's exact policy (bounded, selective, exponential-backoff-free) expressed as a decorator instead of an inline loop, so any function can opt in with one line.
with Connection("http://127.0.0.1:8767", token="demo-token") as conn:
vlans = get_state_with_retry(conn, "SW3")
print("SW3 vlans (after retry):", vlans)SW3's mock backend entry deliberately fails once (a real 503) before succeeding -- a genuine transient failure, not simulated in the lesson code.
SW3 vlans (after retry): [{'id': 10, 'name': 'USERS'}]Real, verified output — the decorator genuinely retried once and succeeded on the second attempt.
Key takeaways
- A class earns its place when state and behavior genuinely belong together — Connection's session is a real example, not an academic one.
- __enter__/__exit__ make with Connection(...) as conn: guarantee cleanup, exception or not — verified directly.
- @retry(...) expresses Reliable Automation's exact bounded, selective retry policy once, reusable on any function with one line — verified against a real transient failure (SW3) and a real unbounded-failure case (SW4).
- None of this is new retry logic — it's the same policy, refactored for reuse.