QA Engineer Skills 2026QA-2026Python Essentials for QA

Python Essentials for QA

Python is the most popular language for backend/API testing, data validation, and scripting in the QA world. Its readable syntax, rich standard library, and vast ecosystem of testing libraries make it the default choice for QA engineers who work beyond the browser.


Why Python for QA

Strength How It Helps QA
Readable syntax Test code is self-documenting; easy for non-Python developers to review
pytest ecosystem The most powerful test framework, with fixtures, parametrize, and plugins
requests library Clean HTTP client for API testing
Data libraries pandas, json, csv — essential for data validation
Scripting speed Quick scripts for log parsing, data cleanup, environment setup
Type hints + mypy Optional static typing catches errors before tests run

pytest: The Test Framework

pytest is the standard Python test framework. It is simpler than unittest, more powerful, and used by the majority of Python QA teams.

Basic Test Structure

# test_login.py
def test_successful_login(api_client):
    response = api_client.post("/auth/login", json={
        "email": "test@example.com",
        "password": "ValidPass123!"
    })
    assert response.status_code == 200
    assert "access_token" in response.json()

def test_login_with_invalid_password(api_client):
    response = api_client.post("/auth/login", json={
        "email": "test@example.com",
        "password": "wrong"
    })
    assert response.status_code == 401
    assert "access_token" not in response.json()

Fixtures

Fixtures provide reusable setup and teardown. They replace the setup/teardown methods of unittest with a more flexible, composable pattern.

# conftest.py
import pytest
import requests

@pytest.fixture(scope="session")
def base_url():
    return os.environ.get("API_BASE_URL", "http://localhost:3000/api/v1")

@pytest.fixture(scope="session")
def auth_token(base_url):
    r = requests.post(f"{base_url}/auth/login", json={
        "email": "admin@test.com", "password": "adminpass"
    })
    return r.json()["access_token"]

@pytest.fixture
def auth_headers(auth_token):
    return {"Authorization": f"Bearer {auth_token}"}

@pytest.fixture
def api_client(base_url, auth_headers):
    session = requests.Session()
    session.headers.update(auth_headers)
    session.base_url = base_url
    return session

Fixture scopes control lifecycle:

  • scope="function" (default): runs before/after each test
  • scope="class": once per test class
  • scope="module": once per file
  • scope="session": once per test run

Parametrize

Run the same test with different data:

@pytest.mark.parametrize("email,expected_status", [
    ("valid@test.com", 200),
    ("", 400),
    ("not-an-email", 422),
    ("valid@test.com; DROP TABLE users", 422),
    ("a" * 255 + "@test.com", 422),
])
def test_login_email_validation(api_client, email, expected_status):
    response = api_client.post("/auth/login", json={
        "email": email, "password": "ValidPass123!"
    })
    assert response.status_code == expected_status

Markers

Tag tests for selective execution:

@pytest.mark.smoke
def test_health_check(base_url):
    assert requests.get(f"{base_url}/health").status_code == 200

@pytest.mark.slow
def test_full_checkout_flow(api_client):
    # ... lengthy test
    pass
pytest -m smoke          # Run only smoke tests
pytest -m "not slow"     # Skip slow tests

The requests Library

The standard HTTP client for API testing in Python.

import requests

# GET with query parameters
r = requests.get("https://api.example.com/users", params={"page": 1, "limit": 10})
print(r.status_code)     # 200
print(r.json())          # parsed JSON response
print(r.headers)         # response headers
print(r.elapsed)         # time taken

# POST with JSON body
r = requests.post("https://api.example.com/users", json={
    "name": "Alice",
    "email": "alice@example.com"
})

# PUT with headers
r = requests.put("https://api.example.com/users/123",
    json={"name": "Alice Updated"},
    headers={"Authorization": "Bearer token123"}
)

# DELETE
r = requests.delete("https://api.example.com/users/123",
    headers={"Authorization": "Bearer token123"}
)
assert r.status_code == 204

Session Objects

For tests that need to maintain state (cookies, headers) across requests:

session = requests.Session()
session.headers.update({"Authorization": "Bearer token123"})

# All subsequent requests include the auth header
r1 = session.get("https://api.example.com/users/me")
r2 = session.get("https://api.example.com/users/me/orders")

Data Validation Patterns

import json
from datetime import datetime

def test_user_response_structure(api_client, base_url):
    r = api_client.get(f"{base_url}/users/me")
    user = r.json()

    # Required fields exist
    required_fields = {"id", "name", "email", "created_at", "role"}
    assert required_fields.issubset(user.keys()), f"Missing: {required_fields - user.keys()}"

    # Sensitive fields absent
    assert "password" not in user
    assert "password_hash" not in user

    # Type validation
    assert isinstance(user["id"], int)
    assert isinstance(user["name"], str)
    assert len(user["name"]) > 0

    # Date format validation
    datetime.fromisoformat(user["created_at"])  # Raises if invalid

    # Enum validation
    assert user["role"] in {"admin", "editor", "viewer"}

Package Management

Tool Best For
pip Simple projects, quick installs
poetry Dependency management with lock files, publishing
uv Fast installs, modern replacement for pip + venv
# Create virtual environment and install dependencies
python -m venv .venv
source .venv/bin/activate
pip install pytest requests httpx

# Or with poetry
poetry init
poetry add pytest requests httpx

# Or with uv
uv init
uv add pytest requests httpx

Practical Exercise

Write a pytest test file that:

  1. Uses a fixture to authenticate and get a token
  2. Tests creating a user (POST)
  3. Tests retrieving the created user (GET)
  4. Tests that duplicate email creation returns 409
  5. Uses parametrize to test email validation with at least 5 inputs
  6. Cleans up the created user in a fixture teardown

Key Takeaways

  • pytest is the standard: learn fixtures, parametrize, and markers
  • requests is the standard HTTP client: learn sessions, JSON handling, and response inspection
  • Use conftest.py for shared fixtures across test files
  • Validate response structure (fields, types, absence of sensitive data), not just status codes
  • Use parametrize for data-driven testing instead of copy-pasting test functions