---
title: 'pytest fixtures for Selenium: conftest.py and scope'
description: Every test file so far has carried its own copy of the same five-line
  `driver` fixture. This chapter moves it to one place, explains what `yield` and
  `scope`…
source_url:
  html: https://testingbot.com/resources/courses/selenium-python/pytest-fixtures
  md: https://testingbot.com/resources/courses/selenium-python/pytest-fixtures.md
---

Course

[Selenium with Python and pytest](https://testingbot.com/resources/courses/selenium-python)

Chapter 5 of 10

1. [1 Selenium Python Tutorial with pytest](https://testingbot.com/resources/courses/selenium-python)
2. [2 Set up Selenium with Python and pytest](https://testingbot.com/resources/courses/selenium-python/setup-and-first-test)
3. [3 Selenium locators in Python: the By API](https://testingbot.com/resources/courses/selenium-python/locators)
4. [4 Selenium waits in Python: WebDriverWait and time.sleep](https://testingbot.com/resources/courses/selenium-python/waits)
5. [5 pytest fixtures for Selenium: conftest.py and scope](https://testingbot.com/resources/courses/selenium-python/pytest-fixtures)
6. [6 Assertions, screenshots on failure and test reports](https://testingbot.com/resources/courses/selenium-python/assertions-screenshots-reports)
7. [7 Page objects in Python](https://testingbot.com/resources/courses/selenium-python/page-objects)
8. [8 Run Selenium tests in parallel with pytest-xdist](https://testingbot.com/resources/courses/selenium-python/parallel-with-pytest-xdist)
9. [9 Run Selenium Python tests on the TestingBot grid](https://testingbot.com/resources/courses/selenium-python/run-on-testingbot)
10. [10 Selenium Python Quiz](https://testingbot.com/resources/courses/selenium-python/knowledge-check)

[Exit course](https://testingbot.com/resources/courses)

 Chapter 5 of 10 Intermediate 6 min read 2 quick checks Updated Nov 2026 

# pytest fixtures for Selenium: conftest.py and scope
On this page
1. [How a fixture works](https://testingbot.com#how-a-fixture-works)
2. [conftest.py](https://testingbot.com#conftest-py)
3. [Fixture scope](https://testingbot.com#fixture-scope)
4. [Parametrising test data](https://testingbot.com#parametrising-test-data)
5. [Markers and pytest.ini](https://testingbot.com#markers-and-pytest-ini)

Every test file so far has carried its own copy of the same five-line `driver` fixture. This chapter moves it to one place, explains what `yield` and `scope` do, adds command-line switches for the browser and headless mode, and uses parametrisation to run one test with several inputs.

## How a fixture works[#](https://testingbot.com#how-a-fixture-works)

A fixture is a function decorated with `@pytest.fixture`. When a test lists the fixture's name as an argument, pytest calls the fixture first and passes in what it returns or yields:

```python
@pytest.fixture
def driver():
    driver = webdriver.Chrome() # setup
    yield driver # the test runs here
    driver.quit() # teardown
```

Everything before `yield` is setup, everything after it is teardown. pytest runs the teardown **whether the test passed or failed** , which is what the `try`/`finally` in chapter 1 did by hand. If the setup itself raises (Chrome fails to start, say), the test is reported as an error rather than a failure, and the teardown is skipped because there is nothing to clean up.

## conftest.py[#](https://testingbot.com#conftest-py)

pytest automatically loads a file named `conftest.py` and makes its fixtures available to every test in that directory and below, without any import. Put it in the project root, next to `pytest.ini`:

```python
import pytest
from selenium import webdriver

def pytest_addoption(parser):
    parser.addoption("--browser", action="append", choices=["chrome", "firefox"],
                     help="browser to run on; repeat for several (default: chrome)")
    parser.addoption("--headless", action="store_true", help="run browsers headless")

def pytest_generate_tests(metafunc):
    if "browser" in metafunc.fixturenames:
        metafunc.parametrize("browser", metafunc.config.getoption("browser") or ["chrome"])

@pytest.fixture
def driver(request, browser):
    headless = request.config.getoption("headless")
    if browser == "firefox":
        options = webdriver.FirefoxOptions()
        if headless:
            options.add_argument("-headless")
        driver = webdriver.Firefox(options=options)
    else:
        options = webdriver.ChromeOptions()
        if headless:
            options.add_argument("--headless=new")
            options.add_argument("--window-size=1920,1080")
        driver = webdriver.Chrome(options=options)

    yield driver
    driver.quit()
```

Delete the local `driver` fixtures from your test files: a fixture defined in a test module overrides the one in `conftest.py`, so a forgotten copy silently bypasses everything below. `pytest --fixtures-per-test` shows which fixture definition each test actually gets.

Three pieces work together here:

- **`pytest_addoption`** is a hook that registers command-line options. `action="append"` lets you pass `--browser` more than once.
- **`pytest_generate_tests`** is a hook pytest calls for every test function. If the test needs `browser` (directly, or through the `driver` fixture that asks for it), the hook parametrises it with the browsers from the command line.
- **`request`** is a built-in fixture that gives access to the test being run: `request.config` for options, `request.node` for the test item itself (chapter 5 uses that).

Now one set of tests runs on whatever you choose:

```bash
pytest -v --headless --browser chrome --browser firefox
```

```
tests/test_form.py::test_submit_shows_confirmation[chrome] PASSED [12%]
tests/test_form.py::test_submit_shows_confirmation[firefox] PASSED [25%]
tests/test_form.py::test_dropdown_value[chrome-One-1] PASSED [37%]
...
============================== 8 passed in 12.56s ==============================
```

The browser name in square brackets is part of each test's ID, so a failure tells you exactly which browser it happened on.

## Fixture scope[#](https://testingbot.com#fixture-scope)

`@pytest.fixture(scope=...)` decides how often a fixture is created:

| Scope | Created once per | Browser-test use |
| --- | --- | --- |
| `function` (default) | test | one fresh browser per test: the safe default |
| `class` | test class | rarely worth it |
| `module` | test file | a read-only smoke file where speed matters more than isolation |
| `session` | whole run (per worker with pytest-xdist) | expensive setup that is not a browser: test users, API tokens, a seeded database |

A wider scope is tempting because starting a browser takes a second or two. Here is what it costs. With a module-scoped browser, these three tests share one Chrome:

```python
@pytest.fixture(scope="module")
def shared_driver():
    options = webdriver.ChromeOptions()
    options.add_argument("--headless=new")
    driver = webdriver.Chrome(options=options)
    yield driver
    driver.quit()

def test_types_into_the_field(shared_driver):
    shared_driver.get(URL)
    shared_driver.find_element(By.ID, "my-text-id").send_keys("left over")

def test_expects_an_empty_field(shared_driver):
    # Same browser as the previous test. Passes only because get() reloads the page;
    # cookies and localStorage from the previous test would still be there.
    shared_driver.get(URL)
    assert shared_driver.find_element(By.ID, "my-text-id").get_attribute("value") == ""

def test_relies_on_previous_state(shared_driver):
    # No get(): this test silently depends on the one before it.
    assert shared_driver.title == "Web form"
```

Run the file and all three pass. Run only the last one, with `pytest tests/test_scope_demo.py::test_relies_on_previous_state`, and it fails with `assert '' == 'Web form'`: on its own, the browser never opened the page. Tests that pass together but fail alone (or in a different order, or on a different parallel worker) are the hardest kind of flaky test to diagnose. Keep the browser function-scoped and you never have that problem.

A suite passes when run in full but one test fails when run alone with `pytest path::test_name`. The driver fixture has `scope="session"`. What is the most likely cause?

 The test depends on browser state (a page, cookie or login) left behind by an earlier test. Running a single test skips `conftest.py`. Session-scoped fixtures do not run teardown.

With one shared browser, a test can quietly rely on what earlier tests did. Run alone, that state is missing. `conftest.py` is always loaded, and session fixtures do tear down, just once at the end.

## Parametrising test data[#](https://testingbot.com#parametrising-test-data)

`@pytest.mark.parametrize` runs one test function once per set of values:

```python
@pytest.mark.parametrize("option, value", [
    ("One", "1"),
    ("Two", "2"),
    ("Three", "3"),
])
def test_dropdown_value(driver, option, value):
    driver.get(URL)
    dropdown = Select(driver.find_element(By.NAME, "my-select"))
    dropdown.select_by_visible_text(option)
    assert dropdown.first_selected_option.get_attribute("value") == value
```

The parameter sets combine with the browser parameter, which is why the run above has `[chrome-One-1]` and `[firefox-Three-3]`: three values times two browsers is six tests. Each is reported, and can fail, independently, which beats a loop inside one test that stops at the first bad value. Add `ids=[...]` to the decorator if the generated IDs get hard to read.

## Markers and pytest.ini[#](https://testingbot.com#markers-and-pytest-ini)

Create `pytest.ini` in the project root:

```ini
[pytest]
testpaths = tests
addopts = -ra
markers =
    smoke: fast checks that run on every commit
```

- `testpaths` tells pytest where to look, so a stray script elsewhere is never collected.
- `addopts = -ra` prints a short summary of every non-passing test at the end of each run.
- `markers` registers custom marks. Decorate a test with `@pytest.mark.smoke`, then run just that group with `pytest -m smoke` or everything else with `pytest -m "not smoke"`. Registering marks means a typo such as `@pytest.mark.smok` produces a warning instead of a silently empty selection.

You changed the `driver` fixture in `conftest.py` to run headless, but one test file still opens a visible browser. What should you check first?

 Whether the file name starts with `test_`. Whether `conftest.py` was imported at the top of that file. Whether that file still defines its own `driver` fixture, which overrides the shared one.

A fixture defined in a test module takes precedence over one with the same name in `conftest.py`. The file is clearly being collected, since its tests run, and `conftest.py` must never be imported: pytest loads it automatically.

The fixture now owns the browser's whole lifetime, which makes it the natural place for the next feature: when a test fails, take a screenshot before the browser closes.

[Previous · Chapter 4 Selenium waits in Python: WebDriverWait and time.sleep](https://testingbot.com/resources/courses/selenium-python/waits) [Next · Chapter 6 Assertions, screenshots on failure and test reports](https://testingbot.com/resources/courses/selenium-python/assertions-screenshots-reports)

On this page

1. [How a fixture works](https://testingbot.com#how-a-fixture-works)
2. [conftest.py](https://testingbot.com#conftest-py)
3. [Fixture scope](https://testingbot.com#fixture-scope)
4. [Parametrising test data](https://testingbot.com#parametrising-test-data)
5. [Markers and pytest.ini](https://testingbot.com#markers-and-pytest-ini)

Certification

This course has a free, hands-on certificate.

[How it works](https://testingbot.com/resources/courses/selenium-python/certification)
