Python Modules, Packages and Imports for Playwright Test Projects

As soon as a test project grows beyond one file, you split code into modules: page objects in one place, test data in another, helpers in a third. Python’s import system connects them. In this lesson you will learn how modules and packages work, which import style to use, how to lay out a Playwright project, and how to fix the ModuleNotFoundError that almost every pytest beginner meets.

Advertisement

📖 What You’ll Learn: what a module and a package are, import styles (import, from … import, aliases), what __init__.py does, the if __name__ == “__main__” guard, a clean Playwright project layout, why plain pytest cannot find your pages package, and two reliable fixes (a root conftest.py or pythonpath in pytest.ini).


📦 Modules: One File, One Purpose

A module is simply a .py file. Everything defined in it (variables, functions, classes) can be imported into another file. Create utils/helpers.py:

📄 utils/helpers.py

"""Small helpers shared by many tests."""

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


def full_url(path: str) -> str:
    """Join the base URL and a path such as 'inventory.html'."""
    return BASE_URL + path.lstrip("/")


def price_to_float(text: str) -> float:
    """Convert '$29.99' to 29.99."""
    return float(text.replace("$", "").strip())


if __name__ == "__main__":
    # Runs only when this file is executed directly, not when it is imported
    print(full_url("/inventory.html"))
    print(price_to_float("$29.99"))
  • BASE_URL, full_url and price_to_float are now reusable from any test.
  • The text in triple quotes right under a def is a docstring; editors show it when you hover over the function.
  • path.lstrip("/") removes a leading slash so the URL never gets a double slash.

The if __name__ == "__main__" guard

Every module has a built-in variable __name__. When you run a file directly, Python sets it to "__main__"; when the file is imported, it is set to the module’s name (here "utils.helpers"). The guard lets a file contain quick self-checks that never run during an import:

⌨️ Terminal

python -m utils.helpers

🖥️ Output

https://www.saucedemo.com/inventory.html
29.99

python -m utils.helpers runs the module by its import name from the project folder, which is the most reliable way to run a module that lives inside a package.

A second small example shows the two values side by side (main_guard.py):

📄 main_guard.py

import utils.helpers

print("Imported module name:", utils.helpers.__name__)
print("This file's name:", __name__)

🖥️ Output

Imported module name: utils.helpers
This file's name: __main__

📥 Import Styles

There are three common ways to import, and each has its place:

import json                                   # 1. whole module
from utils.helpers import full_url, BASE_URL  # 2. specific names
from playwright.sync_api import Page, expect  # 2. the style you use in every test
import utils.helpers as h                     # 3. module with a short alias
  1. import json brings in the module; you write json.loads(...). This makes it obvious where each name comes from.
  2. from module import name brings names straight into your file; you write full_url(...). Use it for things you use often, such as Page and expect.
  3. as h gives a long module name a short alias.

⚠️ Watch out: Avoid from module import *. It imports every public name, hides where things come from, and can silently overwrite your own names.

Python looks for modules in a list of folders called sys.path: the folder of the script you run (or the current folder with python -m), then the virtual environment’s installed packages, then the standard library. If the folder that contains your package is not on that list, the import fails with ModuleNotFoundError.


🗂️ Packages: Folders of Modules

A package is a folder of modules. Traditionally it contains an __init__.py file, which can be empty. A typical Playwright project looks like this:

🖥️ Output

playwright-python-demo/
├── pytest.ini
├── conftest.py
├── pages/                  # package: page objects
│   ├── __init__.py
│   └── login_page.py
├── utils/                  # package: helpers and test data loaders
│   ├── __init__.py
│   └── helpers.py
└── tests/                  # test files (no imports from here)
    └── test_login.py
  • pages and utils are packages, so you import from them with dots: from pages.login_page import LoginPage.
  • __init__.py marks the folder as a regular package. It runs once when the package is first imported, so it is also a place for package-wide setup (most of the time it stays empty).
  • Since Python 3.3, a folder without __init__.py still works as a *namespace package*. Keeping an empty __init__.py is clearer and avoids surprises with tools, so this course always adds one.

Here is the page object and a test that uses it (pages/login_page.py and tests/test_login.py):

📄 pages/login_page.py

from playwright.sync_api import Page

from utils.helpers import full_url


class LoginPage:
    """Actions for the Sauce Demo login page."""

    def __init__(self, page: Page):
        self.page = page
        self.username = page.get_by_placeholder("Username")
        self.password = page.get_by_placeholder("Password")
        self.login_button = page.get_by_role("button", name="Login")

    def open(self) -> None:
        self.page.goto(full_url(""))

    def login(self, user: str, password: str) -> None:
        self.username.fill(user)
        self.password.fill(password)
        self.login_button.click()

📄 tests/test_login.py

import re

from playwright.sync_api import Page, expect

from pages.login_page import LoginPage
from utils.helpers import full_url, price_to_float


def test_login_with_page_object(page: Page):
    login = LoginPage(page)
    login.open()
    login.login("standard_user", "secret_sauce")
    expect(page).to_have_url(full_url("inventory.html"))


def test_backpack_price(page: Page):
    LoginPage(page).open()
    LoginPage(page).login("standard_user", "secret_sauce")
    price_text = page.locator("[data-test='inventory-item-price']").first.inner_text()
    assert price_to_float(price_text) == 29.99
    expect(page.locator(".title")).to_have_text(re.compile("Products"))
  • Imports are grouped in the order recommended by Python’s style guide (PEP 8): standard library (re), third-party packages (playwright), then your own project (pages, utils), with a blank line between groups.
  • The page object imports a helper from utils, and the test imports from both packages.

🚨 The Classic Error: ModuleNotFoundError: No module named ‘pages’

With exactly the layout above but without conftest.py and pytest.ini, running the pytest command fails during collection:

🖥️ Output

ImportError while importing test module 'C:\pwv\exp8\proj\tests\test_login.py'.
E   ModuleNotFoundError: No module named 'pages'
!!!!!!!!!!!!!!!!!!! Interrupted: 1 error during collection !!!!!!!!!!!!!!!!!!!!
1 error in 0.12s

Why? By default pytest adds the test file’s own folder (tests/) to sys.path, not the project root. The pages folder is in the root, so Python cannot see it.

Interestingly, python -m pytest works in the same situation, because running Python with -m also puts the current folder on sys.path. That difference confuses many people: a suite passes on one machine and fails on another depending on how it was started. Use one of these fixes so both commands always work:

Fix 1 (recommended): tell pytest where your code lives. Add pythonpath to pytest.ini:

📄 pytest.ini

[pytest]
pythonpath = .
testpaths = tests

pythonpath = . adds the folder that contains pytest.ini (the project root) to sys.path before tests are imported. It is explicit and works no matter how pytest is started.

**Fix 2: put a conftest.py in the project root.** pytest adds the folder of each conftest.py to sys.path, so a root conftest.py (even an empty one) also fixes the import. You will add fixtures there anyway:

📄 conftest.py

# Shared fixtures go here (see the pytest fixtures lessons in Module 3)

We tested all the combinations with pytest 9:

Project setuppytestpython -m pytest
No conftest.py, no pythonpathModuleNotFoundErrorPasses
Empty conftest.py in the rootPassesPasses
pythonpath = . in pytest.iniPassesPasses

With the fixes in place, the tests pass:

⌨️ Terminal

pytest -q

🖥️ Output

..                                                                       [100%]
2 passed in 3.35s

💡 Tip: Never add sys.path.append(...) lines at the top of test files to fix imports. They break as soon as files move. pythonpath = . in one place is the clean solution.


↔️ Relative Imports

Inside a package, a module can import a sibling with a leading dot, for example from .login_page import LoginPage inside pages/__init__.py. Relative imports only work for modules that are imported as part of a package, not for files run directly as scripts. In test projects, absolute imports (from pages.login_page import LoginPage) are simpler and easier to read, so prefer them.


⚠️ Common Mistakes

  • Naming a file after a library. A file called playwright.py or pytest.py in your project shadows the real package, because the script’s own folder comes first on sys.path. You then get errors like ModuleNotFoundError: No module named 'playwright.sync_api'; 'playwright' is not a package. Never reuse library names for your files.
  • Circular imports. If a.py imports b.py and b.py imports a.py, one of them sees a half-loaded module. Move shared code into a third module.
  • Importing from test files. Tests should import from pages and utils, never from other test_*.py files. Shared things belong in packages or conftest.py.

❓ Frequently Asked Questions

🤔 Do I need __init__.py in the tests folder?

No. pytest collects tests without it. Some teams add it to allow test files with the same name in different folders; if you do, pytest uses the package name to tell them apart.

🤔 What is the difference between a module and a package?

A module is one .py file. A package is a folder of modules (usually with an __init__.py), imported with dotted names such as pages.login_page.

🤔 Why does my import work in VS Code but fail in the terminal?

VS Code’s Testing view may start pytest differently (for example from a different folder). Adding pythonpath = . to pytest.ini makes the result the same everywhere.

🤔 Where should test data files live?

In their own folder, such as test_data/, loaded by helper functions in utils. You will do this in Module 7.


🏁 Summary

  • A module is a .py file; a package is a folder of modules, usually with __init__.py.
  • Prefer import module or from package.module import Name; avoid import * and sys.path hacks.
  • Plain pytest adds the test file’s folder to sys.path, so from pages... import fails without help.
  • Fix it with pythonpath = . in pytest.ini (or a root conftest.py), and keep library names out of your file names.

Next, you will take a closer look at virtual environments and pip, so every machine runs your tests with exactly the same packages.

🎬 Watch the Playwright tutorial series on YouTube!

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

Leave a Comment