Skip to content

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. await yields 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; @contextmanager to make your own.
  • asyncio.gather for concurrent I/O; test with pytest-asyncio.
  • Be ready to write cosine, safe-json, rate-limiter, pass-rate from scratch.