← All interview Q&A
Free · Top 25 questions

Selenium + Python interview questions

The 25 Selenium + Python questions interviewers keep asking — pytest, waits, framework design and the comparisons you must get right.

Each answer has three parts — Concept (the real difference), Interview Strategy (how to say it in the room), and Code (a quick glimpse). This is the same format as my paid kits.

250+ questions in the full kit

SDET Selenium Python Foundation Kit 2026

This is a glimpse. The full Foundation Kit covers Python for SDETs, pytest, locators, waits, POM, fixtures, API testing, CI/CD and the scenario rounds — interview-ready answers throughout.

01

Selenium with Python vs Selenium with Java — which should you pick?

Mid
Concept

The Selenium WebDriver API is the same W3C standard underneath, so what the browser can actually do is identical in both languages. The real difference is the language and the ecosystem around it. Python is shorter to write, reads almost like English, and pairs naturally with pytest, which makes it quick to stand up a framework. Java is more verbose but pairs with TestNG and Maven, and it still owns the larger enterprise QA job pool in India. The honest answer is that you match the binding to the team you sit in, not to a personal favourite.

Interview Strategy

Lead with the point that the binding does not change what Selenium can do — the W3C protocol underneath is identical, so this is a language and ecosystem question, not a capability one. The trap to avoid is sounding like a Python fan-girl who dismisses Java; that reads as junior in an Indian enterprise interview where Java is everywhere.

How to phrase it

I would start by saying the WebDriver layer is the same W3C standard in both, so the browser automation itself is identical — the choice is really about the language and its ecosystem. Python is more concise and reads almost like plain English, and it pairs beautifully with pytest, so I can stand up a framework very fast and the data-driven work stays clean. Java is more ceremony, but it has TestNG, Maven, and frankly the bigger enterprise job pool here in India, so a lot of large QA teams are standardised on it. The mature answer I always give is that I match the test language to the development team's stack. If the application is built in Java, I lean towards Java tests so the developers can read and even contribute to them, because a test nobody on the team can read is a test that rots. So it is less which is better and more which fits the room I am in.

Key points to hit

  • WebDriver is the same W3C protocol in both — capability is identical.
  • Python is concise and pairs with pytest; Java pairs with TestNG and Maven.
  • Java still has the larger enterprise QA job pool in India.
  • Match the test language to the dev team's stack so engineers can read the suite.
  • Avoid bashing either language — that reads as inexperience.
Code
# Python — concise
driver.find_element(By.ID, "pay").click()

# Java — more ceremony (for contrast)
# driver.findElement(By.id("pay")).click();
02

pytest vs unittest — which test runner for Selenium?

Mid
Concept

unittest is Python's built-in xUnit-style framework: test classes that subclass TestCase, setUp and tearDown methods, and assertEqual-style assertions. pytest is a third-party runner that uses plain assert statements, a powerful fixture system, and a deep plugin ecosystem — parametrize for data-driving, xdist for parallel runs, and HTML or Allure reporting. The detail interviewers reward is that pytest can run your existing unittest tests unchanged, so adopting it is never a rewrite, only a gradual migration.

Interview Strategy

State a clear default — pytest — and back it with the two things that actually save you time on a real suite: fixtures and parametrize. The trap is dismissing unittest entirely; acknowledge it is the right call when you cannot add a dependency, for example a locked-down corporate environment.

How to phrase it

I default to pytest, and the reason is purely how much boilerplate it removes from a real suite. Fixtures let me share a driver across files with proper scope control, and parametrize lets me run one test body over many data rows, each reported separately. The plugin ecosystem is the other half — xdist for parallel runs, and HTML or Allure reports — and those are exactly the things a production suite needs. unittest is Python's built-in framework, so it is the right choice when I genuinely cannot add a third-party dependency, say a restricted corporate environment. The senior signal I always drop in is that pytest can run unittest tests as they are, so a team sitting on a unittest suite does not have to rewrite anything — they install pytest, keep their old tests running, and write new tests in the cleaner style. So migration is gradual, not a big-bang rewrite, which is usually what unblocks the decision.

Key points to hit

  • unittest is built-in, class-based, with setUp/tearDown and assertEqual.
  • pytest uses plain assert, fixtures, and a rich plugin ecosystem.
  • parametrize and xdist are what real suites actually need.
  • pytest runs existing unittest tests unchanged — migration is gradual.
  • unittest still wins when you cannot add a dependency.
Code
# pytest — plain assert, fixture injected by name
def test_title(driver):
    assert driver.title == "Home"

# unittest — class-based, assertEqual
# class TestHome(unittest.TestCase):
#     def test_title(self):
#         self.assertEqual(self.driver.title, "Home")
03

Selenium vs Playwright in Python — what is the real difference?

Mid
Concept

Selenium is the long-standing W3C WebDriver standard with the largest job pool and the widest real-browser and grid support. Playwright is the newer Microsoft framework with built-in auto-waiting, network interception, tracing, and a single API across Chromium, Firefox, and WebKit. They solve the same problem, but Playwright removes most of the explicit synchronisation work that Selenium leaves to you — in Selenium you write WebDriverWait yourself, in Playwright the locator auto-waits before every action. In 2026 the common pattern is greenfield suites on Playwright, mature suites staying on Selenium.

Interview Strategy

Do not bash either tool — interviewers hear that as inexperience. Frame it as a genuine trade-off: Selenium wins on job volume and ecosystem reach, Playwright wins on speed and developer experience because it auto-waits. The trap is claiming Playwright makes Selenium obsolete; in India the Selenium job pool is still by far the larger one.

How to phrase it

I would not bash either, because they genuinely win on different axes. Selenium is the W3C WebDriver standard — it has the largest job pool, the widest grid and real-device reach, and almost every company already runs it somewhere. Playwright is newer and its headline feature is auto-waiting: the locator waits for the element to be actionable before every click, so a lot of the flakiness you fight in Selenium just disappears, and you get network interception and tracing built in. The honest difference is synchronisation — in Selenium I write the WebDriverWait myself, in Playwright the framework handles it. So my actual position for 2026 is that for a brand-new suite I would seriously consider Playwright for the developer experience and speed, but where Selenium is already running and maintained, there is no good reason to rip it out. And given the Indian job market, Selenium is still the skill that opens the most doors, so I keep both sharp.

Key points to hit

  • Both are the same job: drive a browser. Selenium is the W3C standard, Playwright is newer.
  • Playwright auto-waits before every action; Selenium leaves synchronisation to you.
  • Playwright ships network interception and tracing built in.
  • Selenium wins on job volume, grid reach, and existing adoption in India.
  • 2026 pattern: greenfield on Playwright, mature suites stay on Selenium.
Code
# Selenium — you manage the wait
WebDriverWait(driver, 10).until(
    EC.element_to_be_clickable((By.ID, "pay"))
).click()

# Playwright — auto-waits, no explicit wait
# page.get_by_role("button", name="Pay").click()
04

Implicit wait vs explicit wait — which one and why?

Junior
Concept

An implicit wait is a global setting — driver.implicitly_wait of ten seconds — that tells the driver to poll for an element's presence on every find_element for up to that long. An explicit wait, WebDriverWait, waits for a specific condition on a specific element: visible, clickable, text present, and so on. Explicit is the one you reach for, because real flakiness is about state, not just existence — an element can be in the DOM but not yet clickable. The rule interviewers want to hear is that you never mix the two.

Interview Strategy

Lead with the rule the interviewer is fishing for: never mix implicit and explicit waits, because the two stack unpredictably and inflate your timeouts. The trap is describing implicit wait as a convenient global default; in a serious suite you set no implicit wait at all and drive everything with explicit conditions.

How to phrase it

The short version is that I set no implicit wait and use explicit waits everywhere. An implicit wait is a global setting that polls for an element's presence on every find, but it only knows about presence — it cannot wait for an element to become clickable or visible, which is where the real flakiness lives. An explicit wait, WebDriverWait, waits for a specific condition on a specific element, so I can say wait until this button is clickable, then click. The rule I always state, because it is the one interviewers are checking for, is that you must never mix the two. If you set a ten-second implicit wait and also use explicit waits, the two stack in ways that are very hard to predict and your timeouts balloon — a find that should fail fast now takes far longer. So my standard is implicit wait left at zero, and an explicit WebDriverWait per condition, matched to what I am actually about to do with the element.

Key points to hit

  • Implicit wait is global and only polls for presence in the DOM.
  • Explicit wait (WebDriverWait) waits for a specific condition on a specific element.
  • Real flakiness is about state — visible, clickable — not just existence.
  • Never mix implicit and explicit waits; they stack and inflate timeouts.
  • Standard: implicit wait at zero, explicit wait per condition.
Code
wait = WebDriverWait(driver, 10)
wait.until(EC.visibility_of_element_located((By.ID, "otp")))

# Do NOT also set this — it stacks with the explicit wait:
# driver.implicitly_wait(10)
05

time.sleep() vs WebDriverWait — why is sleep an anti-pattern?

Junior
Concept

time.sleep pauses the whole thread for a fixed number of seconds regardless of what the page is doing. That makes it both slow — you always pay the full wait even when the element was ready in half a second — and flaky, because on a slow run the element may still not be ready when the sleep ends. WebDriverWait polls for a real condition and continues the instant it is met, up to a maximum timeout, so it is faster on the common case and more reliable on the slow case. Hard sleeps are the single biggest cause of slow, brittle suites.

Interview Strategy

Name time.sleep for what it is — a hard sleep — and say plainly that it never belongs in a committed test, only as a throwaway debugging line you delete. The trap is defending it as occasionally necessary; the better framing is that any case where you reach for sleep is actually a case for a better explicit condition.

How to phrase it

I would call time.sleep what it is — a hard sleep — and say it has no place in a committed test. The problem is it is both slow and flaky at the same time. It is slow because you always pay the full duration even if the element was ready in half a second, so a suite full of five-second sleeps is needlessly minutes long. And it is flaky because if a run is slow, the element still might not be ready when the sleep ends, so the test fails anyway. WebDriverWait fixes both: it polls for a real condition and continues the instant the condition is met, up to a timeout, so it is fast on the good case and patient on the slow case. The only time I will type time.sleep is as a throwaway line while I am debugging interactively, and I delete it before I commit. If I ever feel I need a sleep in a real test, that is a signal I have not found the right condition to wait on yet — so I go and find it.

Key points to hit

  • time.sleep pauses a fixed time regardless of page state — slow and flaky.
  • You always pay the full duration even when the element is ready early.
  • WebDriverWait polls and continues the instant the condition is met.
  • Hard sleeps are the biggest single cause of slow, brittle suites.
  • Reaching for a sleep means you haven't found the right condition to wait on.
Code
# Flaky and slow — never commit this
import time
time.sleep(5)

# Deterministic — continues the instant the result appears
WebDriverWait(driver, 10).until(
    EC.visibility_of_element_located((By.ID, "result"))
)
06

find_element vs find_elements — what is the gotcha?

Junior
Concept

find_element returns the first matching element and raises NoSuchElementException if nothing matches. find_elements returns a list of all matches, and crucially returns an empty list — not an exception — when nothing matches. That difference is the whole point: if you want to check whether something exists without wrapping everything in try and except, you use find_elements and test whether the list is empty. The gotcha is using find_element for an existence check and then having to catch an exception you did not need.

Interview Strategy

Anchor the answer on the behaviour difference that actually matters: one raises, the other returns an empty list. The trap is the existence-check antipattern — candidates wrap find_element in try/except just to see if an element is present, when find_elements and a truthiness check reads far cleaner.

How to phrase it

The core difference is what happens when nothing matches. find_element returns the first match and raises NoSuchElementException if there is none. find_elements returns a list of every match, and when there is no match it returns an empty list rather than throwing. That single difference drives how I use them. When I genuinely expect exactly one element and its absence is a real failure, I use find_element and let the exception surface. But when I want to check whether something is present — say, is an error banner showing — I use find_elements and just test whether the list is non-empty, because an empty list is falsy and reads cleanly in an if. The gotcha I always call out is wrapping find_element in a try and except purely to do an existence check. That is the long, ugly way to do something find_elements gives you in one line. And the other small trap is looping with find_element when you actually meant find_elements to get the whole set.

Key points to hit

  • find_element returns the first match, raises NoSuchElementException if none.
  • find_elements returns a list, returns an empty list when nothing matches.
  • Use find_elements for existence checks — an empty list reads cleanly in an if.
  • Antipattern: wrapping find_element in try/except just to check presence.
  • Use find_element for the single element whose absence is a real failure.
Code
# Clean existence check — empty list is falsy
if driver.find_elements(By.CSS_SELECTOR, ".error"):
    pytest.fail("error message was shown")

# Raises NoSuchElementException if missing
submit = driver.find_element(By.ID, "submit")
07

CSS selector vs XPath — which locator should you prefer?

Mid
Concept

CSS selectors are shorter, generally faster, and read more naturally for matching by id, class, or attribute. XPath is more powerful: it can traverse upward to a parent or ancestor, match on an element's visible text, and express relationships CSS simply cannot. The modern browsers have narrowed the speed gap, so the honest decision is mostly about readability and capability. The senior point sitting above both is that the most stable locator is a dedicated test id, not a clever selector of either kind.

Interview Strategy

Give a default — CSS for most things — and a precise reason to switch — text matching or axis traversal like finding a parent. The trap is the absolute-XPath habit, those long //div/div/span chains that break the moment the layout shifts; flag that explicitly. End on the real senior point: stable test ids beat clever selectors.

How to phrase it

My default is CSS, because it is shorter, reads more naturally for id, class, and attribute matches, and is generally fast. I reach for XPath only when CSS genuinely cannot do the job — and there are really two of those cases. One is matching on visible text, where XPath lets me say the button whose text is Pay. The other is axis traversal, where I need to go up to a parent or ancestor, which CSS cannot express. The trap I am careful to avoid is absolute XPath — those long slash-div-slash-div chains copied from the browser — because they break the instant the layout changes. So I keep my XPath relative and anchored to something stable. But the point I always land on, because it is the one that marks a senior, is that the best locator is not a clever CSS or XPath at all — it is a dedicated test id that developers add for the purpose. A data-testid attribute does not change when the design changes, so I push for those first and treat CSS and XPath as what I use when they are not available.

Key points to hit

  • CSS is shorter, faster, and natural for id, class, and attribute matches.
  • XPath handles text matching and axis traversal (parent/ancestor) that CSS cannot.
  • Modern browsers narrowed the speed gap — readability matters more now.
  • Avoid absolute XPath; it breaks on any layout change.
  • The most stable locator is a dedicated test id, not a clever selector.
Code
# CSS — preferred for id / class / attribute
driver.find_element(By.CSS_SELECTOR, "input[name='email']")

# XPath — when you need text or a parent/ancestor
driver.find_element(By.XPATH, "//button[text()='Pay']")

# Best of all — a stable test id
driver.find_element(By.CSS_SELECTOR, "[data-testid='pay-button']")
08

pytest fixture vs setUp/tearDown methods — what changes?

Mid
Concept

unittest uses setUp and tearDown methods that run before and after every test in a class, and they are tied to that one class. A pytest fixture is a standalone, reusable function that tests request by name as a parameter. It carries a scope — function, class, module, or session — so you control how often it runs, and a single yield statement holds both the setup before it and the teardown after it in one readable place. The flexibility comes from scope plus composition: fixtures can request other fixtures.

Interview Strategy

Frame the difference as flexibility, not syntax: scope and composition. The trap is presenting them as equivalent ways to set up a driver; the real win is that a fixture's scope lets you spin a driver up once per session instead of once per test, which setUp can never do cleanly.

How to phrase it

setUp and tearDown are methods that run before and after every test in a class, and they are bound to that one class. A pytest fixture is a free-standing function that any test can request just by naming it as a parameter, and that unlocks two things setUp cannot do well. The first is scope. A fixture can be function-scoped, class-scoped, module-scoped, or session-scoped, so I can say spin this driver up once for the entire session rather than once per test, which is a big speed win on a heavy suite. setUp always runs per test. The second is composition — a fixture can request other fixtures, so I build a logged-in-driver fixture on top of a plain driver fixture, and they layer cleanly. The detail I like to highlight is the yield. A single fixture function holds the setup before the yield and the teardown after it, so the create-and-clean-up logic for one resource sits together in one readable place, instead of being split across a setUp and a tearDown at opposite ends of a class.

Key points to hit

  • setUp/tearDown run per test and are bound to one class.
  • A fixture is a standalone function tests request by name as a parameter.
  • Fixture scope (function/class/module/session) controls how often it runs.
  • Fixtures compose — one can request another to layer setup.
  • A yield fixture keeps setup and teardown in one readable place.
Code
@pytest.fixture
def driver():
    d = webdriver.Chrome()
    yield d          # the test runs here
    d.quit()         # teardown after the test

def test_home(driver):
    driver.get("https://app.test")
    assert driver.title == "Home"
09

conftest.py vs in-test setup — where should fixtures live?

Mid
Concept

conftest.py is a special file pytest discovers automatically: any fixture defined in it is available to every test file in the same folder and all subfolders, with no import statement anywhere. In-file setup keeps a fixture local to the one module that defines it. So conftest.py is exactly how you make a cross-cutting fixture — a driver, a base URL, a logged-in session — available suite-wide from one place, while narrow, test-specific fixtures stay local to the file that needs them.

Interview Strategy

Lead with the discovery magic, because it is the detail interviewers like to hear: pytest finds conftest.py on its own, so shared fixtures need no import. The trap is dumping every fixture into conftest.py; the discipline is that only genuinely cross-cutting fixtures go there, and one-off fixtures stay local.

How to phrase it

conftest.py is a special file that pytest discovers automatically — and that is the key word, automatically. Any fixture I define there is available to every test file in that folder and its subfolders without a single import line. So it is the natural home for the fixtures that cut across the whole suite: the driver, the base URL, a logged-in session. I put those in conftest.py so there is exactly one place to change how the suite sets up, and the test files stay clean and focused on the actual scenario. What I deliberately keep out of conftest.py is the narrow stuff — a fixture only one module needs stays local to that module, because promoting it to conftest just adds noise to a shared file. The line I draw is simple: if more than one file needs it, it goes in conftest.py; if only one file needs it, it stays in that file. And the auto-discovery is the part I make sure to mention, because no-import shared fixtures is the thing interviewers are checking you actually know.

Key points to hit

  • conftest.py is auto-discovered — its fixtures need no import anywhere.
  • Fixtures in it are available to every test in that folder and subfolders.
  • Put cross-cutting fixtures there: driver, base URL, login.
  • Keep narrow, single-file fixtures local to the module that needs them.
  • One place to change shared setup keeps test files clean.
Code
# conftest.py — shared everywhere, no import needed
import pytest
from selenium import webdriver

@pytest.fixture
def driver():
    d = webdriver.Chrome()
    yield d
    d.quit()

# any test file in the tree:
def test_home(driver):
    driver.get("https://app.test")
10

pytest plain assert vs unittest assertEqual — does it matter?

Junior
Concept

unittest needs a specific method per comparison — assertEqual, assertTrue, assertIn, assertGreater, and so on. pytest uses Python's plain assert statement and applies assertion rewriting, which inspects the failed expression and prints a detailed message showing both operands and the difference. So you get readable code and a rich failure report without memorising a method for every kind of check. assertEqual-style methods are only required inside unittest.

Interview Strategy

The headline is that pytest gives you the rich failure output of assertEqual while letting you write a plain assert. The trap is thinking plain assert means worse error messages — it is the opposite, thanks to assertion rewriting. Add the one-assert-per-check discipline so a failure points at exactly one thing.

How to phrase it

In unittest you need a specific method for each comparison — assertEqual, assertTrue, assertIn — so you end up memorising a vocabulary of assert methods. pytest lets me write Python's plain assert and still get a great failure message, because of something called assertion rewriting. When an assert fails, pytest inspects the expression and prints both sides and the difference, so a failed assert of driver dot title equals Home shows me the actual title it got versus what I expected. So I get the readability of a plain assert and the diagnostic detail of assertEqual at the same time, which is the best of both. The habit I pair with this is one assert per logical check. If I cram three conditions into one assert, the failure tells me the group failed but not which one. Keeping them separate means a failure points at the exact problem. So the answer is yes it matters, but in pytest's favour — plainer code, equally rich failures.

Key points to hit

  • unittest needs a method per comparison; pytest uses plain assert.
  • pytest's assertion rewriting prints both operands and the diff on failure.
  • You get readable code and rich failure output together.
  • assertEqual-style methods are only required inside unittest.
  • Keep one assert per logical check so failures pinpoint the cause.
Code
# pytest — plain assert, rich auto-diff on failure
assert driver.title == "Home"
assert "Welcome" in driver.page_source

# unittest — a method per comparison
# self.assertEqual(driver.title, "Home")
# self.assertIn("Welcome", driver.page_source)
11

Page Object Model vs PageFactory-style — how does Python differ?

Senior
Concept

POM wraps each page in a class that exposes user actions as methods and hides the locators inside, so the test reads like a list of steps rather than a wall of selectors. PageFactory is a Java and C-sharp helper that injects WebElements into fields using FindBy annotations and lazy proxies. Python has no native PageFactory, so the Python equivalent is just clean POM: locators as class-level constants, actions as methods, and no annotation magic. The senior refinement is that page methods should return data or the next page object, never raw WebElements.

Interview Strategy

Clear up the common confusion first: PageFactory is a Java idiom, not a pattern Python needs. The trap is candidates trying to recreate FindBy annotations in Python; the right answer is that plain POM with locator constants and methods is the Pythonic equivalent and is actually cleaner.

How to phrase it

POM is the pattern — each page is a class that exposes actions as methods and keeps the locators private, so a test reads like login, add to cart, checkout, not a pile of selectors. PageFactory is something different and Java-specific: it is a helper that injects elements into fields using FindBy annotations and lazy proxies. People sometimes ask how to do PageFactory in Python, and the honest answer is that you do not, because Python has no native PageFactory and does not need one. The Python equivalent is just clean POM — I put the locators as class-level constants, I write methods for the actions, and there is no annotation magic at all, which I would argue is actually clearer than the Java version. The refinement that signals seniority is what the methods return. A good page method does not hand back a raw WebElement for the test to poke at; it returns data the test can assert on, or it returns the next page object after a navigation. That keeps the locators sealed inside the page where they belong, so a UI change touches one class, not fifty tests.

Key points to hit

  • POM: each page is a class of action methods with locators hidden inside.
  • PageFactory is a Java/C# idiom using FindBy annotations — Python has none.
  • Python equivalent is plain POM: locator constants plus action methods.
  • Page methods return data or the next page object, never raw WebElements.
  • Hiding locators in the page means a UI change touches one class.
Code
class LoginPage:
    EMAIL = (By.ID, "email")
    PASSWORD = (By.ID, "password")
    SUBMIT = (By.CSS_SELECTOR, "[data-testid='login']")

    def __init__(self, driver):
        self.driver = driver

    def login(self, email, password):
        self.driver.find_element(*self.EMAIL).send_keys(email)
        self.driver.find_element(*self.PASSWORD).send_keys(password)
        self.driver.find_element(*self.SUBMIT).click()
        return DashboardPage(self.driver)   # return the next page
12

driver.get() vs navigation (back/forward/refresh) — when to use which?

Junior
Concept

driver.get of a URL loads a fresh address and blocks until the page load completes under the active page-load strategy. The navigation methods — driver.back, driver.forward, driver.refresh — move through the existing browser history or reload the current page, rather than loading a new address. So get is how you land somewhere deterministically, and the navigation methods are for tests that genuinely exercise history or a reload.

Interview Strategy

Draw the line clearly: get loads a new address, the navigation trio works on existing history. The trap is using back to 'return' to a previous page when a direct get would be clearer and far less flaky — call that out as the common mistake.

How to phrase it

driver.get loads a fresh URL and waits for the page to finish loading, so it is how I land on a known page deterministically — it does not care what was there before, it just goes. The navigation methods are a different thing: back, forward, and refresh move through the history the browser already has, or reload the current page. So I use them only when the test is genuinely about that behaviour — for example, verifying that going back from a confirmation page returns the user to their cart in the right state, or that a refresh preserves a form. The mistake I watch for, and call out in reviews, is using back as a lazy way to get to a previous page. If I just want to be on the login page, I call get with the login URL — that is deterministic and reads clearly. Relying on back assumes the history is exactly what I think it is, which is fragile and a classic source of flake. So the rule is: get to go somewhere, navigation methods only when history or reload is the actual thing under test.

Key points to hit

  • driver.get loads a new URL and waits for the page load to complete.
  • back, forward, refresh act on existing history or reload the current page.
  • Use get to land deterministically on a known page.
  • Use navigation only when history or reload is genuinely under test.
  • Antipattern: using back to 'return' to a page a direct get would reach.
Code
driver.get("https://app.test/login")   # deterministic landing
driver.refresh()                        # reload current page
driver.back()                           # only if testing history
13

close() vs quit() — what is the difference?

Junior
Concept

driver.close shuts the current browser window or tab and leaves the WebDriver session alive. driver.quit closes every window and ends the WebDriver session entirely, which terminates the underlying browser and driver process and frees its resources. quit is the one that fully cleans up. The practical consequence is that calling close in teardown can leave orphan browser and chromedriver processes piling up on a CI agent across a long run.

Interview Strategy

Make the resource consequence the centre of the answer: quit ends the session and frees the process, close only shuts one window. The trap is teardown that calls close — on a long suite that leaks processes and eventually exhausts the CI agent, which is a real, expensive failure.

How to phrase it

close shuts the current window or tab but leaves the WebDriver session running. quit closes every window and ends the session, which kills the browser and the driver process and frees everything. So quit is the one that actually cleans up. Why this matters in practice is process leakage. If I put close in my teardown instead of quit, the session stays alive after each test, and over a long suite I accumulate orphan browser and chromedriver processes on the machine. On a CI agent that is genuinely damaging — eventually you run out of memory or hit a process limit and the whole pipeline starts failing for reasons that have nothing to do with the tests. So my standard is quit in the fixture teardown, every time, so each test releases its session cleanly. I only use close in the narrow case where a single test opens an extra tab — say a payment popup — and I want to dismiss just that one tab and carry on in the original window. For ending the session, it is always quit.

Key points to hit

  • close shuts the current window/tab, session stays alive.
  • quit closes all windows and ends the session, freeing the process.
  • close in teardown leaks orphan browser/driver processes.
  • Leaked processes are a real CI failure — memory and process limits.
  • Standard: quit in fixture teardown; close only to dismiss an extra tab.
Code
@pytest.fixture
def driver():
    d = webdriver.Chrome()
    yield d
    d.quit()    # ends the session, frees the process — not close()
14

pytest markers vs ad-hoc grouping — how do you organise tests?

Mid
Concept

A pytest marker is a label you attach to a test with a decorator, like at-pytest-dot-mark-dot-smoke, and then select on the command line with dash-m. Grouping only by file or naming convention works for organisation, but you cannot combine or filter those groupings the way you can with markers. Markers give you cross-cutting selections — smoke, regression, slow — that span files, and you can compose them with boolean expressions like smoke and not slow.

Interview Strategy

Contrast the two on selectability: folders organise but cannot be filtered and combined, markers can. The trap is unregistered markers throwing warnings; mention registering them in pytest.ini, which signals you have actually run a marked suite rather than just read about it.

How to phrase it

A marker is a label I put on a test with a decorator — smoke, regression, slow — and then I select on it with dash-m on the command line. The contrast with grouping by folder or filename is that those organise the code but they cannot be filtered or combined. With markers I can say run everything tagged smoke, and because markers are cross-cutting, that selection spans every file, not just one folder. The real power is composing them: I can run dash-m smoke and not slow to get the fast smoke tests for a pull-request gate, and the full regression set on a nightly. The detail I make sure to mention is registering my markers in pytest.ini. If you do not register them, pytest warns about unknown markers on every run, and a strict config can even turn that into an error, so registering them keeps the output clean and shows I have actually operated a marked suite. So my answer is: markers for the cross-cutting selections CI needs, folders just for code organisation, and the two doing different jobs.

Key points to hit

  • A marker is a decorator label selected with -m on the command line.
  • Folders organise code but cannot be filtered or combined; markers can.
  • Markers are cross-cutting: smoke, regression, slow span every file.
  • Compose with boolean expressions, e.g. -m "smoke and not slow".
  • Register markers in pytest.ini to avoid unknown-marker warnings.
Code
import pytest

@pytest.mark.smoke
def test_login(driver): ...

# pytest.ini
# [pytest]
# markers =
#     smoke: fast critical-path checks
#     slow: long-running tests

# run: pytest -m "smoke and not slow"
15

@pytest.mark.parametrize vs hardcoded data — how do you data-drive?

Mid
Concept

Hardcoding test data means either copying a test once per input — pure duplication — or looping over inputs inside one test, where the first failed assertion stops the loop and hides every later case. parametrize runs the same test body once per data row, and each row is reported as a separate, independently passing or failing case. So instead of one test that dies on the first bad row, you get a clear count: row three of five failed, the rest passed.

Interview Strategy

Make the failure reporting the centre of the answer: the difference between 'one test failed' and 'row three of five failed' is the whole value. The trap is the loop-inside-one-test pattern, which feels DRY but hides every failure after the first — name it explicitly.

How to phrase it

The honest comparison is about what happens when a data row fails. If I hardcode, I either copy the test once per input, which is duplication I will forget to keep in sync, or I loop over the inputs inside one test. The loop is the dangerous one, because the first failed assertion stops the loop, so if row two fails I never even run rows three, four, and five, and they are hidden. parametrize fixes exactly that. It runs the same test body once per data row, and each row is reported as its own independent test case. So instead of one test that dies on the first bad input, I get a precise picture — row three of five failed, the other four passed — which tells me exactly which input is broken without re-running anything. It also keeps the body completely DRY, because there is one test function and the data lives in a tidy list of tuples above it. So parametrize gives me independent reporting and no duplication at once, which is why I never loop test data by hand.

Key points to hit

  • Hardcoding means duplication, or a loop that hides failures after the first.
  • parametrize runs the test body once per data row.
  • Each row is an independent, separately reported pass/fail.
  • You get 'row 3 of 5 failed', not just 'the test failed'.
  • The body stays DRY — one function, data in a list above it.
Code
@pytest.mark.parametrize("username, expected_ok", [
    ("valid_user", True),
    ("locked_user", False),
    ("", False),
])
def test_login(driver, username, expected_ok):
    assert do_login(driver, username) is expected_ok
16

Headless vs headed browser — when do you run which?

Junior
Concept

Headed runs a visible browser window you can watch; headless runs the same browser engine with no UI drawn. Headless is faster and is what CI uses, because a build agent usually has no display at all. The catch is that rendering and a few interactions can differ subtly from a real window — most often because the headless window has a different default size — so a layout-dependent test can behave differently. Setting an explicit window size in headless is what removes most of that difference.

Interview Strategy

Match each mode to where it belongs: headed locally for watching and debugging, headless in CI for speed and because there is no display. The trap is blaming 'a headless bug' when a test passes headed and fails headless; flag that it is almost always a viewport or timing issue you fix by setting the window size.

How to phrase it

Headed gives me a visible browser window, headless runs the same engine with no UI drawn. I run headed locally, because when I am writing or debugging a test I want to watch what the browser actually does. I run headless in CI, for two reasons: it is faster, and the build agent typically has no display at all, so a visible window is not even an option. The subtlety worth raising is that headless and headed are not perfectly identical — rendering and some interactions can differ, and the usual culprit is window size, because the headless window defaults to a different, often smaller, size. So I always set an explicit window size in headless, something like nineteen-twenty by ten-eighty, so responsive layouts behave the same as on a real screen. And the framing I am careful to use: if a test passes headed but fails headless, I do not call it a headless bug. Nine times out of ten it is a viewport or a timing difference, and the fix is setting the window size and tightening the waits, not avoiding headless.

Key points to hit

  • Headed shows a window; headless runs the same engine with no UI.
  • Headless is faster and required in CI, where there's often no display.
  • Rendering can differ subtly — usually because of window size.
  • Always set an explicit window size in headless for responsive layouts.
  • Passes headed, fails headless usually means viewport/timing, not a 'headless bug'.
Code
opts = webdriver.ChromeOptions()
opts.add_argument("--headless=new")        # modern headless mode
opts.add_argument("--window-size=1920,1080")
driver = webdriver.Chrome(options=opts)
17

Selenium Manager vs webdriver-manager vs manual driver path?

Mid
Concept

Manual setup means downloading the chromedriver that matches your browser, pointing Selenium at it with a Service, and re-downloading it every time the browser auto-updates — the source of the classic version-mismatch failure. webdriver-manager was a popular third-party library that automated that download. Selenium 4.6 and later ship Selenium Manager built in: it resolves and downloads the correct driver automatically with zero config, so webdriver.Chrome simply works. As of 2026 the built-in manager is the default and the third-party library is no longer needed.

Interview Strategy

Lead with the version cut-off, because naming Selenium 4.6 is the current, senior signal — it shows you know the modern default rather than the old webdriver-manager habit. The trap is recommending webdriver-manager or a hardcoded path as if they were still best practice; they are legacy now.

How to phrase it

There are really three eras here. The old manual way was downloading the exact chromedriver that matched your Chrome version, pointing Selenium at it with a Service, and then re-downloading it every single time Chrome auto-updated — which is the classic driver version mismatch failure everyone has hit. Then webdriver-manager came along as a third-party library that did that download for you, and for years that was the standard fix. But the current answer, and this is the version number I make sure to say, is that Selenium 4.6 and later ship Selenium Manager built in. It resolves and downloads the right driver automatically with zero configuration, so I just write webdriver.Chrome with no path and no manager library, and it works. So on any modern Selenium I do not install webdriver-manager and I do not hardcode a path — the built-in manager handles it, and it quietly killed the whole class of version-mismatch failures. Knowing that 4.6 cut-off is what shows I am current rather than repeating advice from a few years ago.

Key points to hit

  • Manual: download matching chromedriver, set a Service, re-download on every browser update.
  • webdriver-manager: legacy third-party library that automated the download.
  • Selenium 4.6+ ships Selenium Manager — auto-resolves the driver, zero config.
  • On modern Selenium, webdriver.Chrome() just works — no path, no manager lib.
  • Built-in manager killed the classic driver version-mismatch failure.
Code
# Selenium 4.6+ — Selenium Manager handles the driver
driver = webdriver.Chrome()

# Legacy ways (avoid on modern Selenium):
# from selenium.webdriver.chrome.service import Service
# driver = webdriver.Chrome(service=Service("/path/to/chromedriver"))
18

venv + requirements.txt vs global installs — why isolate?

Mid
Concept

Installing packages globally mixes every project's dependencies into one shared set, so two projects that need different versions of the same library clash. A virtual environment gives each project its own isolated set of packages, and requirements.txt records them with pinned versions so anyone — and CI — installs exactly the same versions you did. Together they are what make 'works on my machine' a non-issue and a build reproducible months later.

Interview Strategy

Frame isolation as reproducibility, not tidiness: a venv plus a pinned requirements.txt is what makes a green build today still green next month. The trap is leaving requirements.txt unpinned; mention pip freeze so the versions are captured exactly, which is the difference between reproducible and 'whatever was latest that day'.

How to phrase it

Installing globally puts every project's packages into one shared pile, so the moment two projects need different versions of the same library, they clash and one of them breaks. A virtual environment fixes that by giving each project its own isolated set of packages, completely separate from everything else on the machine. Then requirements.txt records those packages, and the part that matters is pinning the versions — not just selenium, but selenium equals a specific version — so when a teammate or CI installs from it, they get exactly what I have, not whatever happens to be latest that day. I capture those pinned versions with pip freeze. The reason I care about this so much is reproducibility. A venv plus a pinned requirements.txt is what makes works-on-my-machine stop being a thing, because everyone is running identical dependencies. And it is what keeps a build that is green today still green next month, even if some library ships a breaking release in the meantime, because I am pinned to the version I tested against. So I always work in a venv and always commit a pinned requirements file.

Key points to hit

  • Global installs mix all projects' dependencies and cause version clashes.
  • A venv gives each project its own isolated packages.
  • requirements.txt with pinned versions makes installs identical for everyone and CI.
  • Capture pinned versions with pip freeze.
  • Isolation plus pinning is what keeps a green build green next month.
Code
python -m venv .venv
source .venv/bin/activate      # Windows: .venv\Scripts\activate
pip install -r requirements.txt

# capture pinned versions:
# pip freeze > requirements.txt
19

Local run vs Selenium Grid — when do you need Grid?

Senior
Concept

Running locally executes every test on the one machine's installed browsers. Selenium Grid distributes tests across multiple machines and browser-and-OS combinations through a hub and nodes, or via Docker containers, so you can run in parallel and on platforms you do not have locally — Safari on macOS, older browser versions, and so on. You point the driver at the Grid URL using webdriver.Remote instead of a local driver. Grid combined with pytest-xdist is how you cut a slow suite's wall-clock time and get real cross-browser coverage.

Interview Strategy

Tie the need to two concrete triggers: cross-browser or cross-OS coverage you cannot get locally, and cutting suite time with parallel nodes. The trap is reaching for Grid too early; for development you run locally, and Grid earns its complexity only when those triggers actually appear.

How to phrase it

Running locally means every test runs on the browsers installed on my one machine, which is exactly right while I am developing. Selenium Grid distributes tests across multiple machines and browser-and-OS combinations through a hub and nodes, or as Docker containers. The way I connect to it is with webdriver.Remote pointed at the Grid URL instead of a local Chrome. I reach for Grid when one of two real needs shows up. The first is coverage I cannot get locally — I am on Windows but I need to verify Safari on macOS, or an older browser version, and Grid or a cloud grid like a vendor service gives me those platforms. The second is speed: when the suite gets slow, Grid lets me spread tests across nodes and run them in parallel, and combined with pytest-xdist on the client side, that is how I take a suite from forty minutes down to single digits. What I would not do is stand up Grid on day one — it is real infrastructure to run and maintain, so it earns its place only when cross-browser coverage or suite time actually become problems.

Key points to hit

  • Local runs use the one machine's browsers; Grid distributes across machines.
  • Grid gives cross-browser and cross-OS coverage you can't get locally.
  • Connect with webdriver.Remote pointed at the Grid URL.
  • Grid plus pytest-xdist is how you cut a slow suite's wall-clock time.
  • Don't reach for Grid until coverage or suite speed is actually a problem.
Code
driver = webdriver.Remote(
    command_executor="http://grid:4444/wd/hub",
    options=webdriver.ChromeOptions(),
)
20

Selenium 4 relative locators vs classic locators?

Mid
Concept

Classic locators find elements directly by id, CSS, or XPath. Selenium 4 added relative — or friendly — locators: above, below, to_left_of, to_right_of, and near, accessed through locate_with. They find an element by its visual position relative to another element, which is useful when the target has no stable attribute of its own but sits in a predictable place, like the input directly below a labelled field. They are powerful but position-dependent, so they break when the layout shifts.

Interview Strategy

Present relative locators as a niche tool for layout-based finds when no clean selector exists, not a general-purpose replacement. The trap is over-using them; flag that they are position-dependent and brittle to layout changes, so a stable test id still comes first. Knowing when not to use them is the senior part.

How to phrase it

Classic locators find an element directly — by id, CSS, or XPath. Selenium 4 added relative locators, sometimes called friendly locators: above, below, to-left-of, to-right-of, and near, which I use through locate_with. Instead of describing the element itself, they describe where it sits relative to another element — so I can say find the input below this label. Where they genuinely help is when the target has no stable attribute of its own but it is in a predictable position next to something I can locate cleanly. That said, I am cautious with them, and the caution is the senior part of the answer. They are position-dependent by definition, so they break the moment the layout changes — a responsive breakpoint that stacks fields vertically can flip an above into a below. So my order of preference is unchanged: a stable test id first, then a normal CSS or XPath, and relative locators only as the fallback when there is genuinely no clean attribute to grab. Knowing they exist and, more importantly, knowing when not to use them is what I would want to demonstrate.

Key points to hit

  • Classic locators target an element directly by id, CSS, or XPath.
  • Selenium 4 relative locators (above, below, near, etc.) use visual position.
  • Useful when the target has no stable attribute but sits predictably.
  • Position-dependent — they break when the layout changes.
  • Prefer a stable test id first; relative locators are a fallback.
Code
from selenium.webdriver.support.relative_locator import locate_with

password = driver.find_element(
    locate_with(By.TAG_NAME, "input").below({By.ID: "password-label"})
)
21

ActionChains vs execute_script (JS) — which for tricky interactions?

Mid
Concept

ActionChains drives real, user-like input through the browser — hover, drag-and-drop, right-click, key combinations — by queuing actions and then calling perform. execute_script runs raw JavaScript directly in the page, so it can click, scroll, or set values even on elements that are awkward or impossible to reach through normal interaction. The key trade-off is realism versus reach: ActionChains behaves like a user and exercises the real event handlers, while a JavaScript click bypasses those checks and can pass on a button a real user could never click.

Interview Strategy

Set up the trade-off as realism versus reach, and make ActionChains the default because it mimics a real user. The trap is the JavaScript click as a habit; flag that it bypasses the visibility and enabled checks a real user hits, so it can hide genuine bugs — use it only as a last resort.

How to phrase it

ActionChains drives real user-like input — hover, drag-and-drop, right-click, key combinations — by queuing the actions and then calling perform. execute_script runs raw JavaScript straight in the page, so it can click, scroll, or set a value even on an element that is awkward to reach. My default is ActionChains, and the reason is that it behaves like an actual user and goes through the real event handlers, so my test exercises the same path a person would. I fall back to execute_script only when WebDriver genuinely cannot do something — the most common honest case is scrolling an element into view before interacting with it. The warning I always attach is about the JavaScript click. If an element is not clickable for a real user because it is hidden or disabled, a JS click will still fire and the test will pass — which means it can hide a real bug where the button should not be clickable at all. So I treat a JS click as a last resort, not a convenience, because the whole point of an end-to-end test is to behave like the user. Realism first, reach only when realism cannot get there.

Key points to hit

  • ActionChains drives real user input: hover, drag, right-click, key combos.
  • execute_script runs raw JS — can reach elements normal interaction can't.
  • Trade-off is realism (ActionChains) versus reach (JS).
  • Default to ActionChains so the real event handlers fire.
  • A JS click bypasses visibility/enabled checks and can hide real bugs.
Code
from selenium.webdriver import ActionChains

# Realistic — hover via real input
ActionChains(driver).move_to_element(menu).perform()

# Last resort — JS reaches an awkward element
driver.execute_script("arguments[0].click();", el)
22

expected_conditions: visibility vs presence vs clickable?

Mid
Concept

These three expected_conditions are progressively stricter. presence_of_element_located waits only for the element to exist in the DOM — it may still be hidden. visibility_of_element_located waits for it to be in the DOM and actually displayed. element_to_be_clickable waits for it to be visible and enabled. Choosing the wrong one is a classic flaky bug: waiting only for presence and then clicking fails intermittently, because the element exists before it is ready to be clicked.

Interview Strategy

Frame the three as a strictness ladder and tie each to the action you intend. The trap is the presence-then-click bug; name it directly, because waiting for presence and then clicking is one of the most common causes of intermittent failures interviewers want you to recognise.

How to phrase it

I think of these three as a ladder of strictness. presence-of-element-located is the weakest — it only waits for the element to be in the DOM, and it may still be hidden or not yet rendered. visibility-of-element-located is stricter: it waits for the element to be in the DOM and actually displayed. element-to-be-clickable is the strictest: visible and enabled. The way I choose is to match the condition to what I am about to do. If I just need to read a hidden attribute, presence is enough. If I am asserting that a message is shown to the user, I wait for visibility. And if I am about to click, I wait for clickable, because visible is not the same as ready — a button can be on screen but still disabled. The classic flaky bug, and the one I make sure to name, is waiting for presence and then clicking. The element exists, so the wait passes, but it is not actually clickable yet, so the click fails intermittently depending on timing. Matching the condition to the intent is exactly what removes that whole category of flake.

Key points to hit

  • presence: element is in the DOM, possibly hidden.
  • visibility: element is in the DOM and displayed.
  • clickable: element is visible and enabled.
  • Match the condition to the action: presence to read, visibility to assert, clickable to click.
  • Classic flake: waiting for presence then clicking — the element isn't ready yet.
Code
wait = WebDriverWait(driver, 10)
wait.until(EC.element_to_be_clickable((By.ID, "pay"))).click()
wait.until(EC.visibility_of_element_located((By.ID, "confirmation")))
23

pytest-xdist parallel (-n) vs sequential runs?

Senior
Concept

By default pytest runs tests one after another in a single process. pytest-xdist with the dash-n flag spreads them across multiple worker processes and CPU cores, cutting wall-clock time dramatically — dash-n auto uses every available core. The price is that tests must be independent: no shared mutable state, no fixed execution order, no two tests fighting over the same data record or port. So parallelism is less a feature you switch on and more a discipline your tests have to already meet.

Interview Strategy

Position parallelism as something you earn by writing independent tests, not a flag you flip on a fragile suite. The trap is enabling dash-n on order-dependent tests and watching them fail randomly; name shared state, fixed order, and hardcoded ports as the usual breakers, and the insight that parallelism exposes hidden coupling.

How to phrase it

By default pytest runs tests sequentially in one process. pytest-xdist with dash-n spreads them across worker processes and cores — dash-n auto uses every core I have — and that can take a suite from many minutes to a fraction of that. But I am careful to frame it correctly: parallelism is not a switch I flip on any suite, it is something the tests have to earn by being independent. The requirements are no shared mutable state, no dependence on execution order, and no two tests competing for the same data record or the same port. So before I turn on dash-n, I make sure each test sets up its own data and spins its own driver fixture, so it can run alone or alongside any other test. The insight I like to share is that parallelism is actually a great diagnostic — the moment you run dash-n, any hidden coupling between tests surfaces as random failures, because tests that secretly relied on running in a fixed order now do not. So I treat the first parallel run as a way to discover and fix that coupling, after which the suite is both fast and genuinely independent.

Key points to hit

  • Default pytest is sequential; xdist -n spreads tests across processes and cores.
  • -n auto uses every available core.
  • Tests must be independent: no shared state, no fixed order.
  • Usual breakers: shared mutable state, hardcoded ports, ordering assumptions.
  • Parallelism exposes hidden coupling — the first -n run finds it.
Code
# run across all cores
# pytest -n auto

# each test owns its driver and data — no shared state
def test_checkout(driver):
    user = create_user()      # this test's own data
    login(driver, user)
    assert checkout(driver)
24

Explicit wait vs page load strategy — different problems?

Senior
Concept

An explicit wait synchronises on a single element or condition after the page has loaded — it is about element readiness. The page load strategy controls when driver.get returns at all: normal waits for the full load event, eager returns at DOMContentLoaded before sub-resources finish, and none returns almost immediately. So they tune two different stages — page load strategy tunes navigation, explicit waits tune the dynamic content that appears after navigation. They are not interchangeable.

Interview Strategy

Separate the two stages cleanly: page load strategy decides how long get blocks, explicit waits handle content that arrives after that. The trap is thinking one replaces the other; even with eager you still need explicit waits for AJAX content, and saying so is the strong senior signal.

How to phrase it

These solve two different problems at two different stages, and that distinction is the whole point. The page load strategy controls when driver.get returns — normal waits for the full load event including images and sub-resources, eager returns earlier at DOMContentLoaded, and none returns almost immediately. So it is tuning navigation, how long get blocks before my code continues. An explicit wait is a completely separate thing — it synchronises on one element or condition after the page is already loaded, so it is about element readiness, especially for content that arrives later via AJAX. The reason they are not interchangeable is that setting eager only makes get return sooner; it does nothing for a table that loads its rows by an XHR call a second later — for that I still need an explicit wait on the rows. So the way I use them together is: eager can be a useful speed-up on a heavy page where I do not care about every sub-resource, but it never removes the need for explicit waits on dynamic elements. One tunes when navigation finishes, the other tunes when a specific element is ready. Keeping those separate is exactly what I would want to show.

Key points to hit

  • Page load strategy controls when driver.get returns (normal/eager/none).
  • Explicit wait synchronises on an element or condition after load.
  • One tunes navigation; the other tunes element readiness.
  • eager speeds up navigation but doesn't help AJAX content.
  • Even with eager you still need explicit waits for dynamic elements.
Code
opts = webdriver.ChromeOptions()
opts.page_load_strategy = "eager"   # get() returns at DOMContentLoaded
driver = webdriver.Chrome(options=opts)

# still wait per element for AJAX content:
WebDriverWait(driver, 10).until(
    EC.visibility_of_element_located((By.ID, "data-table"))
)
25

Sequential test ordering vs independent tests — does order matter?

Senior
Concept

Tests that depend on a fixed running order — one creates a record the next reads, the third deletes it — are fragile: they break the instant you run a single test alone, run a subset, reorder them, or run in parallel. Independent tests each set up the state they need and clean up after themselves, so any one can run in isolation or alongside any other. Independence is the precondition for parallelism and reliable CI; order-dependence is the hidden reason a suite collapses the day you add pytest-xdist.

Interview Strategy

Make independence the headline value and connect it directly to parallelism and selective runs. The trap is the chained test pattern that quietly works in full sequential runs and then explodes under xdist or when someone runs one test alone; name that failure mode as the giveaway of order-dependence.

How to phrase it

My standard is that order must never matter, and I write every test to be self-contained — it sets up its own data, gets its own driver fixture, and cleans up after itself. The contrast is the chained style, where one test creates a record, the next reads it, and a third deletes it. That style appears to work as long as you always run the whole file in order, which is exactly why it is so dangerous — it hides the problem. The moment someone runs a single test alone to debug it, or runs a subset, or reorders, or — the big one — adds pytest-xdist to parallelise, those tests fail, because the state they silently depended on is not there. So order-dependence is usually the hidden reason a suite collapses the day you turn on parallel runs. The framing I land on is that independence is not just tidiness, it is the precondition for everything you actually want: parallelism, selective runs, and a CI you can trust. A test you cannot run on its own is a test you cannot really trust, so I make each one stand alone.

Key points to hit

  • Order-dependent tests break on solo runs, subsets, reordering, and parallelism.
  • Independent tests set up their own state and clean up after themselves.
  • Chained tests appear to work in full sequential runs — which hides the bug.
  • The giveaway: the suite collapses the day you add pytest-xdist.
  • Independence is the precondition for parallelism and reliable CI.
Code
# self-contained — runs alone or alongside anything
def test_checkout(driver):
    user = create_user()      # its own data
    login(driver, user)
    assert checkout(driver)
    # teardown via fixture; nothing left for another test to depend on

Want more free SDET prep?

Get my free interview resources and new Q&A drops straight to your inbox.

250+ questions in the full kit

SDET Selenium Python Foundation Kit 2026

This is a glimpse. The full Foundation Kit covers Python for SDETs, pytest, locators, waits, POM, fixtures, API testing, CI/CD and the scenario rounds — interview-ready answers throughout.

Other stacks