Six short live-coding drills from the network-automation domain: pure functions, stdlib only, each solvable in 8–10 minutes. Statement, approach, reference solution, and the pitfalls that sink people in a codepad.
These are the classic "write it in a plain editor, no interpreter, no autocomplete" warm-ups. Each one hides one or two off-by-one / lost-tail / ordering traps that only show up when you can't run the code. The reference solutions below are the canonical ones from the drill set—study the Pitfalls boxes as much as the code.
Problem. Convert between a compact range string and an explicit list of integers.
expand_ranges("10,20,30-32") → [10, 20, 30, 31, 32]
"", " , ") → []."7-7" → [7]."10-2") → ValueError.compress_ranges([10, 11, 12, 15]) → "10-12,15"
[1,2,4] → "1-2,4".[] → "".compress_ranges(expand_ranges(s)) == s for a canonical string.For expansion, split on commas, strip and skip blanks, and for each chunk either parse a single int or split a lo-hi pair on the first - and add range(lo, hi+1); collect into a set, then sorted(). For compression, sort the unique values and sweep once, extending a run while the next value is prev+1 and flushing a start/start-prev token otherwise. Time O(n log n) for the sort (plus output size for expansion); space O(n).
def expand_ranges(spec: str) -> list[int]:
out: set[int] = set()
for chunk in spec.split(","):
chunk = chunk.strip()
if not chunk:
continue
if "-" in chunk:
lo, _, hi = chunk.partition("-")
try:
start, end = int(lo), int(hi)
except ValueError:
raise ValueError(f"bad range: {chunk}")
if start > end:
raise ValueError(f"reversed range: {chunk}")
out.update(range(start, end + 1))
else:
try:
out.add(int(chunk))
except ValueError:
raise ValueError(f"bad number: {chunk}")
return sorted(out)
def compress_ranges(nums: list[int]) -> str:
values = sorted(set(nums))
if not values:
return ""
parts: list[str] = []
start = prev = values[0]
for n in values[1:]:
if n == prev + 1:
prev = n
continue
parts.append(str(start) if start == prev else f"{start}-{prev}")
start = prev = n
parts.append(str(start) if start == prev else f"{start}-{prev}")
return ",".join(parts)
range(): range(start, end) instead of range(start, end + 1) drops the last VLAN.compress must append the last token once more after the for.Problem. Canonicalize MAC addresses from any vendor format and read the address-type bits.
normalize_mac → "aa:bb:cc:dd:ee:ff" (lower case, colon-separated).
"AA:BB:CC:DD:EE:FF", "aabb.ccdd.eeff", "aa-bb-cc-dd-ee-ff", "AABBCCDDEEFF", and forms with spaces.ValueError.to_cisco → "aabb.ccdd.eeff".
is_multicast(mac) → the least-significant bit of the first octet (the I/G bit). 01:00:5e:... → True, ff:ff:... → True, 00:11:... → False.
is_locally_administered(mac) → the second-least-significant bit of the first octet (the U/L bit). 02:00:... → True, aa:bb:... → True, 00:11:... → False.
Talking point: why format normalization is the first thing you do when comparing data from different vendors.
Strip all separators (: . - and whitespace), lower-case, and validate against a strict 12-hex-digit regex—a single _digits helper does this for every entry point. Reformat by slicing the clean digit string into groups (2s for colon form, 4s for Cisco). The I/G and U/L flags are just bit masks (& 0b1 and & 0b10) on the first octet parsed with int(x, 16). Time and space O(1) (fixed 12-char input).
import re
HEX = re.compile(r"^[0-9a-f]{12}$")
def _digits(mac: str) -> str:
cleaned = re.sub(r"[\s:.\-]", "", mac).lower()
if not HEX.match(cleaned):
raise ValueError(f"invalid MAC: {mac}")
return cleaned
def normalize_mac(mac: str) -> str:
d = _digits(mac)
return ":".join(d[i : i + 2] for i in range(0, 12, 2))
def to_cisco(mac: str) -> str:
d = _digits(mac)
return ".".join(d[i : i + 4] for i in range(0, 12, 4))
def is_multicast(mac: str) -> bool:
return bool(int(_digits(mac)[:2], 16) & 0b1)
def is_locally_administered(mac: str) -> bool:
return bool(int(_digits(mac)[:2], 16) & 0b10)
& 0b1), U/L is bit 1 (& 0b10) of the first octet—easy to swap them or read the wrong octet.str vs int: comparing the parsed octet must use int(x, 16); masking the string does nothing.except:: catch the specific exception, not everything.Problem. Compute interface-counter differences that survive counter wrap, then convert to rates and rank talkers.
delta(prev, curr, width=64) → int:
curr >= prev → the plain difference.curr < prev and width == 32 → the counter wrapped: 2**32 - prev + curr.curr < prev and width == 64 → treat it as a counter reset → 0.width not 32/64, or negative values → ValueError.rate_bps(prev, curr, dt, width=64) → float (bytes → bits per second); dt <= 0 → ValueError.
top_talkers(samples, dt, n) → list[(iface, bps)], where samples is {"Et1": (prev_bytes, curr_bytes), ...}. Sort by descending rate, breaking ties by interface name.
Likely follow-up: why 32-bit SNMP counters are painful at 10G+ and how 64-bit counters / telemetry fix it.
delta is a guard-clause cascade: validate width and non-negativity first, then the normal case, then the 32-bit wrap formula, then the 64-bit reset case. rate_bps reuses delta and multiplies by 8 over dt. top_talkers maps every sample to a (iface, rate) pair and sorts by the key (-rate, iface) so ties fall back to name order, then slices the first n. Time O(m log m) for m interfaces; space O(m).
def delta(prev: int, curr: int, width: int = 64) -> int:
if width not in (32, 64):
raise ValueError("width must be 32 or 64")
if prev < 0 or curr < 0:
raise ValueError("counters must be non-negative")
if curr >= prev:
return curr - prev
if width == 32:
return (1 << 32) - prev + curr
return 0
def rate_bps(prev: int, curr: int, dt: float, width: int = 64) -> float:
if dt <= 0:
raise ValueError("dt must be positive")
return delta(prev, curr, width) * 8 / dt
def top_talkers(
samples: dict[str, tuple[int, int]], dt: float, n: int
) -> list[tuple[str, float]]:
rates = [(iface, rate_bps(p, c, dt)) for iface, (p, c) in samples.items()]
rates.sort(key=lambda kv: (-kv[1], kv[0]))
return rates[:n]
* 8 / dt, not //.dict iteration must be turned into a stable sort—the tie-break on interface name ((-rate, iface)) is what the test expects.return in one of the branches—every path in delta must return.Problem. Plan point-to-point addressing. The ipaddress module is allowed.
allocate_p2p(supernet, count) → list[str]: carve supernet into /31s for p2p links and return the first count.
("10.0.0.0/29", 3) → ["10.0.0.0/31", "10.0.0.2/31", "10.0.0.4/31"].count == 0 → []; not enough space → ValueError; a supernet smaller than /31 → ValueError.next_free(supernet, used, prefixlen) → str | None: the first free /prefixlen block inside supernet that doesn't overlap any entry in used. None free → None. A prefixlen outside [supernet.prefixlen, 32] → ValueError.
Follow-up: why /31 on p2p rather than /30 (RFC 3021)? How would you do this in an IPAM with concurrent requests (locking / reservation)?
allocate_p2p validates the count and that the supernet is at least a /31, then pulls from the lazy net.subnets(new_prefix=31) generator count times, raising if it's exhausted early. next_free validates the prefix length against the supernet range, materializes the used blocks, and returns the first candidate /prefixlen that overlaps none of them—a linear scan. Time O(k) for allocate_p2p and O(b · u) for next_free (blocks × used); space O(u).
import ipaddress
def allocate_p2p(supernet: str, count: int) -> list[str]:
net = ipaddress.ip_network(supernet, strict=True)
if count < 0:
raise ValueError("count must be non-negative")
if net.prefixlen > 31:
raise ValueError("supernet too small")
subnets = net.subnets(new_prefix=31)
out: list[str] = []
for _ in range(count):
try:
out.append(str(next(subnets)))
except StopIteration:
raise ValueError("not enough space in supernet")
return out
def next_free(supernet: str, used: list[str], prefixlen: int) -> str | None:
net = ipaddress.ip_network(supernet, strict=True)
if prefixlen < net.prefixlen or prefixlen > net.max_prefixlen:
raise ValueError("invalid prefixlen")
taken = [ipaddress.ip_network(u, strict=False) for u in used]
for candidate in net.subnets(new_prefix=prefixlen):
if not any(candidate.overlaps(t) for t in taken):
return str(candidate)
return None
[supernet.prefixlen, 32]—guard both ends before iterating, and a supernet finer than /31 can't be split for p2p.subnets() generator lazily (via next()) avoids building every /31 just to take a few—an efficiency trade-off worth naming aloud.candidate.overlaps(t); the used blocks may be a different prefix length than the candidate.used / count == 0: the empty-input edge cases must return [] / the first block cleanly.Problem. Parse a key=value config string into a dict.
parse_kv("a=1;b=2") → {"a": "1", "b": "2"}
"a=1;;b=2;" → {"a":"1","b":"2"}." a = 1 " → {"a": "1"}."a=" → {"a": ""}.=: "a=b=c" → {"a": "b=c"} (split on = once)."a=1;a=2" → {"a": "2"}.= → ValueError: "abc".ValueError: "=1", " =1".Split on ;, strip each segment, and skip blanks (edges and ;; produce them). For each remaining segment require a =, split it once with split("=", 1) so the value keeps any further =, strip both sides, reject an empty key, and assign into the dict so a later duplicate overwrites. Time O(n) in the input length; space O(k) for k keys.
def parse_kv(s: str) -> dict[str, str]:
ret: dict[str, str] = {}
for seg in s.split(";"):
seg = seg.strip()
if seg == "": # пустые куски (края, ";;") — пропускаем
continue
if "=" not in seg: # нет разделителя — это ошибка, а не пустой сегмент
raise ValueError(f"segment without '=': {seg!r}")
key, val = seg.split("=", 1) # limit=1: значение может содержать '='
key = key.strip()
val = val.strip()
if key == "": # пустой ключ недопустим (пустое значение — можно)
raise ValueError(f"empty key in: {seg!r}")
ret[key] = val # дубликат: последний побеждает
return ret
split(";"): edges and ;; yield empty pieces—skip them, don't crash on them.split("=") without limit=1: it shreds "a=b=c" into three parts—use split("=", 1)."a=" is fine), but a missing = or an empty key must raise—don't conflate them.except: and a forgotten return at the end are the usual slips.Problem. Merge overlapping intervals.
merge_intervals([(1,3),(2,6),(8,10),(15,18)]) → [(1,6),(8,10),(15,18)]
[] → [].lo <= last_hi):
[(1,5),(5,9)] → [(1,9)] (shared point 5 → merge).[(1,2),(3,4)] → [(1,2),(3,4)] (gap → no merge).(5,5) is valid.(5,3) where lo > hi → ValueError.First pass validates every interval (lo > hi raises) before any sorting, so a reversed interval can't slip through. Then sort, seed the result with the first interval, and for each subsequent one either extend the last result's high end when lo <= last_hi, or append a fresh interval on a gap. Time O(n log n) for the sort; space O(n) for the output.
def merge_intervals(intervals: list[tuple[int, int]]) -> list[tuple[int, int]]:
norm: list[tuple[int, int]] = []
for lo, hi in intervals:
if lo > hi: # валидируем ДО сортировки/слияния
raise ValueError(f"inverted interval: {(lo, hi)}")
norm.append((lo, hi))
if not norm:
return []
norm.sort() # вход мог быть неотсортирован
merged = [norm[0]]
for lo, hi in norm[1:]:
last_lo, last_hi = merged[-1]
if lo <= last_hi: # перекрытие или соприкосновение концов
merged[-1] = (last_lo, max(last_hi, hi))
else:
merged.append((lo, hi)) # разрыв — новый интервал
return merged
merged[-1] in place it closes the final interval itself—but verify against [(1,2)] and a lone tail element.<= vs <: touching endpoints must merge (lo <= last_hi), so [(1,5),(5,9)] becomes one interval—using < would wrongly split it.lo > hi up front, otherwise an inverted interval sorts in silently and corrupts the merge.