Python interview questions for testers
Updated 2026-11-13 · 12 min read
Thirty Python questions an automation tester gets asked, each tied to how you use it in test code: collections, exceptions, files and JSON, classes and page objects, decorators, the requests library and the standard string and list coding problems.
More companies now ask for Python in QA and SDET roles, especially for API testing, data and AI-related testing, and teams that already write their services in Python. You are not interviewed as a Python developer. You are expected to know the part of Python that test code uses, and to explain it with an example from your own tests. Examples use Python 3.
Core language
1. Why would a team choose Python for test automation?
It is quick to write and easy to read, so tests are short and new people learn it fast. It has a rich ecosystem for testing (pytest, requests, Playwright, Selenium, Appium bindings), is strong for API, data and scripting work, and the same language can drive test data generation and report analysis. The trade-offs: no compile step to catch mistakes early (use type hints and a linter) and slower raw execution, which rarely matters because tests mostly wait on browsers and networks.
2. What is the difference between mutable and immutable types?
An immutable object cannot be changed after it is created: numbers, strings, tuples, frozensets. A mutable one can: lists, dictionaries, sets. This matters in tests when you pass a list to a helper that quietly modifies it, or use a mutable value as a default argument (see question 8).
3. How do a list, tuple, set and dictionary differ, and when do you use each?
A list is an ordered, changeable sequence (the elements found by find_elements). A tuple is an ordered, fixed sequence (a pair of coordinates, or a (by, value) locator). A set holds unique values with no order (de-duplicating links). A dictionary maps keys to values (a row of test data, a JSON object).
locator = ("css selector", "#email") # tuple: fixed pair
unique_links = {a.get_attribute("href") for a in links} # set: no duplicates
user = {"name": "Asha", "email": "[email protected]"} # dict: test data
4. What is a list comprehension? Give a tester's example.
A short way to build a list from another. It reads as "this, for each that, if this condition".
names = [el.text.strip() for el in elements if el.is_displayed()]
prices = [float(p.replace("₹", "").replace(",", "")) for p in price_texts]
Dictionary and set comprehensions work the same way. Keep them short: if one needs a comment, write a normal loop.
5. What is the difference between == and is?
== compares values; is checks whether two names point to the very same object. Use == for values, and is only for None (if result is None).
a = [1, 2]
b = [1, 2]
a == b # True: same contents
a is b # False: two different list objects
6. How do you work with strings, slicing and f-strings?
Strings are immutable and support slicing, so text[::-1] reverses one, text[:3] takes the first three characters and text[-1] the last. Useful methods: strip, split, join, replace, startswith, lower. f-strings put values straight into text.
order_id = 1042
message = f"Order {order_id} total: ₹{total:,.2f}" # ₹2,999.00
7. What are *args and **kwargs?
They let a function accept any number of positional arguments (*args, a tuple) and keyword arguments (**kwargs, a dictionary). Test helpers use them to pass options through.
def click_when_ready(driver, locator, *, timeout=10, **options):
...
def log(message, *details, **fields):
print(message, details, fields)
log("login", "attempt 1", user="asha", ok=True)
8. What is the mutable default argument trap?
A default value is created once, when the function is defined, so a mutable default is shared by every call.
def add_item(item, items=[]): # wrong: the same list every time
items.append(item)
return items
def add_item(item, items=None): # right
items = [] if items is None else items
items.append(item)
return items
In test code this causes tests that pass alone and fail together, because they share leftover data.
Errors, files and structure
9. How does exception handling work in Python?
try:
row = find_user(email)
except KeyError:
row = create_user(email) # a specific, expected problem
except (TimeoutError, ConnectionError) as error:
raise RuntimeError(f"Service not reachable: {error}") from error
else:
log.info("found existing user") # runs only if nothing was raised
finally:
cleanup() # always runs
Catch specific exceptions, never a bare except: (it hides real bugs, including Ctrl+C). Create your own by subclassing Exception.
10. What is a context manager and the with statement?
It sets something up and guarantees it is cleaned up, even on an error. You use it for files, locks, database connections and temporary directories.
with open("users.csv", encoding="utf-8") as f:
rows = f.read().splitlines()
# the file is closed here, whatever happened inside
11. How do you read and write CSV and JSON files for test data?
import csv
import json
with open("data/users.csv", newline="", encoding="utf-8") as f:
users = list(csv.DictReader(f)) # each row becomes a dict
with open("data/config.json", encoding="utf-8") as f:
config = json.load(f)
with open("out/report.json", "w", encoding="utf-8") as f:
json.dump(result, f, indent=2)
Load data once in a fixture or helper, not inside every test, and never commit real credentials.
12. What are modules, packages, virtual environments and pip?
A module is a .py file; a package is a folder of modules (it can have an __init__.py file). A virtual environment is a private folder of installed packages for one project, so projects do not clash. pip installs packages into it, and requirements.txt (or pyproject.toml) records what the project needs.
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
13. How do classes, inheritance and self work?
A class groups data and behaviour. __init__ runs when an object is created; self is the object itself. A subclass inherits and can extend or override.
class BasePage:
def __init__(self, page):
self.page = page
class LoginPage(BasePage):
def login(self, email, password):
self.page.get_by_label("Email").fill(email)
self.page.get_by_label("Password").fill(password)
self.page.get_by_role("button", name="Sign in").click()
14. How would you write a Page Object in Python?
One class per page, holding locators and actions in business terms, with assertions left in the tests.
class ProductsPage:
def __init__(self, page):
self.page = page
self.search_box = page.get_by_test_id("search")
def open(self):
self.page.goto("/#/products")
def search(self, text):
self.search_box.fill(text)
self.page.get_by_test_id("search-button").click()
return self
@property
def names(self):
return self.page.get_by_test_id("product-name") # a locator, not text
The test then asserts with expect(products.names).to_have_text(["Backpack"]). Return locators from page objects, not strings: reading text straight away (all_inner_texts()) does not wait, so it can return the old list while the page is still updating, whereas expect retries until the page catches up.
15. What is a decorator, and where do testers meet one?
A function that wraps another function to add behaviour, applied with @. You meet them constantly: @pytest.fixture, @pytest.mark.parametrize, @pytest.mark.skip. You can write one too, for example to retry a flaky helper or time a step.
import functools
import time
def timed(func):
@functools.wraps(func)
def wrapper(*args, **kwargs):
start = time.perf_counter()
try:
return func(*args, **kwargs)
finally:
print(f"{func.__name__} took {time.perf_counter() - start:.2f}s")
return wrapper
@timed
def load_dashboard(page):
page.goto("/dashboard")
16. What is a generator and why use yield?
A function that produces values one at a time instead of building a whole list, so it is memory-friendly and can pause. In pytest, a fixture uses yield to hand over a resource and run clean-up afterwards (see the pytest questions).
def read_rows(path):
with open(path, encoding="utf-8") as f:
for line in f:
yield line.strip().split(",")
17. What are lambda, map, filter and sorted with a key?
A lambda is a small anonymous function. sorted and max accept a key function; this is where lambdas are most useful in test code.
products = [{"name": "Backpack", "price": 99900}, {"name": "Notebook", "price": 14900}]
cheapest_first = sorted(products, key=lambda p: p["price"])
names = list(map(lambda p: p["name"], products)) # a comprehension is usually clearer
18. What is the difference between a shallow copy and a deep copy?
A shallow copy copies the outer container but shares the inner objects; a deep copy copies everything. When you copy a nested test-data dictionary and edit it, a shallow copy changes the original as well.
import copy
base = {"user": {"role": "admin"}}
shallow = dict(base)
deep = copy.deepcopy(base)
shallow["user"]["role"] = "guest" # also changes base!
deep["user"]["role"] = "viewer" # base is untouched
19. What is the GIL, and does it stop you running tests in parallel?
The Global Interpreter Lock lets only one thread run Python code at a time in standard CPython, so threads do not speed up CPU-heavy work. Test suites mostly wait on browsers and networks, so threads still help, and the usual way to parallelise is separate processes, which is what pytest-xdist does (pytest -n auto). Newer Python versions have an optional free-threaded build, but treat processes as the safe default.
20. What does if __name__ == "__main__": do?
It runs the code below it only when the file is executed directly, not when it is imported. It lets a file be both a module with helper functions and a small script.
21. What are type hints and are they enforced?
Optional annotations that say what types are expected. Python does not enforce them when running, but editors and tools such as mypy use them to find mistakes, and they document your helpers.
def parse_price(text: str) -> float:
return float(text.replace("₹", "").replace(",", ""))
Working with data and services
22. How do Counter and defaultdict help in tests?
Counter counts things; defaultdict gives a default for missing keys.
from collections import Counter, defaultdict
status_counts = Counter(r.status_code for r in responses) # Counter({200: 18, 404: 2})
by_category = defaultdict(list)
for p in products:
by_category[p["category"]].append(p["name"])
23. How do you handle configuration and secrets in test code?
Read them from environment variables or a config file that is not committed, with sensible defaults for non-secret values, and fail clearly when a required one is missing.
import os
BASE_URL = os.environ.get("BASE_URL", "http://localhost:3001")
API_TOKEN = os.environ["API_TOKEN"] # raises KeyError if it is not set
In CI, supply secrets through the CI tool's secret store, never through the repository.
24. Why use logging instead of print?
Logging has levels (debug, info, warning, error), timestamps, and can go to files and to the CI log, and you can turn the detail up for one run without editing code. pytest captures and shows logs for failed tests.
import logging
log = logging.getLogger(__name__)
log.info("created user %s", email)
25. How do you call an API with the requests library?
import requests
session = requests.Session()
response = session.post(f"{base_url}/api/auth/login", json={"email": "[email protected]", "password": "Demo@1234"}, timeout=10)
assert response.status_code == 200
token = response.json()["token"]
cart = session.get(f"{base_url}/api/cart", headers={"Authorization": f"Bearer {token}"}, timeout=10)
assert cart.json()["items"] == []
Always pass a timeout (without one a call can wait forever), use a Session to reuse connections and headers, and check response.status_code before reading the body. See the API testing questions for what to assert.
26. How do you use regular expressions?
The re module finds and extracts patterns. Testers use it to check formats and pull values out of text.
import re
order_id = re.search(r"Order #(\d+)", confirmation_text).group(1)
assert re.fullmatch(r"[6-9]\d{9}", phone) # a 10-digit Indian mobile number
Use raw strings (r"...") so backslashes are not treated as escapes.
27. How do you handle dates, times and time zones?
Use datetime, make times time-zone aware, and convert at the edges. Many bugs come from comparing a UTC time with a local one.
from datetime import datetime, timedelta, timezone
from zoneinfo import ZoneInfo
now_utc = datetime.now(timezone.utc)
now_ist = now_utc.astimezone(ZoneInfo("Asia/Kolkata"))
expires = now_utc + timedelta(days=7)
Coding problems you will be asked
28. Reverse a string and check for a palindrome.
def reverse(text: str) -> str:
return text[::-1]
def is_palindrome(text: str) -> bool:
clean = "".join(c.lower() for c in text if c.isalnum())
return clean == clean[::-1]
assert reverse("qa") == "aq"
assert is_palindrome("A man, a plan, a canal: Panama")
If asked not to use slicing, swap characters from both ends with two indexes, as in the Java version in Java for testers.
29. Check anagrams, find duplicates and the first non-repeating character.
from collections import Counter
def is_anagram(a: str, b: str) -> bool:
return Counter(a.replace(" ", "").lower()) == Counter(b.replace(" ", "").lower())
def duplicates(items):
counts = Counter(items)
return [item for item, n in counts.items() if n > 1]
def first_unique(text: str):
counts = Counter(text)
return next((c for c in text if counts[c] == 1), None)
assert is_anagram("listen", "silent")
assert duplicates(["a", "b", "a", "c", "b"]) == ["a", "b"]
assert first_unique("swiss") == "w"
30. Two-sum, word frequency and flattening a list.
def two_sum(nums, target):
seen = {}
for i, n in enumerate(nums):
if target - n in seen:
return [seen[target - n], i]
seen[n] = i
return []
def word_frequency(text):
return dict(Counter(text.lower().split()))
def flatten(nested):
for item in nested:
if isinstance(item, list):
yield from flatten(item)
else:
yield item
assert two_sum([2, 7, 11, 15], 9) == [0, 1]
assert word_frequency("to be or not to be") == {"to": 2, "be": 2, "or": 1, "not": 1}
assert list(flatten([1, [2, [3, 4]], 5])) == [1, 2, 3, 4, 5]
Say the cost: two_sum is one pass with a dictionary (linear time), where the double loop is quadratic. Then, as an SDET, write tests for your own function: empty list, no pair, duplicates, negatives.
How to practise
Type these out and run them. Then write a short pytest file that tests your own functions with normal, empty and awkward input; the pytest questions and the Python tutorial show how.
Want a project you can show in interviews?
The portfolio project kit walks you through testing a deliberately buggy app with Selenium or Playwright and API tests, then explaining it. The app itself is free.
