Resource library

Automation Interview

Python Interview Questions for Testers and QA Automation (2026)

Master python interview questions for testers with 60+ answers on pytest, fixtures, Requests, data structures, coding katas, and QA automation job roles.

52 min read | 7,091 words

TL;DR

Prepare for Python tester coding interviews by solving collection, string, parsing, API, retry, fixture, mocking, and debugging problems with runnable tests. Clarify the input contract, cover boundaries, state complexity, and explain how the solution behaves in a real automation suite.

Key Takeaways

  • Interviewers evaluate contracts, edge cases, tests, readability, and debugging in addition to correct output.
  • Know lists, tuples, sets, dictionaries, comprehensions, iterators, exceptions, context managers, and type hints.
  • Use the data structure that matches membership, order, counting, grouping, or queue behavior.
  • Write boundary tests before optimization and state time and space complexity.
  • Keep retries bounded and limited to safe transient operations.
  • Use pytest fixtures and parametrization for explicit setup and broad input coverage.
  • Practice explaining production test utility, not only algorithm syntax.

The python interview questions for testers that matter most are grounded in automation work: transform data, find duplicates, validate responses, handle failures, design fixtures, and make code testable. A strong answer is correct, readable, covered by edge cases, and honest about complexity and assumptions.

This guide gives runnable Python and pytest examples plus the explanation an interviewer expects. Type each solution yourself, alter the inputs, and practice talking through decisions before looking at the model answer.

TL;DR

Topic Question count Difficulty
Interview method and Python foundations 16 Beginner to intermediate
Collections, strings, and parsing 15 Beginner to intermediate
Exceptions, typing, and design 13 Intermediate
pytest, fixtures, and mocking 13 Intermediate
Requests and API automation 13 Intermediate to advanced
Data structures and coding katas 13 Intermediate to advanced

In a timed round, solve the simplest correct version first. Test it, explain complexity, then improve it only if the constraints require improvement.

1. What Python coding interview questions testers evaluate

A tester coding round is not always an algorithm contest. It may combine a small data problem, a test-automation task, a bug review, and follow-up design questions. Interviewers look for structured thinking and code that another engineer could maintain.

Start every exercise with a contract. Ask what input types are allowed, whether order matters, how duplicates should behave, whether invalid input should raise or be skipped, how large the input can be, and whether mutation is allowed. If the interviewer cannot answer, state a reasonable assumption and proceed. This prevents a correct algorithm from solving the wrong problem.

Use a simple loop before a compressed one-liner when it communicates intent better. Python fluency is not measured by minimizing line count. Name variables from the domain, keep side effects visible, and avoid mutable default arguments. Add type hints when they clarify the boundary, but do not spend the entire round constructing elaborate types.

After coding, test categories deliberately:

  • Empty and minimal inputs.
  • Typical inputs.
  • Duplicates and ordering.
  • Boundary values.
  • Invalid inputs defined by the contract.
  • A case that would break a naive approach.

State complexity in terms of input size. Mention expected versus worst-case behavior when hash tables are involved only if it matters to the discussion.

The JavaScript coding questions for testers offer parallel exercises in another common SDET language. Compare language mechanics, but keep the same quality bar for contracts and tests.

2. Python language foundations for QA and SDET roles

Know the behavior of core collections. Lists are ordered and mutable. Tuples are ordered and immutable, though contained objects can still be mutable. Sets store unique hashable elements and support efficient expected membership checks. Dictionaries map unique hashable keys to values and preserve insertion order as a language guarantee in modern Python.

Understand equality versus identity. == asks whether values are equal according to their type. is asks whether two references identify the same object. Use is None, but do not use is for string or integer value comparison.

Review truthiness, slicing, unpacking, comprehensions, generator expressions, enumerate, zip, sorted, and key functions. Know that a generator is lazy and usually consumed once. Be able to explain shallow and deep copies, especially when test data contains nested dictionaries or lists.

Functions are objects. Learn positional and keyword arguments, default values, *args, **kwargs, closures, and decorators at a practical level. Never use a mutable list or dictionary as a default when each call should get fresh state:

def add_tag(tag: str, tags: list[str] | None = None) -> list[str]:
    result = [] if tags is None else list(tags)
    result.append(tag)
    return result


assert add_tag("smoke") == ["smoke"]
assert add_tag("regression") == ["regression"]

Review exceptions, with statements, iterables, iterators, modules, package imports, virtual environments, and dependency locking. For automation, also know JSON handling, file paths through pathlib, regular expressions, dates and time zones, and HTTP client conventions used by your team.

Explain these concepts through test consequences. A shared mutable default can leak state between tests. A consumed generator can make a second assertion unexpectedly empty. A shallow copy can allow one test case to mutate another's nested data.

3. Coding exercise: remove duplicates while preserving order

A classic question asks you to remove duplicates. The important detail is whether original order must remain. Converting directly to a set removes duplicates but does not express an ordered-result contract.

A readable O(n) expected-time solution uses a set for membership and a list for output:

from collections.abc import Iterable
from typing import TypeVar

T = TypeVar("T")


def unique_in_order(values: Iterable[T]) -> list[T]:
    seen: set[T] = set()
    result: list[T] = []

    for value in values:
        if value not in seen:
            seen.add(value)
            result.append(value)

    return result


def test_unique_in_order() -> None:
    assert unique_in_order([]) == []
    assert unique_in_order(["a"]) == ["a"]
    assert unique_in_order(["a", "b", "a", "c"]) == ["a", "b", "c"]
    assert unique_in_order([1, 1, 2, 1]) == [1, 2]


if __name__ == "__main__":
    test_unique_in_order()
    print("all tests passed")

This assumes values are hashable. If nested dictionaries are allowed, the set approach raises TypeError. State that constraint. You could use equality-based scanning for unhashable items, but the time cost becomes O(n squared), or derive a stable key if the domain provides one.

An interviewer may ask for duplicate values instead of unique values. Clarify whether each duplicate should appear once or once per repeated occurrence. For test result IDs, you might return an ordered list of IDs observed more than once. A Counter is useful, but a one-pass count plus an ordered pass may communicate the rule best.

Do not optimize away the tests. Empty input, one item, repeated first and last values, mixed ordering, and unhashable input are the questions hiding inside the algorithm.

4. Coding exercise: group failed tests by error type

Automation systems often transform result records. Suppose each result has a test name, status, and optional error type. Return failed test names grouped by error type, preserving input order within each group.

from collections import defaultdict
from typing import TypedDict


class TestResult(TypedDict, total=False):
    name: str
    status: str
    error_type: str


def group_failures(results: list[TestResult]) -> dict[str, list[str]]:
    grouped: defaultdict[str, list[str]] = defaultdict(list)

    for result in results:
        if result.get("status") != "failed":
            continue

        name = result.get("name")
        if not name:
            raise ValueError("failed result must include a name")

        error_type = result.get("error_type", "UnknownError")
        grouped[error_type].append(name)

    return dict(grouped)


sample: list[TestResult] = [
    {"name": "login", "status": "failed", "error_type": "Timeout"},
    {"name": "search", "status": "passed"},
    {"name": "checkout", "status": "failed", "error_type": "Assertion"},
    {"name": "logout", "status": "failed", "error_type": "Timeout"},
]

assert group_failures(sample) == {
    "Timeout": ["login", "logout"],
    "Assertion": ["checkout"],
}

The algorithm is O(n) time and O(f) additional space for failed records. The contract chooses a default category when error type is missing and raises when a failed record lacks a usable name. An alternative could skip malformed input or return validation errors, but that must be deliberate.

Follow-up questions may include sorting groups, returning counts, processing a stream, or handling millions of records. Sorting adds cost. Streaming can yield aggregates only after enough input has arrived, and exact grouping still requires storage proportional to the distinct content retained.

This exercise tests dictionaries, default values, types, validation, and domain judgment in one compact problem.

5. Coding exercise: validate an API response contract

A QA coding task may provide parsed JSON and ask for validation. Separate parsing, schema-like validation, and business rules. Do not assume every key exists or every value has the correct type.

The function below validates a simple paginated response using only the standard library:

from typing import Any


def validate_page(payload: Any) -> list[str]:
    errors: list[str] = []

    if not isinstance(payload, dict):
        return ["response must be an object"]

    items = payload.get("items")
    if not isinstance(items, list):
        errors.append("items must be a list")
        items = []

    next_cursor = payload.get("next_cursor")
    if next_cursor is not None and not isinstance(next_cursor, str):
        errors.append("next_cursor must be a string or null")

    seen_ids: set[str] = set()
    for index, item in enumerate(items):
        if not isinstance(item, dict):
            errors.append(f"items[{index}] must be an object")
            continue

        item_id = item.get("id")
        if not isinstance(item_id, str) or not item_id:
            errors.append(f"items[{index}].id must be a nonempty string")
        elif item_id in seen_ids:
            errors.append(f"duplicate id: {item_id}")
        else:
            seen_ids.add(item_id)

    return errors


assert validate_page({"items": [], "next_cursor": None}) == []
assert validate_page({"items": [{"id": "1"}, {"id": "1"}]}) == [
    "duplicate id: 1"
]
assert validate_page([]) == ["response must be an object"]

Returning all detected errors can be more diagnostic than raising at the first one. A boundary validator might instead raise a custom exception to stop unsafe processing. Discuss which consumer needs the result.

In a real project, a JSON Schema or typed model can validate structure, but business invariants such as uniqueness, allowed state transitions, or cross-field rules still need explicit logic. Never invent a success contract based only on HTTP 200. Validate the status, headers where relevant, body shape, semantic content, and resulting state.

For wider service-testing preparation, review API test engineer interview questions.

6. Design exceptions, cleanup, timeouts, and retries safely

Exception handling should preserve failures and recover only when the code has a defined response. Catch a specific exception near the boundary that can handle it. A broad except Exception: pass converts evidence into a false pass or a confusing later error.

Use try/finally or a context manager for cleanup that must run even when assertions fail. Be careful not to replace the original failure with a cleanup error. In production frameworks, attach cleanup failure information while preserving the primary test result according to runner behavior.

Retries belong around transient, safe operations. Define which exceptions are retryable, maximum attempts, delay policy, total deadline, and observability. Never retry an unsafe write unless the operation has idempotency protection or the contract explicitly permits it.

This runnable example retries a callable for selected exceptions:

import time
from collections.abc import Callable
from typing import TypeVar

T = TypeVar("T")


def retry(
    operation: Callable[[], T],
    *,
    attempts: int,
    delay_seconds: float,
    retry_on: tuple[type[Exception], ...],
) -> T:
    if attempts < 1:
        raise ValueError("attempts must be at least 1")

    for attempt in range(1, attempts + 1):
        try:
            return operation()
        except retry_on:
            if attempt == attempts:
                raise
            time.sleep(delay_seconds)

    raise RuntimeError("unreachable")


calls = 0


def eventually_succeeds() -> str:
    global calls
    calls += 1
    if calls < 3:
        raise TimeoutError("temporary")
    return "ok"


assert retry(
    eventually_succeeds,
    attempts=3,
    delay_seconds=0,
    retry_on=(TimeoutError,),
) == "ok"

For application code, inject a sleep function or clock to make retry tests fast and deterministic. Add exponential backoff and jitter only when requirements justify them. Automation retries should not create synchronized load or disguise a repeatable defect.

7. Use pytest parametrization, fixtures, and monkeypatch

pytest is common in Python test stacks. Know plain assertions, test discovery, fixtures, scopes, parametrization, markers, temporary paths, monkeypatching, and failure output. Follow the versions and plugins pinned by the target repository rather than assuming every environment has the same extensions.

Parametrization is appropriate when the behavior is the same and only data changes:

import pytest


def normalize_status(value: str) -> str:
    normalized = value.strip().lower()
    if normalized not in {"passed", "failed", "skipped"}:
        raise ValueError(f"unsupported status: {value}")
    return normalized


@pytest.mark.parametrize(
    ("raw", "expected"),
    [
        ("PASSED", "passed"),
        (" failed ", "failed"),
        ("Skipped", "skipped"),
    ],
)
def test_normalize_status(raw: str, expected: str) -> None:
    assert normalize_status(raw) == expected


def test_normalize_status_rejects_unknown() -> None:
    with pytest.raises(ValueError, match="unsupported status"):
        normalize_status("blocked")

Run it with pytest -q after installing pytest in a virtual environment.

Fixtures should provide a coherent dependency or resource and guarantee teardown. Scope them narrowly by default. A session-scoped mutable object can leak state across tests. Yield fixtures make setup and teardown visible, but cleanup must tolerate partial setup and already-removed resources.

Use monkeypatch to replace environment variables, attributes, or mappings within a test's lifetime. Patch where the code under test looks up the dependency, not where it was originally defined. Prefer dependency injection for complex collaborators because it reduces patching ambiguity.

Do not turn every literal into a fixture. Test-local data is often easier to read. Use parametrization for meaningful equivalence classes, not hundreds of opaque cases that produce unreviewable failure IDs.

8. Mock external boundaries without testing the mock

Mocking isolates a boundary so a test can control outcomes, but it narrows proof. A mocked HTTP response proves your client logic handles that response. It does not prove DNS, TLS, authentication, routing, schema compatibility, or the real service.

Design code so pure logic is separate from I/O. For example, a function can accept a fetch callable and validate the returned payload. The test supplies a deterministic callable without patching global modules:

from collections.abc import Callable
from typing import Any


def active_user_ids(
    fetch_users: Callable[[], list[dict[str, Any]]],
) -> list[str]:
    users = fetch_users()
    return [
        user["id"]
        for user in users
        if user.get("active") is True and isinstance(user.get("id"), str)
    ]


def fake_fetch_users() -> list[dict[str, Any]]:
    return [
        {"id": "u1", "active": True},
        {"id": "u2", "active": False},
        {"id": 3, "active": True},
    ]


assert active_user_ids(fake_fetch_users) == ["u1"]

For unittest.mock, know Mock, patch, return values, side effects, call assertions, and autospec. Use interaction assertions only when the interaction is part of the contract. Overly specific call-order checks couple tests to implementation and make refactoring expensive.

Create contract tests against a controlled real dependency or provider sandbox where appropriate. Keep a small end-to-end flow for the highest-value integration. The test pyramid is a distribution of evidence, not a ban on mocks or UI tests.

In an interview, state both sides: mocks make rare errors and deterministic branches easy to test, while real integration coverage is still required for boundary confidence.

9. Practice debugging and automation design questions

Code review exercises may contain shared mutable state, missing waits, swallowed exceptions, hard-coded data, duplicate setup, or assertions that prove too little. Read from the failure outward. Identify the first incorrect state, not only the final exception.

For a Selenium Python question, understand explicit waits, locator contracts, browser isolation, and stale elements. Avoid fixed sleeps. The Selenium CSS selector examples in Python show direct supported locator syntax. If JavaScript execution appears in a debugging question, explain its test boundary using Selenium execute_async_script in Python.

For API automation, separate the transport client, domain operations, data builders, and assertions enough to keep tests readable. Validate timeouts explicitly. Do not let a library default create an unbounded or surprisingly long wait. Redact tokens and personal data from logs.

For file processing, use pathlib.Path, explicit encoding, and temporary directories. For dates, use timezone-aware values when comparing events across systems. For concurrency, know that threads, processes, and async tasks have different isolation and scheduling properties. Do not add concurrency until shared state is safe.

A strong debugging answer follows a sequence:

  1. Reproduce with the same code, data, configuration, and environment.
  2. Preserve the original traceback and artifacts.
  3. Classify product, test, data, environment, or infrastructure ownership.
  4. Form one hypothesis from evidence.
  5. Run a test that distinguishes it from alternatives.
  6. Fix the root condition and add a regression check.
  7. Run independently, repeatedly, and with normal concurrency.

This method is more credible than calling any intermittent failure "flaky."

10. Python interview questions for testers: a four-week plan

During week one, refresh Python foundations. Implement collection transformations, counting, grouping, sorting, parsing, and error handling. Write tests for every exercise. Explain mutability, hashing, identity, generators, context managers, and common complexity.

During week two, focus on tester problems. Validate JSON-like data, compare record sets, detect duplicates, parse logs, generate boundary values, and reconcile expected versus actual results. Use type hints and small pure functions. Practice one SQL problem daily if the role includes data validation.

During week three, work with pytest and automation design. Build fixtures with safe cleanup, parameterize equivalence classes, patch one external boundary, and test a retry helper. Create one API test project or browser test using the team's likely tools. Run linting and type checking if configured.

During week four, simulate interviews. Solve one problem in 30 minutes, including clarification, code, tests, complexity, and follow-up discussion. Review a deliberately flawed test. Present a small framework design and a debugging case. Record yourself and remove long, unstructured narration.

Use a daily scorecard:

Dimension Question
Contract Did I state inputs, outputs, invalid cases, and ordering?
Correctness Did I test the result, not just print it?
Boundaries Did I cover empty, minimal, duplicate, and error cases?
Readability Can another engineer understand names and control flow?
Complexity Can I explain time and space honestly?
Test utility Can I connect the solution to automation work?

Do not learn new syntax on the final day. Rehearse familiar patterns, confirm the coding environment, and sleep. In the interview, ask focused questions, solve the base case, and adapt when constraints change.

11. Core Python Interview Questions for Testers

Q: How does Python pass arguments?

Python binds each parameter name to the same object reference supplied by the caller; it does not copy the object or use C-style pass-by-reference. Mutating a supplied list is visible to the caller, while assigning a new list to the local parameter is not. In test helpers, I either return a new value or make mutation explicit in the function name and contract.

Q: What makes an object hashable?

A hashable object has a hash value that remains stable during its lifetime and equality behavior consistent with that hash. Such objects can be dictionary keys or set members, which is why tuples of immutable values work but lists do not. For test records, I key lookups by an immutable business ID instead of forcing a mutable payload into a hashable wrapper.

Q: What is LEGB name resolution?

Python resolves an unqualified name through local, enclosing, global, and built-in scopes in that order. Assignment normally creates a local binding unless nonlocal or global says otherwise, which can surprise code that only intended to read a module setting. Automation is easier to isolate when configuration enters through parameters or fixtures rather than mutable globals.

Q: How do shallow and deep copies differ?

A shallow copy creates a new outer container but keeps references to nested objects; copy.deepcopy recursively copies supported nested content. Therefore, changing clone["user"]["role"] after a shallow copy can also change the original fixture. I prefer fresh builders for test cases and reserve deep copying for object graphs whose copy behavior is understood.

Q: What is a decorator?

A decorator receives a function or class and returns the object that should replace it, commonly a wrapper adding timing, logging, or registration. functools.wraps preserves the wrapped function name, documentation, and __wrapped__ link used by introspection. A test decorator must re-raise the original exception so its traceback and failure type remain useful.

Q: What is a context manager?

A context manager defines entry and exit behavior around a with block through __enter__ and __exit__, or an equivalent generator created with contextlib.contextmanager. Its exit path runs even when the body raises, making it suitable for files, locks, sessions, and temporary resources. Returning a truthy value from __exit__ suppresses the exception, so I do that only when suppression is the documented purpose.

Q: How do iterables and iterators differ?

An iterable can produce an iterator, usually through iter(), while an iterator also implements next() and retains traversal state. Calling iter() on a list creates independent traversals, but a generator is its own iterator and is normally exhausted after one pass. If two assertions need the same generated values, I materialize them once and make the memory tradeoff visible.

Q: When should you use a generator?

Use a generator when values can be produced incrementally and callers do not require indexing, repeated traversal, or an immediate snapshot. It is useful for scanning large logs because memory stays bounded by the current item. Its computation and exceptions are deferred until iteration, so tests must consume it to exercise the relevant branch.

Q: Why specify file encoding?

Passing encoding="utf-8" makes text decoding consistent across developer machines and CI runners. Without an explicit encoding, Python uses a platform-dependent default and the same fixture can fail only on one host. I combine pathlib.Path with explicit encoding and test malformed-byte policy separately when external files are not guaranteed to be UTF-8.

Q: How should exceptions be chained?

Translate a low-level failure with raise DomainError(message) from exc when callers need a domain-specific exception but diagnostics still need the cause. The resulting traceback shows both layers and sets __cause__, which tests can inspect without matching an entire traceback. Context may include a safe record identifier, but headers, tokens, passwords, and full sensitive payloads stay out of the message.

Q: What do type hints provide?

Type hints document intended boundaries and enable static tools to detect incompatible calls before execution. Python does not enforce ordinary annotations at runtime, so an annotated dict[str, int] does not validate an untrusted JSON response. I annotate shared fixtures, clients, and transformation functions, then validate external data with explicit runtime checks or an established schema library.

Q: Why use dataclasses for test data?

A dataclass generates predictable initialization, representation, and value equality for domain-shaped test data. frozen=True prevents rebinding fields but does not make a nested list immutable, so field types still determine whether state can leak. I use dataclasses when named fields and equality clarify the scenario, while irregular wire payloads often remain dictionaries.

Q: What does the main guard do?

The if __name__ == "__main__": block runs only when the module is executed as the entry script, not when another module imports it. That prevents a pytest collection import from launching a demo, reading arguments, or making a network call. Reusable behavior belongs in functions, with the guarded block limited to command-line orchestration.

def append_marker(values: list[str]) -> None:
    values.append("checked")

original = ["ready"]
append_marker(original)
assert original == ["ready", "checked"]

12. pytest, Fixtures, and Mocking

Q: How does pytest discover tests?

pytest collects files, classes, and functions using its configured naming patterns, with defaults such as test_*.py and test_*. Collection imports modules, so syntax errors, bad imports, and import-time network calls can fail before any test runs. pytest --collect-only -q reveals collected node IDs and is the first check when a test appears missing.

Q: Which fixture scope is safest?

Function scope is the safest default because every test receives a fresh fixture instance and teardown occurs promptly. Broader class, module, package, or session scope trades isolation for setup cost and is risky for mutable accounts, clients, or databases. I share expensive infrastructure only when its state is immutable or reset by a documented boundary.

Q: How does a yield fixture tear down?

Code before yield performs setup, the yielded object is injected into the test, and code after it performs teardown. pytest resumes teardown even when the test fails, but an exception before reaching yield means that fixture has not registered its post-yield cleanup. For several partially acquired resources, context managers or ExitStack unwind only those acquisitions that succeeded.

Q: What is indirect parametrization?

@pytest.mark.parametrize(..., indirect=True) sends each parameter to a fixture through request.param instead of directly to the test. It is useful when compact data values must drive fixture setup, such as selecting a browser configuration or account role. A plainly named factory fixture is better when indirection makes the node ID or setup path hard to understand.

Q: When should tests be parametrized?

Parametrize cases that exercise the same behavior through the same setup and assertion shape. Meaningful case IDs make the failing row understandable without opening the data source. When scenarios carry different risks, workflows, or expected failures, separate tests communicate intent more clearly than a large conditional matrix.

Q: How do fixtures depend on fixtures?

A fixture requests another fixture by declaring its name as a parameter, and pytest resolves the dependency graph before running the test. This supports composition such as configuration feeding credentials, then both feeding an API client. I avoid deep chains because distant setup and teardown make ownership and failures difficult to trace.

Q: How do you test exceptions?

Put only the operation expected to fail inside pytest.raises, then inspect the captured exception if additional behavior matters. A wide context block can pass because an earlier setup statement raised the same broad exception type. Match stable message content only when the message is part of the contract, and prefer checking structured exception attributes when available.

Q: What does monkeypatch restore?

The monkeypatch fixture temporarily changes attributes, dictionary entries, environment variables, the working directory, or sys.path, then restores them after the requesting test or fixture. Patch the name in the module where the system under test looks it up, not automatically where the dependency was originally defined. For a collaborator with many behaviors, explicit dependency injection usually produces clearer tests than a stack of patches.

Q: Why use autospec?

unittest.mock.create_autospec and patching with autospec=True constrain a double to the real object's attributes and call signature. That catches misspelled methods and impossible argument lists that an unrestricted mock would accept. It still cannot prove HTTP semantics, serialization, deployment configuration, or server compatibility, so boundary tests remain necessary.

Q: Mock versus MagicMock?

Mock provides configurable attributes and calls, while MagicMock also supplies default implementations for many protocol methods such as iteration and context management. Those automatic methods are convenient when the real collaborator genuinely follows that protocol. I choose the smallest double that represents actual behavior because an accidentally iterable mock can let broken production assumptions pass.

Q: How do you test logs?

caplog captures logging records and can set the level for a specific logger during a pytest test. Assertions should inspect fields such as record.levelname, record.name, and record.getMessage() rather than timestamps or formatter punctuation. Logs supplement the behavioral assertion; they should not become the only proof that an operation succeeded or failed.

Q: tmp_path versus tmp_path_factory?

tmp_path gives each test a unique Path, while tmp_path_factory creates temporary directories from a wider-scoped fixture or hook. Per-test paths provide straightforward cleanup and isolation under parallel execution. I use the factory for an expensive immutable artifact, then keep each test's outputs in its own directory.

Q: How should flaky tests be handled?

First preserve the seed, timings, logs, screenshots, worker identity, environment, and original exception so the nondeterminism can be classified. Then look for uncontrolled time, shared data, order dependence, eventual consistency, or concurrency races and run an experiment that separates the leading hypotheses. Quarantine is temporary risk management with an owner and expiry date; automatic reruns alone only hide the defect rate.

import pytest

@pytest.fixture
def labels() -> list[str]:
    return []

@pytest.mark.parametrize("value", ["smoke", "regression"], ids=["fast", "full"])
def test_fixture_is_fresh(labels: list[str], value: str) -> None:
    labels.append(value)
    assert labels == [value]

13. Requests and API Automation Questions

Q: Why set a Requests timeout?

Requests does not impose a timeout unless the caller supplies one, so a test can wait indefinitely on a stalled socket. A (connect, read) tuple limits connection establishment and the wait between received bytes, but it is not a total business-operation deadline. I centralize a deliberate timeout policy in the client and test timeout translation without using real delays.

Q: What does raise_for_status check?

Response.raise_for_status() raises HTTPError for 4xx and 5xx responses and otherwise returns without validating the body. A 200 response may still contain invalid JSON, the wrong schema, or a business-level failure. After the status check, assertions must verify the endpoint contract and any persisted effect that the scenario requires.

Q: Why use requests Session?

A requests.Session reuses connections and persists cookies, headers, authentication, and other defaults across calls. Those conveniences reduce setup noise but can leak one test identity or mutated header into another test. I scope the session through a fixture, reset per-test state as needed, and always close it during teardown.

Q: How do params data and json differ?

params encodes URL query parameters, data sends form fields or caller-provided body content, and json serializes an object as JSON while setting the corresponding content type. Choosing the wrong argument can route the request through a different server validation path even when the Python value looks identical. Inspecting response.request.url, headers, and body is a practical way to diagnose encoding mistakes without logging secrets.

Q: How do you test pagination?

Follow the documented cursor or next link until the service signals completion, recording returned IDs and previously seen cursors. Assert termination, absence of unintended duplicates, ordering guarantees, and behavior at an empty or partial final page. A maximum-page guard turns a repeated cursor into a diagnostic failure instead of an infinite test.

Q: How do you test idempotency?

Send the same protected write more than once with the same idempotency key and verify that it creates one logical effect. Compare response status, resource identity, returned content, and persisted state rather than accepting any two successful responses. Add concurrent duplicate submissions when the risk includes races, because sequential calls do not exercise the same server path.

Q: What belongs in an API client?

An API client owns transport concerns such as base URL construction, authentication, serialization, timeouts, session lifecycle, and consistent error translation. Business expectations stay in tests or domain helpers so a failed assertion explains the requirement being checked. Returning the response or a typed domain result preserves evidence; reducing every outcome to True or False destroys useful diagnostics.

Q: How do you validate JSON?

First decode the response and report a controlled failure if its media type or syntax is wrong. Then validate required fields, types, optionality, and nested structure with explicit checks or the project's schema tool, followed by business invariants such as unique IDs or allowed transitions. Directly indexing a deep path before structural validation often turns a contract defect into an unhelpful KeyError.

Q: Which negative tests matter?

Derive negative cases from the endpoint contract: missing or malformed input, unsupported media, authentication failure, insufficient permission, conflict, rate limit, and invalid state transitions. Check the precise safe error shape and confirm that rejected writes produced no unintended side effect. Avoid asserting undocumented prose word for word when a stable code or structured field exists.

Q: How should tokens be handled?

Load tokens from the runtime secret mechanism, grant the minimum scope, and keep them out of source, fixtures, reports, and recorded traffic. Redaction must happen before request headers or command lines reach the logger because cleaning the final message can miss structured fields. Tests can use unmistakably fake tokens to verify that observability code emits a placeholder rather than the credential.

Q: When are API retries safe?

Retry only failures classified as transient, and only when the operation is idempotent or protected against duplicate effects. Bound attempts or elapsed time, honor Retry-After where applicable, add jitter for distributed clients, and expose each retry in diagnostics. Assertions, validation failures, authentication errors, and deterministic 4xx responses should fail immediately.

Q: How do you mock Requests?

Patch or inject the transport object actually referenced by the client, then configure realistic status, headers, JSON, text, and raised exceptions for the branch under test. Assert the outgoing method, URL, timeout, and safe request fields without coupling the test to irrelevant implementation details. Keep controlled integration coverage because a mock cannot reveal TLS, proxy, encoding, or server-contract mismatches.

Q: Contract versus end-to-end tests?

A contract test checks the agreement at one boundary, such as request fields and response schema, and can usually identify which consumer or provider changed incompatibly. An end-to-end test traverses deployed components and proves that a critical workflow integrates in its real environment. A balanced suite uses focused contracts for broad compatibility coverage and a smaller number of end-to-end paths for deployment confidence.

import requests

def fetch_health(session: requests.Session, base_url: str) -> dict[str, str]:
    response = session.get(f"{base_url}/health", timeout=(2.0, 5.0))
    response.raise_for_status()
    payload = response.json()
    if payload.get("status") != "ok":
        raise ValueError("health response did not report ok")
    return payload

14. Data Structures and Coding Katas With Solutions

Q: How do you find the first unique character?

Count characters once, then scan the original string and return the first character whose count is one. Counter keeps the intent direct, and the two passes remain O(n) time with O(k) space for k distinct characters. Clarify case folding and Unicode normalization because preprocessing can change both identity and the value returned.

Q: How do you check balanced brackets?

Push each opening bracket onto a stack and require every closer to match the most recent opener. Return false for an early closer, a mismatched pair, or a nonempty stack after the scan. This is O(n) time and O(n) worst-case space, with the input contract deciding whether non-bracket characters are ignored or rejected.

Q: How do you merge intervals?

Sort intervals by start, append a disjoint interval, and otherwise extend the end of the last merged interval. Sorting dominates at O(n log n), followed by a linear scan and up to O(n) output space. Ask whether touching endpoints overlap and reject reversed intervals before merging so boundary behavior is explicit.

Q: How do you compare unordered lists with duplicates?

collections.Counter compares hashable elements as a multiset, preserving how often each value occurs while ignoring order. Set equality is insufficient because [1, 1, 2] and [1, 2, 2] collapse to the same set. For dictionary records, derive a stable hashable projection or count a canonical representation only after defining which fields matter.

Q: How do you implement a queue?

Use collections.deque, calling append to enqueue and popleft to dequeue in O(1) time at either end. Removing index zero from a list is O(n) because all remaining references shift. Define whether an empty dequeue raises IndexError, returns a sentinel, or blocks, since those are different queue contracts.

Q: How do you find duplicate IDs?

Maintain one set for IDs already seen, another for duplicates already reported, and a list for first-duplicate order. Each item requires expected O(1) set work, producing expected O(n) time and O(k) auxiliary space. Validate missing IDs and decide whether whitespace or case normalization belongs to the domain before inserting keys.

Q: How do you group records?

A defaultdict(list) can append each record under its grouping key without a separate initialization branch. Missing or null keys need an explicit reject, skip, or sentinel policy so malformed records do not silently join a misleading bucket. Returning a plain dictionary at the boundary avoids exposing default-creation behavior to callers.

Q: How do you intersect large collections?

Build a set from the smaller collection and scan the larger one, which limits auxiliary memory to the smaller distinct input. A plain set intersection returns unique shared values and discards frequency and encounter order. If multiplicity matters, intersect Counters with minimum counts; if streaming order matters, record matches as the larger side is consumed.

Q: How do you reverse words?

" ".join(text.split()[::-1]) reverses whitespace-delimited words but intentionally collapses runs of whitespace and trims the ends. That is correct only when normalized spacing is part of the expected result. If exact separators must survive, tokenize words and whitespace separately, then reverse only the word tokens under a clearly tested rule.

Q: How do you parse a huge log?

Open the file with an explicit encoding and iterate line by line so memory does not grow with file size. Track line numbers and convert malformed records into either a precise exception, a rejected-record stream, or a documented skip metric. Tests should include an invalid middle line and verify that the chosen policy does not lose surrounding valid events.

Q: How do you rotate a list?

For a right rotation, normalize k with modulo length and concatenate the last k elements with the prefix. Handle an empty input before modulo to avoid division by zero, and decide whether the function returns a copy or mutates the original. Slicing uses O(n) extra space; an in-place reversal method is available if the constraint requires constant auxiliary space.

Q: How do you find missing range values?

Construct the expected range as a set and subtract observed values when the range is moderate and clarity matters. The method is linear in the range width and uses range-proportional memory, regardless of how few values were supplied. Arithmetic formulas use less space only under stricter assumptions such as exactly one missing value and no duplicates, so those assumptions must be tested first.

Q: How do you design an LRU cache?

An LRU cache needs O(1) key lookup plus recency ordering, commonly a dictionary paired with a doubly linked list or an OrderedDict. Both reads and updates move the key to most-recent position, and insertion beyond capacity evicts the least-recent key. Tests cover misses, updates, eviction order, zero capacity, and the stated thread-safety policy rather than assuming concurrent safety.

Runnable kata: balanced brackets

A stack is the natural structure because the most recent unmatched opener must be closed first. This implementation rejects unknown characters only if the caller asks it to process an expression made entirely of brackets; for mixed source text, filter or define the contract before calling it.

def has_balanced_brackets(text: str) -> bool:
    pairs = {')': '(', ']': '[', '}': '{'}
    stack: list[str] = []

    for char in text:
        if char in pairs.values():
            stack.append(char)
        elif char in pairs:
            if not stack or stack.pop() != pairs[char]:
                return False

    return not stack


assert has_balanced_brackets('([]{})') is True
assert has_balanced_brackets('([)]') is False
assert has_balanced_brackets(']') is False
assert has_balanced_brackets('(') is False

The loop visits each character once, so time is O(n). In the worst case, every character is an opener and stack space is O(n). Mentioning the early-closing case matters because an implementation that blindly pops raises instead of returning the defined result.

Runnable kata: first unique character

Counter makes the counting intent obvious. The second pass is necessary because a frequency mapping answers how often a character occurs but does not itself express which unique character appeared first.

from collections import Counter


def first_unique_character(text: str) -> str | None:
    counts = Counter(text)
    return next((char for char in text if counts[char] == 1), None)


assert first_unique_character('swiss') == 'w'
assert first_unique_character('aabb') is None
assert first_unique_character('') is None

This solution is O(n) time and O(k) space for k distinct characters. Before coding in an interview, ask whether S and s are equivalent and whether Unicode text must be normalized. Applying casefold or Unicode normalization changes the returned representation, so that decision belongs in the contract rather than an unexplained preprocessing step.

Runnable kata: preserve duplicate IDs once

Test result feeds often need an ordered list of identifiers that appeared more than once. One set records every observed ID, a second prevents the same duplicate from being appended repeatedly, and a list retains the order in which duplication was first detected.

from collections.abc import Iterable


def duplicate_ids(values: Iterable[str]) -> list[str]:
    seen: set[str] = set()
    reported: set[str] = set()
    duplicates: list[str] = []

    for value in values:
        if value in seen and value not in reported:
            reported.add(value)
            duplicates.append(value)
        else:
            seen.add(value)

    return duplicates


assert duplicate_ids(['T1', 'T2', 'T1', 'T1', 'T2']) == ['T1', 'T2']
assert duplicate_ids([]) == []

Expected time is O(n), with O(k) auxiliary space for distinct identifiers. A production version may reject empty IDs or normalize case, but silently applying those rules in an interview changes the input semantics. State the validation policy and add tests for it.

Runnable kata: merge intervals

Sorting converts a global overlap problem into a comparison with the last merged interval. The example treats touching endpoints as overlapping because the next start is allowed to equal the current end.

from collections.abc import Iterable


def merge_intervals(
    intervals: Iterable[tuple[int, int]],
) -> list[tuple[int, int]]:
    ordered = sorted(intervals)
    merged: list[tuple[int, int]] = []

    for start, end in ordered:
        if start > end:
            raise ValueError(f'invalid interval: {(start, end)}')
        if not merged or start > merged[-1][1]:
            merged.append((start, end))
            continue
        previous_start, previous_end = merged[-1]
        merged[-1] = (previous_start, max(previous_end, end))

    return merged


assert merge_intervals([(5, 7), (1, 3), (2, 4)]) == [(1, 4), (5, 7)]
assert merge_intervals([(1, 2), (2, 3)]) == [(1, 3)]
assert merge_intervals([]) == []

Sorting costs O(n log n), and the scan is O(n). The returned list uses O(n) space when nothing overlaps. If the interviewer says touching ranges must remain separate, change the comparison from start > previous_end to start >= previous_end and update the boundary test before changing any other code.

Runnable kata: compare API records by stable ID

A useful comparison reports missing, unexpected, and changed records separately. Building dictionaries makes matching direct and produces diagnostic output that a failed assertion can print.

from typing import Any


def compare_records(
    expected: list[dict[str, Any]],
    actual: list[dict[str, Any]],
) -> dict[str, Any]:
    expected_by_id = {record['id']: record for record in expected}
    actual_by_id = {record['id']: record for record in actual}

    expected_ids = set(expected_by_id)
    actual_ids = set(actual_by_id)
    shared_ids = expected_ids & actual_ids

    return {
        'missing': sorted(expected_ids - actual_ids),
        'unexpected': sorted(actual_ids - expected_ids),
        'changed': {
            record_id: {
                'expected': expected_by_id[record_id],
                'actual': actual_by_id[record_id],
            }
            for record_id in sorted(shared_ids)
            if expected_by_id[record_id] != actual_by_id[record_id]
        },
    }


result = compare_records(
    [{'id': '1', 'active': True}, {'id': '2', 'active': True}],
    [{'id': '1', 'active': False}, {'id': '3', 'active': True}],
)
assert result['missing'] == ['2']
assert result['unexpected'] == ['3']
assert list(result['changed']) == ['1']

The dictionary comprehensions assume IDs are present, hashable, and unique. If duplicates are possible, they must be validated first because a comprehension would silently retain the final record. That observation is often the strongest part of the interview answer: an efficient comparison is worthless if its indexing step destroys evidence that the source violated the contract.

How Interviewers Grade Your Answers

Interviewers score the contract, correctness, readable naming, boundary coverage, complexity, and automation judgment. Narrate decisions rather than every keystroke, solve the simplest correct version, run representative tests, and then discuss improvements. Senior answers also address parallel isolation, secret handling, artifacts, observability, ownership, and maintenance cost.

Review data structures for SDET interviews, pytest interview questions, and Python test automation interview questions for focused practice.

Interview Questions and Answers

Q: What is the difference between a list and a tuple?

Both are ordered sequences. A list is mutable, while a tuple is immutable as a container and can be hashable when all its elements are hashable. I use tuples for fixed records or keys when immutability is part of the contract, not as a performance superstition.

Q: What is the difference between == and is?

== compares values using equality behavior. is compares object identity. I use is None for the singleton and == for ordinary value comparisons.

Q: Why are mutable default arguments dangerous?

The default object is created when the function is defined and reused across calls. Mutations can therefore leak state between calls or tests. I use None as the default and create a fresh object inside.

Q: When would you use a set in test code?

A set is useful for uniqueness, membership, and set comparisons when ordering and duplicates are not part of the contract. I would not use it when I need original order or duplicate counts without adding another structure.

Q: What is a generator?

A generator produces values lazily through iteration instead of building the entire result immediately. It can reduce memory use for streams, but it is usually consumed once and may defer exceptions until iteration.

Q: How do pytest fixtures work?

Fixtures declare reusable dependencies by name. pytest resolves their dependency graph, runs setup according to scope, injects the value, and performs teardown for yield fixtures. I keep mutable fixtures narrow and cleanup reliable.

Q: When should tests be parametrized?

Parametrize when several inputs exercise the same behavior and assertion structure. Give cases meaningful IDs when needed. Separate cases when their setup, expected behavior, or failure meaning differs significantly.

Q: How do you test an exception with pytest?

Use pytest.raises as a context manager, optionally matching a stable part of the message or inspecting the exception. The test should also prove the operation reached the intended invalid condition.

Q: What should a retry implementation include?

A retry needs a narrow exception policy, maximum attempts or deadline, delay strategy, and observability. It should wrap only safe operations or idempotent writes. Tests should cover immediate success, eventual success, final failure, and nonretryable errors.

Q: What is mocking, and what does it not prove?

Mocking replaces a collaborator with controlled behavior so a unit can be tested deterministically. It does not prove the real integration, protocol, configuration, or schema. I combine it with contract and end-to-end coverage according to risk.

Q: How would you compare two API result sets?

First clarify whether order and duplicates matter and which key identifies a record. I may use lists for ordered equality, Counters for multiset equality, or dictionaries keyed by stable ID for field comparison. I report missing, unexpected, and changed records separately.

Q: How do you debug an intermittent automation failure?

I reproduce the exact context, preserve the original traceback and artifacts, classify the layer, and identify the first divergence. I form one hypothesis and run a discriminating experiment. I do not start with an unexplained retry or sleep.

Q: What is time complexity of dictionary lookup?

It is expected O(1) for normal hash-table use, with memory cost for the table. I avoid claiming an unconditional guarantee because collisions and adversarial conditions can affect worst-case behavior.

Q: How do you keep Python test code maintainable?

I keep business intent visible, isolate I/O, use typed boundaries where useful, create deterministic data, and avoid hidden globals. Fixtures have coherent responsibilities, assertions are diagnostic, and dependencies are pinned and reviewed.

A useful scoring rubric distinguishes a correct answer from an interview-ready answer. Correct code earns the baseline. Explicit assumptions show that you understand requirements, focused tests show that you can challenge your own implementation, and precise complexity shows that you recognize scaling costs. Diagnostic failure messages and careful cleanup demonstrate experience maintaining suites after the initial coding round.

When you do not remember an API exactly, say what you know and verify the uncertain detail instead of inventing a method. Interviewers usually respond well to disciplined uncertainty because automation engineers routinely work across libraries and versions. Explain the invariant you need, choose a standard approach, and keep the uncertain call isolated. For take-home work, run the code in a clean environment, pin dependencies, include the command used, and ensure no secret or machine-specific path entered the submission.

In behavioral follow-ups, connect the answer to an incident or measurable engineering outcome without fabricating numbers. Describe the failure signal, your investigation, the change, and how regression coverage proved it. Ownership includes monitoring the fix and documenting why the old design failed, not merely merging a patch.

Common Mistakes

  • Coding before clarifying order, duplicates, invalid input, and mutation.
  • Choosing a one-liner that obscures the contract.
  • Using is for value equality.
  • Using mutable default arguments.
  • Catching Exception and continuing without a defined recovery.
  • Retrying assertions, unsafe writes, or deterministic bugs.
  • Testing only a happy path or printing output without assertions.
  • Using a set when order or duplicate count matters.
  • Overusing mocks and claiming integration confidence.
  • Building session-scoped mutable fixtures that leak state.
  • Patching the wrong import location.
  • Omitting timeouts around external I/O.
  • Claiming O(1) behavior without stating hash-table assumptions.
  • Ignoring cleanup failures, sensitive logs, or parallel execution.

Keep Practicing

Continue with Python coding interview questions for testers, API testing interview questions, QA automation engineer interview questions, and the pytest tutorial for beginners. Use /dashboard for the QAJobFit practice track and /resume-studio to align evidence with the target role. Repeat each kata without notes, change one requirement, and explain the test impact aloud.

Conclusion

The best way to prepare for Python coding interview questions testers encounter is to practice complete engineering answers: clarify the contract, choose a fitting data structure, write readable code, test boundaries, and explain complexity and automation relevance. Syntax is only one part of the score.

Start with the duplicate-removal and API-validation exercises in this guide. Reimplement them without looking, change one requirement, and explain how the tests and complexity change. That practice builds the adaptability a QA or SDET coding round actually measures.

Interview Questions and Answers

What is the difference between a list and a tuple?

Both are ordered sequences. Lists are mutable, while tuples are immutable containers and can be hashable when all elements are hashable. I use a tuple when fixed structure or key suitability is part of the contract.

What is the difference between == and is?

The == operator compares values according to equality behavior. The is operator compares object identity. I use is None for the singleton and == for ordinary values.

Why should you avoid a mutable default argument?

The default object is created once when the function is defined and reused across calls. Mutating it can leak state between calls and tests. I default to None and create a fresh object inside.

When would you use a set in automation code?

I use a set for unique values, membership checks, and set relationships when order and duplicate count do not matter. If order must be preserved, I pair it with a list. If counts matter, Counter may fit better.

What is a Python generator?

A generator yields values lazily as it is iterated. It can reduce memory use for large streams. It is commonly consumed once, and work or exceptions may be deferred until iteration.

How do pytest fixtures work?

Fixtures declare dependencies by name, and pytest resolves and executes them according to scope. A yield fixture exposes a value and then runs teardown. I keep mutable resources test-scoped unless sharing is explicitly safe.

When should you use pytest parametrization?

I use parametrization when multiple data cases exercise the same behavior and assertion structure. I give cases meaningful IDs where useful. If setup or failure meaning differs, separate tests are clearer.

How do you test that Python code raises an exception?

With pytest, I use pytest.raises as a context manager and optionally match a stable message or inspect the captured exception. The test data must reach the intended invalid condition, not fail earlier for another reason.

What should a safe retry helper include?

It needs explicit retryable exceptions, bounded attempts or deadline, delay behavior, and observable failures. It should wrap only safe operations or protected idempotent writes. Tests cover immediate success, eventual success, exhaustion, and nonretryable errors.

What does mocking prove?

Mocking proves how the unit behaves against controlled collaborator behavior. It does not prove the real protocol, service, configuration, or schema. I pair mocks with contract or end-to-end tests based on integration risk.

How would you compare API records when order does not matter?

I first identify a stable unique key and clarify duplicate semantics. Dictionaries keyed by ID help report missing, unexpected, and changed records. A set is enough only when field differences and duplicates are irrelevant.

What is the expected complexity of dictionary lookup?

Normal dictionary lookup is expected O(1) because Python dictionaries use a hash table. Memory use and hash quality matter, and worst-case behavior is not the same guarantee. I state those assumptions when complexity is discussed.

How do you debug an intermittent Python test?

I preserve the original traceback, seed, inputs, logs, environment, and concurrency. I find the first incorrect state, classify the layer, and test one hypothesis. I avoid masking it with a broad retry or sleep.

How do you design testable Python automation?

I separate pure transformation logic from external I/O, inject collaborators where practical, use explicit timeouts, and keep state ownership clear. Tests then cover pure rules quickly while a smaller set validates real boundaries.

What is the difference between shallow and deep copy?

A shallow copy creates a new outer container but retains references to nested objects. A deep copy recursively copies nested content according to object behavior. In test data, a shallow copy can allow one case to mutate another's nested state.

Frequently Asked Questions

What Python coding questions are asked in QA interviews?

Common questions cover strings, lists, dictionaries, sets, duplicate detection, counting, JSON validation, file parsing, retries, fixtures, mocking, and debugging. Senior rounds may add framework design, concurrency, APIs, and complexity tradeoffs.

How much Python should a tester know?

Know core collections, functions, modules, exceptions, context managers, iterators, type hints, files, JSON, and the test framework used by the role. You should be able to write, test, debug, and explain small automation components independently.

Is pytest required for Python QA interviews?

It depends on the role, but pytest is common and worth learning. Be comfortable with assertions, fixtures, scopes, parametrization, temporary paths, monkeypatching, and exception tests.

Should I solve Python questions without built-in functions?

Ask what the interviewer wants to evaluate. Use standard library tools when allowed, then explain or implement the underlying logic if requested. Readability and correct complexity usually matter more than avoiding every built-in.

Do testers need data structures and algorithms?

Yes, at a practical level. Lists, dictionaries, sets, queues, sorting, parsing, and complexity appear often in automation and interviews, even when advanced competitive algorithms are not required.

How should I practice Python coding for an SDET interview?

Use timed exercises that include clarification, runnable code, boundary tests, complexity, and a follow-up requirement change. Also review flawed automation code and explain debugging evidence.

What makes a Python interview answer strong?

A strong answer states assumptions, handles defined edge cases, uses an appropriate data structure, preserves errors, and includes meaningful tests. It also explains how constraints could change the design.

Related Guides