On this page8 sections
CommandsIntermediate6-8 min reference

Python for Testers

The Python features you'll reach for in pytest suites, API tests with requests, and Selenium scripts.

Data Types & Variables

Strings

name = "QA Engineer"
 
# f-strings — modern, fast, recommended
greeting = f"Hello, {name}! You have {len(name)} chars."
filename = f"report-{date:%Y-%m-%d}.html"
 
# .format() — older but still common
"Hello, {}! Today is {}".format(name, "Monday")
"{0} costs {1:.2f}".format("Item", 9.5)         # 'Item costs 9.50'
 
# Slicing
name[0]            # 'Q'
name[-1]           # 'r'
name[:2]           # 'QA'
name[3:]           # 'Engineer'
name[::-1]         # 'reenignE AQ'   reverse
 
# Methods
name.strip()                  # remove leading/trailing whitespace
name.split(" ")               # ['QA', 'Engineer']
", ".join(["a", "b", "c"])    # 'a, b, c'
name.replace("QA", "Test")
name.startswith("QA")
name.endswith("eer")
name.upper()
name.lower()
name.title()                  # 'Qa Engineer'
"  pad  ".center(20, "-")

Numbers

count = 42                # int
price = 9.99              # float
imag  = 1 + 2j            # complex
 
int("42")                 # 42
float("3.14")
str(42)
bool(0)                   # False — also: '', [], {}, None, 0.0
bool("anything")          # True

Booleans, None, and truthiness

True, False               # capitalised
None                      # singleton — never compare with ==
 
x = None
if x is None: ...         # ✓
if x is not None: ...     # ✓
 
# Falsy values: False, 0, 0.0, '', [], {}, set(), None
if not response.json():   # body is empty / falsy
    raise AssertionError("empty body")

Type hints

name: str = "QA"
count: int = 0
prices: list[float] = []                 # Python 3.9+
config: dict[str, str] = {}
maybe_id: int | None = None              # Python 3.10+
 
def greet(name: str, greeting: str = "Hello") -> str:
    return f"{greeting}, {name}"

Hints don't enforce at runtime — use mypy or pyright for static checking.

Collections

Lists

items = ["a", "b", "c"]
 
items.append("d")             # ['a','b','c','d']
items.extend(["e", "f"])
items.insert(0, "z")          # ['z','a',…]
items.remove("a")             # remove first 'a'
last = items.pop()            # remove and return
items.sort()
items.sort(key=len, reverse=True)
items.reverse()
 
len(items)
"a" in items
items.index("c")
items.count("a")
 
# Slicing
items[1:3]
items[:2]
items[-1]
 
# List comprehension
squares    = [x * x for x in range(10)]
evens      = [x for x in nums if x % 2 == 0]
flattened  = [x for row in matrix for x in row]

Tuples

point = (10, 20)              # immutable
x, y = point                  # unpacking
first, *rest = (1, 2, 3, 4)   # first=1, rest=[2,3,4]
 
# namedtuple — typed-tuple feel without dataclass weight
from collections import namedtuple
User = namedtuple("User", ["id", "name", "email"])
u = User(42, "Ada", "ada@example.com")
print(u.name)                 # 'Ada'

Dictionaries

user = {"name": "Ada", "age": 36, "active": True}
 
user["name"]
user.get("missing", "default")     # safe lookup
user.keys()
user.values()
user.items()
user.update({"age": 37, "email": "ada@example.com"})
user.pop("age")
"name" in user
 
# Dict comprehension
upper = {k.upper(): v for k, v in user.items()}
counts = {x: items.count(x) for x in set(items)}
 
# defaultdict — auto-init missing keys
from collections import defaultdict
groups = defaultdict(list)
for u in users:
    groups[u.role].append(u)

Sets

tags = {"smoke", "regression"}
tags.add("e2e")
tags.discard("smoke")        # no error if absent
tags.remove("e2e")           # KeyError if absent
 
a = {1, 2, 3}
b = {3, 4, 5}
a | b                        # union — {1,2,3,4,5}
a & b                        # intersection — {3}
a - b                        # difference — {1,2}
a ^ b                        # symmetric_difference — {1,2,4,5}
 
# Dedupe a list
unique = list(set(items))

Iterating

for u in users:
    print(u.name)
 
for i, u in enumerate(users):
    print(i, u.name)
 
for u, role in zip(users, roles):
    print(u.name, role)
 
for i in range(10):
    print(i)
 
for i in range(0, 100, 10):
    print(i)                  # 0, 10, 20, ..., 90

Functions & Classes

Functions

def greet(name: str, greeting: str = "Hello") -> str:
    return f"{greeting}, {name}"
 
def sum_all(*nums: int) -> int:
    return sum(nums)
 
def make_user(**kwargs) -> dict:
    return {"role": "user", **kwargs}
 
# *args + **kwargs
def log(*args, level: str = "info", **fields):
    print(level, args, fields)
 
# Lambda
square = lambda x: x * x
sorted(users, key=lambda u: u.age)
list(filter(lambda u: u.active, users))

Decorators

import functools
 
# Built-ins
class User:
    @staticmethod
    def is_valid(email: str) -> bool: ...
 
    @classmethod
    def from_dict(cls, data: dict) -> "User":
        return cls(**data)
 
    @property
    def display_name(self) -> str:
        return f"{self.first} {self.last}"
 
# Custom — timing decorator
def timed(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        import time
        t0 = time.perf_counter()
        result = fn(*args, **kwargs)
        print(f"{fn.__name__} took {time.perf_counter() - t0:.3f}s")
        return result
    return wrapper
 
@timed
def slow_thing(): ...

Classes

class BasePage:
    def __init__(self, driver, url: str = "/"):
        self.driver = driver
        self.url = url
 
    def open(self) -> "BasePage":
        self.driver.get(self.url)
        return self
 
class LoginPage(BasePage):
    def __init__(self, driver):
        super().__init__(driver, url="/login")
 
    def login_as(self, email: str, password: str) -> None:
        self.driver.find_element("id", "email").send_keys(email)
        self.driver.find_element("id", "password").send_keys(password)
        self.driver.find_element("id", "submit").click()

Dataclasses — clean test data models

from dataclasses import dataclass, field
 
@dataclass
class User:
    name: str
    email: str
    age: int = 0
    roles: list[str] = field(default_factory=list)
 
u = User(name="Ada", email="ada@example.com")
u.roles.append("admin")
print(u)  # User(name='Ada', email='ada@example.com', age=0, roles=['admin'])
 
@dataclass(frozen=True)            # immutable
class Coordinates:
    lat: float
    lng: float

Abstract base classes (for page objects)

from abc import ABC, abstractmethod
 
class Page(ABC):
    @property
    @abstractmethod
    def path(self) -> str: ...
 
    def open(self): ...
 
class CheckoutPage(Page):
    path = "/checkout"

File & Data Handling

Reading and writing

from pathlib import Path
 
# pathlib — modern path handling
config_path = Path("./config/test.yml")
content = config_path.read_text(encoding="utf-8")
config_path.write_text("hello", encoding="utf-8")
 
# Open with context manager
with open("test.log", "r", encoding="utf-8") as f:
    for line in f:
        if "ERROR" in line:
            print(line.strip())
 
# Append
with open("audit.log", "a") as f:
    f.write(f"{user} logged in\n")

JSON

import json
 
# String ↔ object
data = json.loads('{"name": "Ada"}')
text = json.dumps(data, indent=2)
 
# File ↔ object
with open("config.json") as f:
    config = json.load(f)
 
with open("out.json", "w") as f:
    json.dump(data, f, indent=2)

CSV

import csv
 
# As list of dicts (header → row)
with open("users.csv", newline="") as f:
    for row in csv.DictReader(f):
        print(row["email"])
 
# As list of lists
with open("users.csv", newline="") as f:
    rows = list(csv.reader(f))
 
# Writing
with open("out.csv", "w", newline="") as f:
    writer = csv.DictWriter(f, fieldnames=["name", "email"])
    writer.writeheader()
    writer.writerows([{"name": "Ada", "email": "ada@example.com"}])

YAML

import yaml                          # pip install pyyaml
 
with open("config.yml") as f:
    config = yaml.safe_load(f)        # always safe_load with untrusted input
 
yaml.dump(config, sort_keys=False)

Environment variables and .env

import os
 
base_url = os.environ.get("BASE_URL", "https://staging.example.com")
required_token = os.environ["API_TOKEN"]   # raises KeyError if missing
 
# python-dotenv loads a .env file into os.environ
from dotenv import load_dotenv
load_dotenv()

Pytest Framework

Discovery

By default pytest finds:

  • Files matching test_*.py or *_test.py
  • Functions named test_*
  • Classes named Test* (no __init__)

Assertions

def test_equality():
    assert 1 + 1 == 2
    assert "ada" in user.email
    assert user.id is not None
    assert response.status_code == 200, f"got {response.status_code}: {response.text}"
 
# Floating point
import math
def test_close_enough():
    assert math.isclose(0.1 + 0.2, 0.3)
    # or pytest.approx
    import pytest
    assert 0.1 + 0.2 == pytest.approx(0.3)
 
# Expected exceptions
import pytest
def test_raises():
    with pytest.raises(ValueError, match="invalid"):
        int("abc")

Fixtures

import pytest
 
@pytest.fixture
def api_client():
    import requests
    s = requests.Session()
    s.headers["Authorization"] = "Bearer test-token"
    yield s          # everything before yield = setup
    s.close()        # everything after = teardown
 
@pytest.fixture(scope="session")  # function (default) | class | module | session
def driver():
    from selenium import webdriver
    d = webdriver.Chrome()
    yield d
    d.quit()
 
def test_users(api_client):
    r = api_client.get("https://api.example.com/users")
    assert r.status_code == 200

Parametrize

@pytest.mark.parametrize("email,valid", [
    ("ada@example.com", True),
    ("missing-at.com",   False),
    ("",                 False),
    ("a@b.co",           True),
])
def test_email_validation(email, valid):
    assert is_valid_email(email) is valid

Markers

@pytest.mark.skip(reason="Backend not deployed")
def test_thing(): ...
 
@pytest.mark.xfail(reason="Known bug — see #1234")
def test_known_bug(): ...
 
@pytest.mark.slow                     # custom marker
def test_full_e2e(): ...
 
# Run only slow tests
# pytest -m slow

Register custom markers in pyproject.toml to avoid warnings:

[tool.pytest.ini_options]
markers = [
  "slow: long-running tests",
  "smoke: critical-path tests",
]

conftest.py — shared fixtures and hooks

# tests/conftest.py
import pytest
 
@pytest.fixture(autouse=True)         # runs for every test, no need to ask
def reset_db():
    yield
    db.truncate("test_data")
 
def pytest_collection_modifyitems(config, items):
    # Auto-mark tests in /e2e folder as slow
    for item in items:
        if "e2e" in item.nodeid:
            item.add_marker(pytest.mark.slow)

CLI

pytest                              # all tests
pytest -v                           # verbose
pytest -x                           # stop at first failure
pytest --maxfail=3
pytest -k "login and not slow"      # filter by name expression
pytest tests/api/                   # specific path
pytest tests/api/test_users.py::test_create
pytest --html=report.html --self-contained-html
pytest -n 4                         # 4 parallel workers (pytest-xdist)
pytest --reruns 2                   # retry flaky tests (pytest-rerunfailures)
pytest --lf                         # last failed only
pytest --ff                         # failed first, then the rest

Useful plugins

  • pytest-html — single-file HTML report
  • pytest-xdist-n auto for parallel runs
  • pytest-rerunfailures — automatic retries
  • pytest-bdd — Gherkin-style scenarios
  • pytest-mock — mocker fixture wrapping unittest.mock
  • pytest-cov — coverage reporting
  • pytest-django / pytest-flask — framework integration

Requests Library (API Testing)

import requests
 
# GET
r = requests.get("https://api.example.com/users",
                 headers={"Accept": "application/json"},
                 params={"page": 1, "size": 20},
                 timeout=10)
print(r.status_code)
print(r.json())
 
# POST
r = requests.post("https://api.example.com/users",
                  json={"name": "Ada", "email": "ada@example.com"},
                  headers={"Authorization": "Bearer token"})
 
requests.put("https://api.example.com/users/42", json=data)
requests.patch("https://api.example.com/users/42", json={"email": "new@x.com"})
requests.delete("https://api.example.com/users/42")

Response

r.status_code            # 200
r.ok                     # True if < 400
r.json()                 # parsed JSON (raises if body isn't JSON)
r.text                   # raw text
r.content                # raw bytes
r.headers                # dict-like
r.headers["Content-Type"]
r.cookies
r.elapsed                # timedelta
r.url                    # final URL after redirects
r.history                # redirect chain
 
r.raise_for_status()     # raises HTTPError for 4xx/5xx

Sessions

Session reuses connection, cookies, and default headers across requests:

with requests.Session() as s:
    s.headers["Authorization"] = "Bearer token"
    s.get("https://api.example.com/me")
    s.post("https://api.example.com/orders", json=order)

Auth and timeouts

requests.get(url, auth=("user", "pass"))
requests.get(url, headers={"Authorization": "Bearer token"})
requests.get(url, timeout=(3, 10))    # (connect, read) seconds

File upload

with open("test.csv", "rb") as f:
    requests.post("https://api.example.com/import",
                  files={"file": f},
                  data={"description": "Bulk import"})

Selenium with Python

from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support.ui import WebDriverWait, Select
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.action_chains import ActionChains
 
driver = webdriver.Chrome()
driver.get("https://app.example.com/login")
 
# Locators
driver.find_element(By.ID, "username")
driver.find_element(By.CSS_SELECTOR, ".submit-btn")
driver.find_element(By.XPATH, "//button[@type='submit']")
driver.find_element(By.LINK_TEXT, "Forgot password?")
driver.find_element(By.NAME, "email")
driver.find_elements(By.CSS_SELECTOR, ".user-row")     # plural
 
# Explicit wait
wait = WebDriverWait(driver, 10)
result = wait.until(EC.visibility_of_element_located((By.ID, "result")))
wait.until(EC.element_to_be_clickable((By.ID, "submit"))).click()
wait.until(EC.text_to_be_present_in_element((By.CSS_SELECTOR, ".banner"), "Welcome"))
 
# Action chains
hover_target = driver.find_element(By.CSS_SELECTOR, ".menu")
ActionChains(driver).move_to_element(hover_target).perform()
 
src = driver.find_element(By.ID, "source")
dst = driver.find_element(By.ID, "target")
ActionChains(driver).drag_and_drop(src, dst).perform()
ActionChains(driver).double_click(target).perform()
 
# Select dropdown
country = Select(driver.find_element(By.ID, "country"))
country.select_by_visible_text("United States")
country.select_by_value("US")
country.select_by_index(2)
 
# Screenshots and JS
driver.save_screenshot("login-fail.png")
title = driver.execute_script("return document.title")
driver.execute_script("arguments[0].scrollIntoView({block:'center'})", el)
 
driver.quit()

Common Patterns

Comprehensions

active_emails = [u.email for u in users if u.active]
ids_by_email  = {u.email: u.id for u in users}
seen_roles    = {u.role for u in users}

Exception handling

class TestSetupError(Exception):
    """Raised when test setup fails irrecoverably."""
 
try:
    response = api.get("/health")
    response.raise_for_status()
except requests.HTTPError as e:
    raise TestSetupError(f"API not healthy: {e}") from e
except requests.ConnectionError:
    pytest.skip("API unreachable — skipping")
finally:
    cleanup()

Context managers

with open("file.txt") as f:
    data = f.read()
# file is closed automatically
 
# Custom — for fixtures
from contextlib import contextmanager
 
@contextmanager
def temp_user(api):
    user = api.post("/users", json={"name": "Test"}).json()
    try:
        yield user
    finally:
        api.delete(f"/users/{user['id']}")
 
with temp_user(api_client) as user:
    # ... test with user, cleanup happens automatically
    ...

Test data factories

def make_user(i: int) -> dict:
    return {
        "name":  f"Test User {i:03d}",
        "email": f"user-{i:03d}@test.example",
        "role":  "viewer",
    }
 
users = [make_user(i) for i in range(10)]

Regular expressions

import re
 
re.match(r"\d+", "123abc")        # matches start: <Match span=(0,3) 'abc'>
re.search(r"\d+", "abc123")       # anywhere: <Match span=(3,6) '123'>
re.findall(r"\d+", "1 and 2")     # ['1', '2']
re.sub(r"\s+", " ", "a   b")      # 'a b'
 
m = re.search(r"order-(\d+)-(\w+)", "order-123-abc")
m.group(1)   # '123'
m.groups()   # ('123', 'abc')
 
# Compile if reusing
EMAIL = re.compile(r"^[\w.+-]+@[\w-]+\.[\w.-]+$")
EMAIL.match("ada@example.com")

Date handling

from datetime import datetime, timedelta, timezone
 
now = datetime.now(timezone.utc)
print(now.isoformat())            # '2026-05-03T10:00:00.123456+00:00'
 
# Format
now.strftime("%Y-%m-%d %H:%M")    # '2026-05-03 10:00'
 
# Parse
parsed = datetime.strptime("2026-05-03", "%Y-%m-%d")
 
# Arithmetic
yesterday  = now - timedelta(days=1)
in_an_hour = now + timedelta(hours=1)
diff = (now - other).total_seconds()