04 β Python for Test Scripting¶
JD: "Python for test scripting, strongly preferred given the stack." Expect a live coding screen. This is the Python testers actually get grilled on β not tricky algorithms, but idioms, async, typing, and the small utilities eval/agent testing needs.
1. Data structures & when to use them¶
| Structure | Use | Key ops |
|---|---|---|
list |
ordered, duplicates | append, slicing, comprehension |
dict |
keyβvalue, O(1) lookup | .get(k, default), .items(), merge {**a, **b} |
set |
uniqueness, membership | & \| -, O(1) in |
tuple |
fixed, hashable | dict keys, returns |
collections.Counter |
frequency | Counter(items).most_common(3) |
collections.defaultdict |
grouping | d[k].append(v) without init |
# group test results by status
from collections import defaultdict
by_status = defaultdict(list)
for t in results:
by_status[t["status"]].append(t["name"])
# dedupe while preserving order
seen, out = set(), []
for x in items:
if x not in seen:
seen.add(x); out.append(x)
2. Comprehensions & generators¶
squares = [x*x for x in range(10) if x % 2 == 0]
lookup = {u["id"]: u for u in users}
names = {u["name"] for u in users}
# generator: lazy, memory-friendly for big files/streams
total = sum(len(line) for line in open("huge.log"))
yield makes a generator β essential for streaming/eval over large datasets without loading all into memory.
3. Functions, *args/**kwargs, defaults trap¶
def call(url, *args, retries=3, **headers): ...
# CLASSIC TRAP: mutable default is shared across calls
def bad(x, acc=[]): # acc persists between calls!
acc.append(x); return acc
def good(x, acc=None):
acc = acc if acc is not None else []
acc.append(x); return acc
4. Decorators (fixtures, retries, timing all use them)¶
In plain words: a decorator wraps a function to add behaviour without changing its body.
import functools, time
def retry(times=3, delay=0.5):
def deco(fn):
@functools.wraps(fn)
def wrapper(*a, **k):
for i in range(times):
try:
return fn(*a, **k)
except Exception:
if i == times - 1: raise
time.sleep(delay * (2 ** i)) # exponential backoff
return wrapper
return deco
@retry(times=3)
def flaky_call(): ...
@functools.wraps preserves the wrapped function's name/docstring β mention it.
5. Context managers (with)¶
In plain words: guarantees setup/cleanup even on exceptions β the pattern behind file handles, DB connections, HTTP clients.
from contextlib import contextmanager
@contextmanager
def temp_user(db):
u = db.create_user()
try:
yield u
finally:
db.delete_user(u.id) # always cleans up
with temp_user(db) as u:
...
6. Async / await (agent code is async)¶
In plain words: async lets one thread handle many waiting I/O operations concurrently.
awaityields control while waiting on network/DB.
import asyncio, httpx
async def fetch(client, url):
r = await client.get(url)
return r.status_code
async def main(urls):
async with httpx.AsyncClient() as client:
results = await asyncio.gather(*(fetch(client, u) for u in urls))
return results
asyncio.gather(*tasks)runs them concurrently.- Don't call blocking code inside async (blocks the loop) β use async libs or
run_in_executor. - Testing async:
pytest-asyncio(@pytest.mark.asyncio).
7. Typing (senior code is typed)¶
from typing import Optional, Union
def score(preds: list[float], truth: list[float]) -> dict[str, float]:
...
def find(id: str) -> Optional["User"]: # may return None
...
Typing + Pydantic + mypy = the professional stack. Interviewers notice type hints.
8. Utilities the AI/eval role needs (be able to write these live)¶
Cosine similarity (semantic assertions)¶
import math
def cosine(a, b):
dot = sum(x*y for x, y in zip(a, b))
na = math.sqrt(sum(x*x for x in a))
nb = math.sqrt(sum(y*y for y in b))
return dot / (na * nb) if na and nb else 0.0
Parse-JSON-with-retry (LLMs return messy JSON)¶
import json, re
def safe_json(text):
try:
return json.loads(text)
except json.JSONDecodeError:
m = re.search(r"\{.*\}", text, re.DOTALL) # strip prose/markdown fences
if m: return json.loads(m.group())
raise
Token-bucket rate limiter (calling LLM APIs politely)¶
import time
class RateLimiter:
def __init__(self, rate_per_sec):
self.min_interval = 1.0 / rate_per_sec
self.last = 0.0
def wait(self):
now = time.monotonic()
sleep = self.min_interval - (now - self.last)
if sleep > 0: time.sleep(sleep)
self.last = time.monotonic()
Simple pass-rate over a golden set¶
def pass_rate(cases, predict, judge):
passed = sum(1 for c in cases if judge(predict(c["input"]), c["expected"]))
return passed / len(cases)
9. Errors, files, and the standard library¶
try:
risky()
except (ValueError, KeyError) as e:
log.warning("expected: %s", e)
except Exception:
log.exception("unexpected") # includes traceback
raise
finally:
cleanup()
# read JSONL (common eval dataset format)
import json
with open("dataset.jsonl") as f:
cases = [json.loads(line) for line in f]
Know: pathlib.Path, os.environ.get, datetime, logging, dataclasses, enum.
10. Must-know quick algorithms (if they DSA you)¶
- Reverse/anagram/palindrome strings.
- Two-sum with a dict (O(n)).
- Frequency count with
Counter. - Dedupe preserving order.
- Flatten nested list (recursion).
- Merge two dicts / group-by.
These are the realistic level for a QA automation screen β clean, tested, typed beats clever.
Rapid-fire recall¶
- Mutable-default trap β use
None. - Decorator = wrap to add behaviour;
functools.wraps. with= guaranteed cleanup;@contextmanagerto make your own.asyncio.gatherfor concurrent I/O; test withpytest-asyncio.- Be ready to write cosine, safe-json, rate-limiter, pass-rate from scratch.