Advanced Python Automation for Network Engineers
Concurrent Device Operations with ThreadPoolExecutor
What you'll learn
- Why every capstone through Phase 3.2 has run sequentially
- How ThreadPoolExecutor runs bounded, concurrent device operations
- A real, measured sequential-vs-concurrent timing comparison
- Why asyncio is deferred, and why 20 devices (not 5) are used here
Listen to a guided explanation of this lesson.
Spoken narration isn't supported in this browser.
Every capstone so far has been sequential
[check_device(device) for device in devices] — a plain loop, one device after another. Fine for 5 devices. The cost is real once a fleet is large enough that each device's network round-trip adds up.
A deliberate exception to this curriculum's fleet size
Every capstone since Phase 2.6 has used the same 5 devices (SW1-SW5) — 5 devices can't produce a measurable timing difference between sequential and concurrent execution. This lesson uses a separate, flat, generated 20-device fleet (SIM1-SIM20) specifically to make the comparison real and visible. It's not a new simulation framework — just enough synthetic hostnames to measure a real effect.
Bounded concurrency, for real
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
SIM_HOSTNAMES = [f"SIM{i}" for i in range(1, 21)]
def get_one_state(conn, hostname):
return conn.get_state(hostname)
# Sequential
start = time.perf_counter()
with Connection(BASE_URL, token="demo-token") as conn:
sequential_results = [get_one_state(conn, h) for h in SIM_HOSTNAMES]
sequential_elapsed = time.perf_counter() - start
# Bounded concurrent -- at most 5 requests in flight at once
start = time.perf_counter()
with Connection(BASE_URL, token="demo-token") as conn:
with ThreadPoolExecutor(max_workers=5) as executor:
futures = {executor.submit(get_one_state, conn, h): h for h in SIM_HOSTNAMES}
concurrent_results = [future.result() for future in as_completed(futures)]
concurrent_elapsed = time.perf_counter() - startmax_workers=5 bounds concurrency deliberately -- not unlimited. as_completed() yields each future as it finishes, in completion order, not submission order.
Sequential elapsed: 6.30s for 20 devices
Concurrent elapsed: 1.26s for 20 devices
Speedup: 5.01xReal numbers from an actual run against this module's mock backend (each request carries a small, realistic simulated delay). Not a documented assumption — this exact comparison was executed and timed.
Partial failures don't block the rest
with Connection(BASE_URL, token="demo-token") as conn:
with ThreadPoolExecutor(max_workers=5) as executor:
futures = {executor.submit(check_device, conn, device): device for device in devices}
for future in as_completed(futures):
device = futures[future]
result = future.result() # check_device() already returns a result, never raises
print(f" {device.hostname} finished -> {result.status.value}")check_device() (Lessons 3-5) already converts every exception into an AutomationResult -- so future.result() here never itself raises. A slow or failing device shows up as a result, not a crash.
SW1 finished -> already-compliant
SW2 finished -> validation-failed
SW5 finished -> auth-failed
SW3 finished -> validation-failed
SW4 finished -> unreachable
All 5 devices finished in 7.06s (concurrent, max_workers=5)Real, verified output. Notice the order: SW1, SW2, and SW5 finish almost immediately; SW3 takes slightly longer (its real retry from Lesson 4); SW4 -- genuinely unreachable -- takes the longest, but never blocks the other four from finishing and being collected.
Why not asyncio
A deliberate deferral, not an oversight
asyncio would require rewriting Connection to be async-native — every method, the session, the retry decorator — a much larger rewrite than this module's teaching goal justifies. ThreadPoolExecutor gets the same practical benefit (bounded concurrent network calls) with zero changes to the Connection class already built in Lesson 4. If this curriculum ever needs true high-scale async automation, that's a future module's territory, not a retrofit here.
Key takeaways
- Every capstone through Phase 3.2 ran sequentially — fine at 5 devices, a real cost as fleet size grows.
- ThreadPoolExecutor(max_workers=5) bounds concurrency deliberately; as_completed() yields results as they finish, not in submission order.
- Real, measured result: 6.30s sequential vs 1.26s concurrent for 20 devices — a 5.01x speedup, actually executed, not assumed.
- A genuinely slow or failing device (SW4) never blocks collecting the others' results — verified directly against the real 5-device fleet.
- asyncio is deliberately deferred: ThreadPoolExecutor delivers the same practical benefit without rewriting Connection to be async-native.