Home / Blog / Python for QA Beginners: From Basics to Your First Test Script

Python for QA

Python for QA Beginners: From Basics to Your First Test Script

QA Knowledge Hub·2026-04-04·9 min read

Python is the most accessible programming language for QA engineers transitioning into automation. The syntax reads almost like English, the community is enormous, and every major testing framework — Pytest, Playwright, Selenium, Robot Framework — has Python support.

This guide teaches you Python from a QA engineer's perspective. No computer science theory, no algorithms. Just the concepts you need to write test scripts, read API responses, and build automation frameworks.

Setting Up Python

Step 1: Download Python 3.11+ from python.org. During installation on Windows, check "Add Python to PATH."

Step 2: Install a code editor. VS Code is recommended — it is free, lightweight, and has excellent Python and testing support.

Step 3: Verify installation:

python --version
# Expected: Python 3.11.x or higher

pip --version
# Expected: pip 23.x or higher

Step 4: Create a project folder and a virtual environment:

mkdir qa-python-practice
cd qa-python-practice
python -m venv venv

# Activate (Windows)
venv\Scripts\activate

# Activate (Mac/Linux)
source venv/bin/activate

A virtual environment keeps your project's packages separate from other Python projects on your machine.

Variables and Data Types

Variables store values. Python automatically determines the type — you do not declare it explicitly.

# String — text
test_url = "https://api.example.com"
username = "qa_tester"

# Integer — whole numbers
status_code = 200
retry_count = 3

# Float — decimal numbers
response_time = 0.345  # seconds

# Boolean — True or False
is_logged_in = True
test_passed = False

# Printing values
print(f"Status code: {status_code}")
print(f"Response time: {response_time}s")

The f"..." syntax is an f-string — it lets you embed variables directly inside strings. Use it constantly.

QA use case: Storing test configuration

BASE_URL = "https://staging.example.com"
API_KEY = "test-api-key-12345"
TIMEOUT_SECONDS = 30
MAX_RETRIES = 3

Capital variable names are a convention for constants — values that should not change during a test run.

Strings — Text Manipulation

email = "  TestUser@EXAMPLE.COM  "

# Clean up user input (common in test data prep)
cleaned = email.strip().lower()
print(cleaned)  # "testuser@example.com"

# Check if a string contains a value (assertion use)
error_message = "Invalid email format"
assert "Invalid" in error_message

# String formatting
user_id = 42
endpoint = f"/api/users/{user_id}"  # "/api/users/42"

# Split a string
csv_line = "user_id,name,email,status"
headers = csv_line.split(",")
print(headers)  # ['user_id', 'name', 'email', 'status']

Lists — Ordered Collections

Lists store multiple values in sequence. You use them for test data, response arrays, and collections of test cases.

# Creating lists
test_emails = ["valid@test.com", "no-at-sign.com", "", "a@b.c"]
status_codes = [200, 201, 400, 401, 403, 404, 500]

# Accessing items by index (starts at 0)
first_email = test_emails[0]   # "valid@test.com"
last_email = test_emails[-1]   # "a@b.c"

# Slicing
first_three = status_codes[:3]  # [200, 201, 400]

# Adding to a list
test_emails.append("new@test.com")

# Checking membership
if 404 in status_codes:
    print("404 is in the list")

# Length
print(len(test_emails))  # 5

# Iterating
for code in status_codes:
    if code >= 400:
        print(f"Error code: {code}")

Dictionaries — Key-Value Data

Dictionaries are how Python represents JSON data. Every API response you receive will be a dictionary after parsing.

# Creating a dictionary
user = {
    "id": 1,
    "name": "Test User",
    "email": "test@example.com",
    "is_active": True
}

# Accessing values
print(user["name"])        # "Test User"
print(user.get("phone"))   # None (safe access — no error if key missing)

# Checking key existence
if "email" in user:
    print(f"Email: {user['email']}")

# Updating values
user["is_active"] = False
user["role"] = "admin"  # Adding a new key

# Iterating
for key, value in user.items():
    print(f"{key}: {value}")

QA use case: Validating an API response

response_json = {
    "status": "success",
    "data": {
        "user_id": 42,
        "token": "eyJhbGciOiJIUzI1NiJ9..."
    }
}

# Assertions on the response
assert response_json["status"] == "success"
assert "user_id" in response_json["data"]
assert response_json["data"]["user_id"] == 42

Conditionals — If/Else Logic

status_code = 401

if status_code == 200:
    print("Success")
elif status_code == 401:
    print("Unauthorized — check your API key")
elif status_code == 404:
    print("Resource not found")
else:
    print(f"Unexpected status: {status_code}")

Comparison operators

# Equality
status_code == 200   # True if equal
status_code != 200   # True if not equal

# Numeric comparisons
response_time < 2.0  # True if under 2 seconds
retry_count >= 3     # True if 3 or more

# Boolean logic
is_authenticated and has_permission   # Both must be True
is_error or is_timeout               # At least one must be True
not is_active                        # Reversal

Loops — Repeating Actions

# For loop — iterate over a sequence
endpoints = ["/users", "/orders", "/products", "/cart"]

for endpoint in endpoints:
    print(f"Testing endpoint: {endpoint}")

# Range-based loop
for attempt in range(3):  # 0, 1, 2
    print(f"Attempt {attempt + 1}")

# While loop — run until condition is false
retries = 0
max_retries = 3

while retries < max_retries:
    print(f"Retry {retries + 1}")
    retries += 1

# List comprehension — create a list from a loop
valid_codes = [code for code in status_codes if code < 400]
# [200, 201]

Functions — Reusable Blocks of Code

A function is a named block of code you can call multiple times. In test automation, every interaction pattern becomes a function.

def get_auth_headers(api_key: str) -> dict:
    """Returns headers required for authenticated API calls."""
    return {
        "Authorization": f"Bearer {api_key}",
        "Content-Type": "application/json"
    }

def is_valid_email(email: str) -> bool:
    """Basic email format validation."""
    return "@" in email and "." in email.split("@")[-1]

# Calling functions
headers = get_auth_headers("my-secret-key")
print(is_valid_email("test@example.com"))  # True
print(is_valid_email("not-an-email"))      # False

Default parameter values

def make_api_request(endpoint: str, method: str = "GET", timeout: int = 30):
    print(f"{method} {endpoint} (timeout: {timeout}s)")

make_api_request("/users")            # GET /users (timeout: 30s)
make_api_request("/orders", "POST")   # POST /orders (timeout: 30s)
make_api_request("/data", timeout=5)  # GET /data (timeout: 5s)

Classes group related data and functions together. The Page Object Model is built on classes.

class APIClient:
    """Reusable HTTP client for test automation."""
    
    def __init__(self, base_url: str, api_key: str):
        self.base_url = base_url
        self.headers = {
            "Authorization": f"Bearer {api_key}",
            "Content-Type": "application/json"
        }
    
    def get(self, endpoint: str) -> dict:
        import requests
        url = f"{self.base_url}{endpoint}"
        response = requests.get(url, headers=self.headers)
        return {"status": response.status_code, "body": response.json()}
    
    def post(self, endpoint: str, payload: dict) -> dict:
        import requests
        url = f"{self.base_url}{endpoint}"
        response = requests.post(url, json=payload, headers=self.headers)
        return {"status": response.status_code, "body": response.json()}


# Using the class
client = APIClient("https://api.example.com", "test-key-123")
result = client.get("/users/1")
print(result["status"])  # 200
print(result["body"]["name"])  # "Test User"

Exception Handling — Dealing with Errors

When something goes wrong, Python raises an exception. If you do not handle it, your test crashes. Proper exception handling makes your tests more robust.

import requests

def safe_get(url: str) -> dict | None:
    try:
        response = requests.get(url, timeout=10)
        response.raise_for_status()  # Raises exception for 4xx/5xx
        return response.json()
    except requests.exceptions.Timeout:
        print(f"Request timed out: {url}")
        return None
    except requests.exceptions.HTTPError as e:
        print(f"HTTP error {e.response.status_code}: {url}")
        return None
    except requests.exceptions.ConnectionError:
        print(f"Connection failed: {url}")
        return None

Reading and Writing Files

Test data often lives in files. JSON for API payloads, CSV for bulk test data.

import json

# Reading a JSON file
with open("test_data/users.json", "r") as f:
    users = json.load(f)

for user in users:
    print(f"Testing login for: {user['email']}")

# Writing results to a file
results = {"passed": 45, "failed": 3, "skipped": 2}
with open("test_results.json", "w") as f:
    json.dump(results, f, indent=2)

# Reading a CSV file
import csv

with open("test_data/products.csv", "r") as f:
    reader = csv.DictReader(f)
    for row in reader:
        print(f"Product: {row['name']}, Price: {row['price']}")

Your First Real Test Script

Putting it all together — a test script that calls a real public API and validates the response:

# test_jsonplaceholder.py
import requests

BASE_URL = "https://jsonplaceholder.typicode.com"

def get_user(user_id: int) -> dict:
    """Fetch a user from the API."""
    response = requests.get(f"{BASE_URL}/users/{user_id}")
    return {"status_code": response.status_code, "data": response.json()}

def test_get_existing_user():
    """Verify that fetching user 1 returns correct data."""
    result = get_user(1)
    
    assert result["status_code"] == 200, f"Expected 200, got {result['status_code']}"
    
    user = result["data"]
    assert "id" in user, "Response missing 'id' field"
    assert "name" in user, "Response missing 'name' field"
    assert "email" in user, "Response missing 'email' field"
    assert user["id"] == 1, f"Expected id=1, got id={user['id']}"
    
    print(f"PASS: User {user['id']} — {user['name']}")

def test_get_nonexistent_user():
    """Verify that fetching a user that doesn't exist returns 404."""
    result = get_user(9999)
    
    assert result["status_code"] == 404, f"Expected 404, got {result['status_code']}"
    print("PASS: Non-existent user returns 404")

def test_user_email_format():
    """Verify user 1's email contains @ symbol."""
    result = get_user(1)
    email = result["data"]["email"]
    
    assert "@" in email, f"Email format invalid: {email}"
    print(f"PASS: Email format valid — {email}")

# Run all tests
if __name__ == "__main__":
    test_get_existing_user()
    test_get_nonexistent_user()
    test_user_email_format()
    print("\nAll tests passed!")

Run it:

python test_jsonplaceholder.py

This is a real, working test script. It hits a live API, validates status codes, checks response structure, and reports results.

The Next Step: Pytest

Once you understand basic Python, move to Pytest — the standard Python test framework. It discovers test files automatically, generates reports, and integrates with CI/CD.

pip install pytest

Convert your test script to Pytest format:

# test_api.py — Pytest version
import requests
import pytest

BASE_URL = "https://jsonplaceholder.typicode.com"

def test_get_user_returns_200():
    response = requests.get(f"{BASE_URL}/users/1")
    assert response.status_code == 200

def test_get_user_has_name_field():
    response = requests.get(f"{BASE_URL}/users/1")
    assert "name" in response.json()

def test_nonexistent_user_returns_404():
    response = requests.get(f"{BASE_URL}/users/9999")
    assert response.status_code == 404

Run with:

pytest test_api.py -v

Output:

test_api.py::test_get_user_returns_200 PASSED
test_api.py::test_get_user_has_name_field PASSED
test_api.py::test_nonexistent_user_returns_404 PASSED

Python Concepts Used in Real Automation

Here is a quick reference of Python patterns you will encounter in real test frameworks:

# Fixtures (Pytest) — shared setup/teardown
import pytest

@pytest.fixture
def api_client():
    client = APIClient(BASE_URL, API_KEY)
    yield client
    # cleanup code here (runs after test)

# Parametrize — run same test with multiple inputs
@pytest.mark.parametrize("user_id,expected_status", [
    (1, 200),
    (9999, 404),
    (0, 404),
])
def test_get_user_status(user_id, expected_status):
    response = requests.get(f"{BASE_URL}/users/{user_id}")
    assert response.status_code == expected_status

# Context managers — common in Playwright
with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    page.goto("https://example.com")

# Type hints — used in professional code
def verify_login(email: str, password: str) -> bool:
    ...

Summary

Python for QA is not about becoming a software developer. It is about understanding enough to:

  1. Store and manipulate test data (variables, lists, dicts)
  2. Write reusable test helpers (functions)
  3. Organise test code into maintainable structures (classes)
  4. Call APIs and validate responses (requests + assertions)
  5. Handle failures gracefully (exception handling)
  6. Work with test data files (file I/O)

Everything else — Playwright, Selenium, Pytest fixtures, CI/CD integration — is built on top of these fundamentals. Get these right first and the frameworks will feel intuitive rather than overwhelming.

The script you wrote at the end of this guide is your starting point. Push it to GitHub today. It is the beginning of your automation portfolio.

Recommended Resource

Automation Testing Scenarios Pack

High-quality automation scenarios for UI, API, and microservices systems.

1299Get This Guide →

Related Posts

📝
Interview Prep
Apr 2026·10 min read

30 Performance Testing Interview Questions and Answers

Performance testing interview questions for QA roles — covering load testing, stress testing, JMeter, key metrics, and how to analyze results.

Read article →
📝
Interview Prep
Apr 2026·12 min read

35 Playwright Interview Questions (With Answers)

Playwright interview questions and answers for QA and SDET roles — covering setup, locators, waits, fixtures, API testing, and debugging.

Read article →
📝
Interview Prep
Apr 2026·11 min read

40 SQL Interview Questions for QA Engineers

SQL interview questions specifically for QA and SDET roles — covering SELECT, JOINs, aggregations, NULL handling, and data validation queries with answers.

Read article →