Advanced Lab
Refactoring a Flaky Connection Script
Objective
Apply dataclasses, a custom exception, and a bounded retry wrapper to a fresh flaky-connection scenario, producing a structured, typed result instead of a print statement.
Prerequisites
- Comfortable with @dataclass (Dataclasses, Type Hints, and Custom Exceptions)
- Comfortable with try/except and raising a custom exception class
Scenario
The problem
Dataclasses, Type Hints, and Custom Exceptions taught you the pieces -- a typed result instead of an ad-hoc dict, a custom exception instead of a bare except:. This lab hands you a genuinely flaky function (not the lesson's own example) and asks you to apply those same pieces from scratch: does it succeed, and if so after how many attempts? If it never succeeds, what actually went wrong?
Starter data
# Deterministic flakiness: each hostname fails this many times before
# succeeding (or never, if the count is >= max_attempts).
FAIL_COUNTS = {"SW1": 2, "SW2": 0, "SW3": 5}
_call_counts = {}
def flaky_connect(hostname):
_call_counts[hostname] = _call_counts.get(hostname, 0) + 1
attempt = _call_counts[hostname]
if attempt <= FAIL_COUNTS.get(hostname, 0):
raise Exception(f"simulated failure on attempt {attempt}")
return f"connected to {hostname}"Fixed, deterministic sample data written for this lab -- SW1 fails twice then succeeds, SW2 always succeeds immediately, SW3 fails more times than any reasonable retry budget allows.
Task
flaky_connect() currently raises a bare Exception — replace that with a real custom exception class, DeviceConnectionError.
Write a @dataclass called ConnectionResult with fields: hostname: str, success: bool, attempts: int, error: Optional[str] = None.
Write connect_with_retry(hostname, max_attempts=3) that calls flaky_connect() up to max_attempts times, catching DeviceConnectionError each time. Return a ConnectionResult as soon as it succeeds (recording how many attempts it took); if every attempt fails, return a ConnectionResult with success=False and error set to the last failure's message.
Expected output
ConnectionResult(hostname='SW1', success=True, attempts=3, error=None)
ConnectionResult(hostname='SW2', success=True, attempts=1, error=None)
ConnectionResult(hostname='SW3', success=False, attempts=3, error='simulated failure on attempt 3')Real, verified output with max_attempts=3. SW1 needed all 3 attempts to succeed; SW2 succeeded immediately; SW3 exhausted every attempt and reports the last failure's exact message.
Self-check only -- nothing here is submitted or graded. Compare against your own output manually.
Solution
Try the task yourself first -- the full solution and explanation stay hidden until you click above.
Key takeaways
- A custom exception class, even an empty one, makes the difference between 'catch and handle this specific failure' and 'catch anything and hope.'
- A dataclass result (hostname, success, attempts, error) survives being logged, compared, and passed around in a way a bare print statement never could.
- connect_with_retry() returns immediately on the first success -- attempts only grows as high as it actually needed to.
- flaky_connect() is a deterministic simulation -- no real device or network call was used or required.
Related lessons
Dataclasses, Type Hints, and Custom Exceptions for Automation Results
The exact dataclass-result and custom-exception patterns this lab applies to a new flaky-connection scenario.
Read lessonClasses for Connections and Devices: Composition, Context Managers, and a Retry Decorator
The bounded-retry reasoning this lab's connect_with_retry() function reimplements as a plain function.
Read lessonDone with this lesson?