pytest conftest.py Explained: Share and Override Fixtures Across Your Test Suite

Once fixtures are useful in more than one test file, they belong in conftest.py. pytest loads these special files automatically, makes their fixtures available to every test below them, and lets sub-folders override fixtures with their own versions. In this lesson you will build a small Playwright project with a root conftest.py and two sub-folder conftest files, see exactly where each test’s fixtures come from, and learn the rules that keep conftest files tidy.

Advertisement

📖 What You’ll Learn: what conftest.py is and where pytest looks for it, sharing fixtures without imports, how nested conftest files add and override fixtures, overriding a fixture while reusing the parent version, inspecting fixture sources with –fixtures-per-test, putting hooks in conftest.py, and common mistakes such as importing from conftest.


📄 What Is conftest.py?

conftest.py is a normal Python file with a special name. pytest finds it automatically and:

  • makes every fixture defined in it available to all tests in the same folder and all sub-folders, with no import;
  • runs any hook functions in it (functions with special names such as pytest_addoption) to customise pytest itself;
  • adds its folder to sys.path, which is why a root conftest.py also fixes the import problem from Lesson 19.

You can have one conftest.py per folder. They stack: a test sees the fixtures from every conftest.py between the project root and its own folder.


🗂️ The Example Project

🖥️ Output

project/
├── pytest.ini
├── conftest.py                 # shared by every test
└── tests/
    ├── test_home.py
    ├── shop/
    │   ├── conftest.py         # adds logged_in_page for shop tests
    │   └── test_cart.py
    └── admin/
        ├── conftest.py         # overrides the user fixture
        └── test_admin.py

📄 pytest.ini

[pytest]
testpaths = tests
pythonpath = .

The root conftest.py

📄 conftest.py

import pytest

BASE_URL = "https://www.saucedemo.com/"


@pytest.fixture(scope="session")
def base_url_value() -> str:
    """The application under test (shared by every test)."""
    return BASE_URL


@pytest.fixture
def user() -> dict:
    """The default user for most tests."""
    return {"username": "standard_user", "password": "secret_sauce", "role": "customer"}
  • base_url_value is session-scoped: it never changes during a run.
  • user is function-scoped and returns fresh data each time.

A test in the root tests folder

📄 tests/test_home.py

from playwright.sync_api import Page, expect


def test_login_page_title(page: Page, base_url_value: str):
    page.goto(base_url_value)
    expect(page).to_have_title("Swag Labs")


def test_default_user(user: dict):
    print(f"\n  tests/test_home.py sees role={user['role']}")
    assert user["username"] == "standard_user"

Neither fixture is imported: pytest finds them in the root conftest.py.


➕ Adding Fixtures in a Sub-folder

Fixtures that only the shop tests need go in tests/shop/conftest.py. They can use fixtures from the root conftest.py and from pytest-playwright:

📄 tests/shop/conftest.py

import re

import pytest
from playwright.sync_api import Page, expect


@pytest.fixture
def logged_in_page(page: Page, base_url_value: str, user: dict) -> Page:
    """A page that is already logged in as `user`."""
    page.goto(base_url_value)
    page.get_by_placeholder("Username").fill(user["username"])
    page.get_by_placeholder("Password").fill(user["password"])
    page.get_by_role("button", name="Login").click()
    expect(page).to_have_url(re.compile(r".*/inventory\.html"))
    return page

📄 tests/shop/test_cart.py

from playwright.sync_api import Page, expect


def test_add_backpack(logged_in_page: Page):
    logged_in_page.locator("[data-test='add-to-cart-sauce-labs-backpack']").click()
    expect(logged_in_page.locator("[data-test='shopping-cart-badge']")).to_have_text("1")

logged_in_page is visible to tests in tests/shop/ only. A test in tests/admin/ that asked for it would fail with “fixture ‘logged_in_page’ not found”.


🔄 Overriding a Fixture in a Sub-folder

The admin tests need a different user. Instead of a new fixture name, tests/admin/conftest.py overrides user. It can even receive the parent version by requesting a fixture with the same name:

📄 tests/admin/conftest.py

import pytest


@pytest.fixture
def user(user: dict) -> dict:
    """Same as the root user, but with the admin role."""
    return {**user, "role": "admin"}

📄 tests/admin/test_admin.py

def test_admin_role(user: dict):
    print(f"\n  tests/admin/test_admin.py sees role={user['role']}")
    assert user["role"] == "admin"
    assert user["username"] == "standard_user"   # inherited from the root fixture
  • Inside tests/admin/, the nearest user fixture wins.
  • Because the override asks for user as a parameter, pytest passes it the parent fixture’s value (from the root conftest.py). {**user, "role": "admin"} copies it and changes one key.
  • This is exactly the technique you used for browser_context_args in Lesson 6: your conftest.py overrides the plugin’s fixture and extends the original.

Run everything:

⌨️ Terminal

pytest -q -s

🖥️ Output

  tests/admin/test_admin.py sees role=admin
...
  tests/test_home.py sees role=customer
.
4 passed, 1 warning in 11.51s

(The warning is pytest-playwright’s harmless base_url deprecation notice from Lesson 2.)

The same fixture name gives admin in one folder and customer in another.


🔎 Where Does Each Fixture Come From?

When a project has several conftest files, --fixtures-per-test shows which file every fixture of a test comes from:

⌨️ Terminal

pytest -q --fixtures-per-test tests/admin/test_admin.py

🖥️ Output

---------------------- fixtures used by test_admin_role -----------------------
------------------------ (tests\admin\test_admin.py:1) ------------------------
delete_output_dir -- ..\_venv0\Lib\site-packages\pytest_playwright\pytest_playwright.py:95
    no docstring available
pytestconfig -- ...\_pytest\fixtures.py:1644
    Session-scoped fixture that returns the session's :class:`pytest.Config`
    object.
user -- tests\admin\conftest.py:4
    Same as the root user, but with the admin role.

user comes from tests\admin\conftest.py line 4, the override. The other two entries are automatic fixtures from pytest-playwright and pytest itself. The docstring of each fixture is shown under it, another reason to write one. Related commands: pytest --fixtures lists every available fixture, and pytest --fixtures -v adds the file each one comes from.


🪝 Hooks Also Live in conftest.py

Besides fixtures, conftest.py can contain hook functions: functions with names pytest recognises, which let you change pytest’s behaviour. For example, adding a custom command-line option:

def pytest_addoption(parser):
    parser.addoption("--env", default="staging", help="Environment to test against")

Option hooks such as pytest_addoption must live in the root conftest.py (or a plugin), because pytest reads them before it collects test folders. You will write hooks in pytest Plugins and Hooks.


🧭 A Recommended Layout for Playwright Projects

FilePut here
conftest.py (root)Settings overrides (browser_context_args), command-line options, session fixtures (config, API clients), screenshot-on-failure hooks
tests/<area>/conftest.pyFixtures for one feature area, such as logged_in_page for shop tests or an admin user
pages/, utils/ packagesPage objects and helpers (imported normally, not fixtures)

⚠️ Common Mistakes

  • Importing from conftest.py. Never write from conftest import user. pytest loads conftest files itself; importing them can load a second copy. If you need shared helper functions (not fixtures), put them in a normal module such as utils/helpers.py.
  • One giant root conftest.py. Move area-specific fixtures into the conftest.py of that folder.
  • Duplicate conftest.py at the same level as pytest.ini and in tests/ with conflicting fixtures. Keep one owner for each fixture name and override deliberately.
  • Expecting a sub-folder fixture to be visible higher up. Fixtures flow down the folder tree, never up or sideways.

❓ Frequently Asked Questions

🤔 Do I need __init__.py files for conftest.py to work?

No. pytest finds conftest.py files in the folders it collects tests from, with or without __init__.py.

🤔 Can a test file override a conftest fixture?

Yes. A fixture defined in the test module itself is the closest of all, so it wins for the tests in that file.

🤔 Why is my fixture “not found”?

The fixture is defined in a conftest.py that is not on the path from the root to the test’s folder (for example in a sibling folder), or the name is misspelled. pytest --fixtures lists what the test can see.

🤔 Can I override pytest-playwright fixtures such as page?

Yes, the same way, but prefer overriding the settings fixtures (browser_context_args, browser_type_launch_args) or writing new fixtures on top of page. Replacing page itself is rarely necessary.


🏁 Summary

  • conftest.py fixtures are available to every test in its folder and below, without imports.
  • Nested conftest files add fixtures and override names; an override can request the parent version by name.
  • --fixtures-per-test and pytest --fixtures show where fixtures come from.
  • Keep option hooks in the root conftest.py, area fixtures in sub-folders, and helpers in normal modules.

Next, you will run the same test with many inputs using @pytest.mark.parametrize.

🎬 Watch the Playwright tutorial series on YouTube!

Subscribe to SqaToolsByDY • Practice on the SQA Tools Automation Practice Page

Leave a Comment