Home / Blog / Python for QA Beginners: From Basics to Your First Test Script
Python for QA Beginners: From Basics to Your First Test Script
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 higherStep 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/activateA 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 = 3Capital 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"] == 42Conditionals — 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 # ReversalLoops — 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")) # FalseDefault 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 — Organising Related Code
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 NoneReading 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.pyThis 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 pytestConvert 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 == 404Run with:
pytest test_api.py -vOutput:
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 PASSEDPython 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:
- Store and manipulate test data (variables, lists, dicts)
- Write reusable test helpers (functions)
- Organise test code into maintainable structures (classes)
- Call APIs and validate responses (requests + assertions)
- Handle failures gracefully (exception handling)
- 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.
Related Posts
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 →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 →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 →