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.
📖 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_urlandprice_to_floatare now reusable from any test.- The text in triple quotes right under a
defis 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
import jsonbrings in the module; you writejson.loads(...). This makes it obvious where each name comes from.from module import namebrings names straight into your file; you writefull_url(...). Use it for things you use often, such asPageandexpect.as hgives 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
pagesandutilsare packages, so you import from them with dots:from pages.login_page import LoginPage.__init__.pymarks 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__.pystill works as a *namespace package*. Keeping an empty__init__.pyis 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 setup | pytest | python -m pytest |
|---|---|---|
| No conftest.py, no pythonpath | ModuleNotFoundError | Passes |
| Empty conftest.py in the root | Passes | Passes |
pythonpath = . in pytest.ini | Passes | Passes |
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.pyorpytest.pyin your project shadows the real package, because the script’s own folder comes first onsys.path. You then get errors likeModuleNotFoundError: No module named 'playwright.sync_api'; 'playwright' is not a package. Never reuse library names for your files.
- Circular imports. If
a.pyimportsb.pyandb.pyimportsa.py, one of them sees a half-loaded module. Move shared code into a third module. - Importing from test files. Tests should import from
pagesandutils, never from othertest_*.pyfiles. Shared things belong in packages orconftest.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
.pyfile; a package is a folder of modules, usually with__init__.py. - Prefer
import moduleorfrom package.module import Name; avoidimport *andsys.pathhacks. - Plain
pytestadds the test file’s folder tosys.path, sofrom pages... importfails 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