PRACTICAL GUIDE / Selenium Python pytest driver fixture design

A pytest WebDriver fixture that never leaks a browser

Build a pytest WebDriver fixture that creates isolated Selenium sessions, always quits them, and leaves useful evidence when setup or teardown fails.

By The Testing AcademyUpdated August 7, 202620 min read
All field guides
In this guide7 sections
  1. Follow pytest's real setup and teardown order
  2. Protect the session when setup fails before yield
  3. Capture the failure before quitting the browser
  4. Separate a leaked session from shared browser state
  5. Distinguish a missed finalizer from a stuck quit
  6. Wire local and remote drivers without changing ownership
  7. Migrate broad fixtures one failure class at a time

What you will learn

  • Follow pytest's real setup and teardown order
  • Protect the session when setup fails before yield
  • Capture the failure before quitting the browser
  • Separate a leaked session from shared browser state

The first Selenium test passes, and the second opens on the account page left behind by the first. CI later reports Chrome processes still running after the job should be finished. Both symptoms point to the same design error: no single fixture clearly owns a WebDriver session from creation through quit().

Putting webdriver.Chrome() in setup_method and driver.close() in a helper may look tidy, but ownership is split across call sites. A failed login can skip the helper. A second window can survive close(). A broad fixture scope can preserve cookies and storage long after the test that created them has ended.

Follow pytest's real setup and teardown order

A pytest test requests fixtures by naming them as function parameters. Fixtures may request other fixtures in the same way. Pytest resolves those dependencies, creates each required fixture, caches its value for the fixture's scope, and tears fixtures down in reverse dependency order.

The default scope is function. That creates a new fixture instance for each test and destroys it when the test ends. Other supported scopes are class, module, package, and session. A broader scope is not simply an optimization switch. It is a declaration that every consumer in that scope may receive the same object and its accumulated state.

A yield fixture divides setup from teardown. Code before yield creates the value. The yielded value is passed to the test or dependent fixture. Code after yield runs when pytest finalizes the fixture, including when the test assertion fails. Fixtures that completed setup are finalized in reverse order.

There is an important exception. If a yield fixture raises before it reaches yield, pytest does not run statements placed after that yield. Fixtures that had already completed setup are still torn down, but the failing fixture has not handed control to pytest's yield finalizer. That difference explains many orphaned browsers.

Start with a function-scoped driver whose creation, configuration, handoff, and shutdown stay together. This fixture is runnable with pytest and Selenium installed. It runs Chrome headless only when the CI environment variable is present.

Python
# conftest.py
import os

import pytest
from selenium import webdriver


@pytest.fixture
def driver(request):
    options = webdriver.ChromeOptions()
    if os.getenv("CI"):
        options.add_argument("--headless")
    options.add_argument("--window-size=1440,900")

    browser = webdriver.Chrome(options=options)
    session_id = browser.session_id
    print(f"DRIVER_SETUP nodeid={request.node.nodeid} session={session_id}")

    try:
        yield browser
    finally:
        print(
            f"DRIVER_TEARDOWN nodeid={request.node.nodeid} "
            f"session={session_id}"
        )
        browser.quit()

The fixture stores session_id before teardown because it is useful evidence and belongs to the created session. The finally block means a test failure, skip during the call phase, or exception injected at the yield point still attempts quit(). If webdriver.Chrome() itself raises, the code never enters the try, and there is no successfully returned browser object to quit.

A test remains concerned with product behavior rather than session management. This example uses selector strategies exposed by Selenium's Python bindings and an explicit wait for the product result.

Python
# test_login.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


def test_invalid_password_keeps_user_signed_out(driver, live_server_url):
    driver.get(f"{live_server_url}/login")
    driver.find_element(By.ID, "email").send_keys("qa@example.test")
    driver.find_element(By.ID, "password").send_keys("wrong-password")
    driver.find_element(By.CSS_SELECTOR, "button[type='submit']").click()

    error = WebDriverWait(driver, 10).until(
        EC.visibility_of_element_located((By.CSS_SELECTOR, "[role='alert']"))
    )
    assert "incorrect" in error.text.lower()

Multiple windows show why a test may legitimately call close() even though fixture teardown must still call quit(). Suppose an export button opens a report in a new tab. The test can close that tab after checking it and return to the original window. If the assertion raises before close(), the fixture's finally still quits the session and closes both windows.

Python
# test_report_export.py
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


def test_export_opens_generated_report(driver, live_server_url):
    driver.get(f"{live_server_url}/reports/monthly")
    original_window = driver.current_window_handle
    original_handles = set(driver.window_handles)

    driver.find_element(By.ID, "export-report").click()
    WebDriverWait(driver, 10).until(EC.number_of_windows_to_be(2))

    report_window = next(
        handle for handle in driver.window_handles
        if handle not in original_handles
    )
    driver.switch_to.window(report_window)
    assert "/reports/generated/" in driver.current_url

    driver.close()
    driver.switch_to.window(original_window)
    assert driver.current_window_handle == original_window

Here, close() expresses a product step: dismiss the report tab and continue in the application. It is not the fixture's cleanup guarantee. Removing the final quit() because this test closes one window would leave the session alive in tests that never open a second tab. Conversely, forbidding tests from closing any window would make multi-window behavior awkward to verify. Ownership makes the difference clear: tests may manipulate windows; the fixture ends the session.

This case also provides a useful diagnosis. If number_of_windows_to_be(2) times out, inspect driver.window_handles before assuming the fixture leaked something. The product may have navigated the same tab, the browser may have blocked the popup because the click path changed, or the locator may have selected the wrong export control. A leaked prior session cannot add a window handle to a newly created session with a different session ID.

Calling out that near-miss matters. Fixture correctness cannot rescue a test that uses a nonexistent selector strategy. Validate imports and signatures before treating every setup error as a lifecycle failure.

Protect the session when setup fails before yield

The basic fixture still needs one adjustment when it performs browser operations before handing the driver to the test. Teams often set a base URL, authenticate, resize a remote window, or install cookies in fixture setup. If one of those operations raises and the try begins only immediately before yield, the browser leaks.

Place the try directly after successful driver creation. Everything that can fail after the session exists belongs inside it.

Python
import os

import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.support.ui import WebDriverWait


@pytest.fixture
def authenticated_driver(request, base_url):
    options = webdriver.ChromeOptions()
    if os.getenv("CI"):
        options.add_argument("--headless")

    browser = webdriver.Chrome(options=options)
    session_id = browser.session_id

    try:
        browser.get(f"{base_url}/test-support/login")
        WebDriverWait(browser, 10).until(
            EC.presence_of_element_located((By.CSS_SELECTOR, "[data-user-id]"))
        )
        print(
            f"DRIVER_READY nodeid={request.node.nodeid} "
            f"session={session_id}"
        )
        yield browser
    finally:
        browser.quit()

If navigation or the wait fails, Python executes finally while the fixture is unwinding. This is different from writing browser.quit() as an unprotected line after yield. The cost is that teardown can now produce a second exception while setup is already failing. Log the session ID before quitting, and avoid replacing the first cause with decorative cleanup assertions.

Pytest also supports request.addfinalizer. Registering a finalizer immediately after driver creation can protect later setup because pytest will call it even if that fixture raises afterward. The official docs warn that a registered finalizer runs once added, so add it only after the state requiring cleanup exists. For one driver, try/finally keeps the relationship easier to read. Direct finalizers become useful when setup creates several independent resources at different points and each needs cleanup registered as soon as it exists.

Do not confuse a fixture setup failure with a test failure. Pytest prints ERROR for a fixture that cannot prepare the test and FAILED for a test call whose assertion or product interaction fails. A setup error at authenticated_driver means the test body did not run. Screenshots, logs, and incident routing should retain that phase.

Use pytest's built-in setup display to see fixture order. The first command lists tests without running them. The second executes one node and prints fixture setup and teardown activity.

Shell
pytest --collect-only -q tests/ui/test_login.py
pytest -vv --setup-show -s tests/ui/test_login.py::test_invalid_password_keeps_user_signed_out

For a function-scoped fixture named driver, the output contains a SETUP F driver entry before the test and a matching TEARDOWN F driver after it. A session-scoped fixture is marked with S. Read the actual node ID shown by collection rather than guessing parametrized suffixes.

Capture the failure before quitting the browser

The driver fixture should own the session, but it should not also know every reporting policy. Split those concerns with a dependent fixture. Pytest tears the dependent fixture down first, so it can take a screenshot while the driver is still alive. The lower-level driver fixture then calls quit().

First, store the report for each test phase on the collected test item. This hook uses the established hook-wrapper form supported by pytest.

Python
# conftest.py
import pytest


@pytest.hookimpl(hookwrapper=True)
def pytest_runtest_makereport(item, call):
    outcome = yield
    report = outcome.get_result()
    setattr(item, f"rep_{report.when}", report)

Then add a fixture that depends on driver. Tests request browser_with_artifacts, not both fixtures. tmp_path provides a unique directory for the test, so parallel workers do not overwrite a shared failure.png.

Python
# conftest.py
from pathlib import Path

import pytest
from selenium.common.exceptions import WebDriverException


@pytest.fixture
def browser_with_artifacts(driver, request, tmp_path: Path):
    yield driver

    report = getattr(request.node, "rep_call", None)
    if report is None or not report.failed:
        return

    screenshot = tmp_path / "failure.png"
    try:
        driver.save_screenshot(str(screenshot))
        print(f"SCREENSHOT nodeid={request.node.nodeid} path={screenshot}")
    except WebDriverException as error:
        print(
            f"SCREENSHOT_FAILED nodeid={request.node.nodeid} "
            f"session={driver.session_id} error={error.msg}"
        )

tmp_path is convenient for local investigation, but many CI systems upload one known directory. Keep that policy in a separate fixture so path construction does not spread through tests. Sanitize the node ID because parametrized IDs contain characters that are inconvenient in file names, and include the worker identifier when parallel execution supplies one through the environment.

Python
# conftest.py
import os
import re
from pathlib import Path

import pytest
from selenium.common.exceptions import WebDriverException


@pytest.fixture
def failure_artifact_path(request) -> Path:
    root = Path(os.getenv("PYTEST_ARTIFACT_DIR", "artifacts/screenshots"))
    root.mkdir(parents=True, exist_ok=True)
    safe_nodeid = re.sub(r"[^A-Za-z0-9_.-]+", "_", request.node.nodeid)
    worker = os.getenv("PYTEST_XDIST_WORKER", "main")
    return root / f"{worker}-{safe_nodeid}.png"


@pytest.fixture
def browser_with_artifacts(driver, request, failure_artifact_path: Path):
    yield driver

    report = getattr(request.node, "rep_call", None)
    if report is None or not report.failed:
        return

    try:
        saved = driver.save_screenshot(str(failure_artifact_path))
        if not saved:
            print(
                f"SCREENSHOT_NOT_SAVED nodeid={request.node.nodeid} "
                f"session={driver.session_id}"
            )
    except WebDriverException as error:
        print(
            f"SCREENSHOT_FAILED nodeid={request.node.nodeid} "
            f"session={driver.session_id} error={error.msg}"
        )

The two browser_with_artifacts examples are alternatives, not fixtures to define together. Keep one implementation in conftest.py; duplicate fixture names would cause the later definition in the module to replace the earlier name. The known-directory version adds file-system work and artifact retention cost to failed tests. It also needs a CI upload step outside pytest. Those costs are worthwhile when remote workers disappear after a job, but unnecessary for a small local-only suite.

Notice that save_screenshot() returns a boolean. Logging the path without checking that return value can claim evidence exists when writing failed. An exception is also possible if the browser or window is already gone. Neither case should prevent the lower-level fixture from attempting quit().

This captures failures from the test call phase. A driver that fails during its own setup never reaches browser_with_artifacts, so this fixture cannot take that screenshot. For setup failures after the browser starts, capture evidence inside the driver's except or finally block before quit(). That duplication is justified because the lifecycle phase is different.

Avoid calling pytest.fail() only because a screenshot could not be saved. The original assertion is more useful than a secondary artifact failure. Log the artifact error with the node ID and session ID, then let driver teardown proceed. If artifact creation is a compliance requirement, report it separately without discarding the product failure.

Page source can help when rendering is blank, but it may contain tokens or personal data. Treat driver.page_source, browser logs, and screenshots as sensitive CI artifacts. Redact or limit retention according to the system under test. A fixture design that uploads every page unconditionally creates a data-handling problem as well as storage cost.

Separate a leaked session from shared browser state

An old page appearing in the next test does not prove that a Chrome process leaked. It may be the same intentionally cached driver, a new driver connected to a reused profile, a server-side login restored through authentication, or a remote Grid session that outlived the client.

Compare session IDs first. The Selenium Python WebDriver exposes session_id, the identifier returned for the controlled session. If two function-scoped tests log the same nonempty ID, inspect the fixture cache and imports. Perhaps a second fixture wraps a session-scoped driver, or a module global returns the same object. If IDs differ, the browser sessions are different and the shared state lives elsewhere.

Capabilities add context. Record driver.capabilities fields relevant to routing, such as browser name, browser version, and platform name, but avoid assuming every vendor exposes identical extension keys. On a Grid, correlate the Selenium session ID with server logs. A local process list alone cannot show whether the remote end deleted the session.

driver.close() and driver.quit() answer different ownership questions. close() closes the current window. quit() ends the session and closes every associated window. A test that opens a second tab and closes the first can leave the session healthy. Fixture teardown should normally quit because the fixture created the whole session.

The following diagnostic test proves that tests receive different sessions without asserting a vendor-specific format for the ID.

Python
# test_isolation.py
def test_session_identity_is_available(driver, record_property):
    assert driver.session_id
    record_property("selenium_session_id", driver.session_id)
    record_property("browser_name", driver.capabilities.get("browserName", "unknown"))


def test_new_function_gets_its_own_session(driver, record_property):
    assert driver.session_id
    record_property("selenium_session_id", driver.session_id)

The JUnit report will contain each test's recorded property. Review the actual values rather than hard-coding that IDs must be UUIDs. Selenium servers may choose another valid representation.

A near-miss is server-side account state. Two new session IDs can still log into the same user whose cart, feature flags, or locale persists in the application database. Browser cleanup cannot reset that. Allocate unique users per test or reset the account through a documented test-support boundary. Clearing cookies until the test passes hides the ownership error outside WebDriver.

Another near-miss is a custom Chrome profile supplied through browser arguments. New WebDriver sessions that point at one persistent user-data directory can restore local state. Parallel sessions may also contend for that directory. Remove the shared profile option from normal isolation tests. If profile persistence is the behavior under test, give each test a unique directory and clean it after quit().

A stale element error can look like cross-test contamination in a short CI log because it often appears after navigation or a rerender. It has a different boundary. A WebElement represents an element reference obtained in the current session; replacing that DOM node can make the reference stale even when the session is perfectly isolated. Compare the session ID, then inspect where the element was located and where it was reused. Re-locate the element after the state change or wait for the intended new state. Recreating the whole driver may hide the symptom while leaving the test's invalid element lifetime untouched.

An invalid session error points the other direction. If a test receives a driver object but its first command fails because the session has already ended, find who called quit() early. A helper may be treating a borrowed fixture as an owned driver, or a broad fixture may have recovered from one test by replacing a module global without updating all references. The fix is not a retry around every WebDriver command. Remove session shutdown from consumers and leave it with the fixture that constructed the driver.

Remote infrastructure adds one more distinction. A Grid node can disappear before client teardown. quit() may then raise because the remote endpoint cannot process the command. Keep the original session ID and Grid routing evidence, report the teardown failure, and use Grid-side observability to decide whether the server later reclaimed the session. Do not print QUIT_OK from a finally block unless the call actually returned successfully.

Distinguish a missed finalizer from a stuck quit

Two jobs can end with a browser still visible and almost identical pytest output. In the first, fixture finalization never started because the process was terminated or the driver was created outside the owning fixture. In the second, finalization started but quit() failed or never returned after the Grid connection or browser process became unhealthy. Rewriting both as pytest did not clean up sends the second problem to the wrong owner.

The existing DRIVER_TEARDOWN line is printed before browser.quit(). It proves that control entered the finally block. It does not prove the WebDriver command returned, that the remote server deleted the session, or that browser processes exited. Treat that line as a start marker.

Add a terminal outcome to the fixture's logging policy. A healthy sequence contains setup, teardown start, and teardown success with the same node ID and saved session ID. A missing-owner sequence contains setup with no teardown start. A stuck call contains teardown start with no success or error before the CI job terminates. A handled failure contains teardown start followed by an error outcome and the same session ID, which gives the Grid team a correlation key.

The session_id field is the first field to compare, not the operating-system process count. A process listing can include a different test, a Grid-managed browser, or a browser created by another tool. If JUnit reports a terminal success for session A while Grid still lists session A as active, the fixture completed its client command and the server-side lifecycle needs investigation. If the residual browser belongs to session B and no fixture record names B, search for a second constructor path.

Some values are actively misleading. A teardown start line labeled as though cleanup finished can make an unreturned quit() look healthy. A nonempty session ID proves creation reached a WebDriver session, not that shutdown succeeded. A zero pytest exit status also says nothing about process reclamation when teardown errors were swallowed by custom code. Preserve the exception outcome rather than converting cleanup into an unconditional success message.

Land outcome logging before changing fixture scope. Run one passing test, one deliberate call-phase failure in a disposable local proof, and one controlled remote-unavailability exercise in the environment owned by the Grid team. The expected evidence differs for each path, so the logging contract can be reviewed without migrating the suite at the same time. Remove any deliberate failing case from the normal product suite after the proof.

Then migrate constructor call sites and watch which sequence disappears. Tests that call quit() on a borrowed driver often fail first with an invalid session during artifact capture or fixture teardown. Tests that relied on a broad login state fail earlier in their product setup. A Grid near its session limit may show longer queueing once function scope creates one session per test. Those breakages have different owners and should not be grouped under one generic stability ticket.

The rollout is working when every recorded setup has one terminal teardown outcome, every terminal outcome uses the same session ID as setup, and Grid-side session records agree with the client result. Count successful quits separately from attempted quits and teardown errors. A one-to-one count of setup and start markers is insufficient because a hung command contributes to both.

The trade-off is specific. Function-scoped ownership increases browser creation requests, Grid slot turnover, and startup latency. Terminal logging adds small JUnit or text artifacts and requires retention long enough to correlate with Grid logs. Reusing a session reduces startup demand but transfers reset complexity into every consumer and makes one unhealthy session capable of invalidating several tests.

The test-framework owner owns fixture finalization and the node-to-session record. The Grid or browser-infrastructure owner owns remote deletion, node health, and reclamation after a lost client. The CI owner owns job termination signals and the time allowed for pytest to finalize. A handoff needs the node ID, saved session ID, local or remote branch, setup and teardown outcomes, the original test phase and error, the CI termination reason, and matching Grid log location. Capabilities or URLs containing credentials should be removed.

No fixture can run Python cleanup after the executor receives an unconditional hard kill. The outcome ledger can show that teardown never finished, but it cannot end the browser from a process that no longer exists. CI-level process isolation and Grid-side session reclamation must handle that failure. A longer finally block does not make it reachable after termination.

Wire local and remote drivers without changing ownership

The same fixture can choose a local driver or Selenium Grid from an environment variable. Keep both branches inside the function-scoped owner. Do not create a module-level Remote object simply because the Grid endpoint is shared.

Python
# conftest.py
import os

import pytest
from selenium import webdriver


@pytest.fixture
def driver(request):
    options = webdriver.ChromeOptions()
    options.add_argument("--window-size=1440,900")
    if os.getenv("CI"):
        options.add_argument("--headless")

    remote_url = os.getenv("SELENIUM_REMOTE_URL")
    if remote_url:
        browser = webdriver.Remote(
            command_executor=remote_url,
            options=options,
        )
    else:
        browser = webdriver.Chrome(options=options)

    session_id = browser.session_id
    try:
        print(
            f"DRIVER_SETUP nodeid={request.node.nodeid} "
            f"session={session_id} remote={bool(remote_url)}"
        )
        yield browser
    finally:
        browser.quit()

Remote creation costs network time and Grid capacity, while function scope provides strong isolation. That is the concrete trade-off. Before broadening scope, inspect real queue and session creation data from your Grid. Possible alternatives include running fewer tests per job, adding Grid capacity, moving non-browser checks down the test pyramid, or using API setup to shorten the browser path.

A CI job should retain phase information, JUnit output, pytest logs, and the artifact directory. This generic job assumes dependencies are installed by your project's existing requirements file.

YAML
steps:
  - name: Run isolated Selenium tests
    env:
      CI: "true"
      SELENIUM_REMOTE_URL: "http://selenium:4444"
    run: |
      mkdir -p artifacts
      pytest tests/ui \
        -vv \
        --junitxml=artifacts/junit.xml \
        --log-file=artifacts/pytest.log

Do not add --setup-show permanently to a large parallel suite unless the extra log volume helps your team. Enable it on a focused diagnostic job or reproduce one node. The useful record is the pairing between node ID and session ID, not thousands of unrelated fixture lines.

Migrate broad fixtures one failure class at a time

Inventory every place that creates a driver: conftest.py, base classes, helper modules, direct calls inside tests, and plugin fixtures. Search for both webdriver. constructors and webdriver.Remote. Also find every close() and quit() call. A test calling quit() on a shared fixture is just as dangerous as a fixture never calling it.

Create one function-scoped owner and move a small test module onto it. Keep assertions unchanged. Log session IDs and run that module in its normal CI parallel mode with reruns disabled. If failures appear, classify them. Tests may have depended on login state, a window left open by an earlier test, or server data created out of band. Those are dependencies the old fixture concealed.

Next, move screenshot capture into a dependent fixture and verify it runs before driver teardown. Force one deliberate assertion failure in a temporary local change, confirm the image is readable, then remove that deliberate failure before merging. The repository caller should perform that proof in its own validation environment; the article code should not leave a failing test behind.

Convert remaining modules in batches and watch actual job duration, Grid queue time, and session count. Do not claim a fixed slowdown before measuring your suite. If the added cost is unacceptable, optimize setup rather than weakening ownership without evidence.

During rollout, add a temporary audit that fails review when tests call webdriver.Chrome, webdriver.Firefox, or webdriver.Remote outside approved fixture modules. A text search is enough to find most direct constructors, but inspect aliases and factory wrappers manually. The goal is not permanent bureaucracy. It is to prevent new unowned sessions while the old paths are being removed.

Run a small canary job with reruns disabled and process cleanup visible at the CI runner boundary. Compare the number of successful session creations with the number of attempted quits and investigate mismatches by node ID. Those are counts from your real job, not universal performance targets. A one-to-one pattern can still include a failed quit, so keep outcomes as well as totals.

Keep session scope for tests whose subject is session persistence, multi-step journeys intentionally split across checks, or browser behavior that cannot be reconstructed per function. Mark those modules clearly and prevent parallel mutation. Even there, call quit() once from the session fixture and design recovery for a browser that becomes unusable midway through the run.

Do not use a shared driver for ordinary independent tests, call close() as universal teardown, hide quit() exceptions without logging the session, or take artifacts after the driver fixture has finalized. A good driver fixture is deliberately boring: one successful constructor, one owner, one handoff, one evidence trail, and one guaranteed attempt to end the session.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 25, 2026 / Reviewed August 7, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Official docs.pytest.org reference

    docs.pytest.org

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official docs.pytest.org reference

    docs.pytest.org

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

FAQ / QUICK ANSWERS

Questions testers ask

What scope should a Selenium driver fixture use in pytest?

Start with function scope so each test owns one WebDriver session. Broader scopes are appropriate only when tests are designed to share all browser state and the suite has a proven reset contract.

Why was driver.quit not called after pytest setup failed?

If setup raises before a plain yield statement, pytest cannot reach teardown code placed after that yield. Put post-creation setup and the yield inside a try block, then call quit from finally.

Is driver.close the same as driver.quit?

Closing a window removes the current browsing context, while quitting ends the WebDriver session and closes every associated window. Fixture teardown normally needs quit because the fixture owns the session, not one tab.

How do I save a Selenium screenshot before fixture cleanup?

Capture the test call report with pytest_runtest_makereport, then use a dependent fixture to save the screenshot during its teardown. That teardown runs before the lower-level driver fixture quits the session.

Can I use a session-scoped browser to make Selenium tests faster?

Only tests written for shared state should use one session for the whole run. Cookie deletion does not automatically restore every origin's storage, open window, permission, download, or server-side account state.