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") # TrueBooleans, 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, ..., 90Functions & 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: floatAbstract 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_*.pyor*_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 == 200Parametrize
@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 validMarkers
@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 slowRegister 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 restUseful plugins
pytest-html— single-file HTML reportpytest-xdist—-n autofor parallel runspytest-rerunfailures— automatic retriespytest-bdd— Gherkin-style scenariospytest-mock— mocker fixture wrappingunittest.mockpytest-cov— coverage reportingpytest-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/5xxSessions
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) secondsFile 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()