Run Playwright Python Tests: pytest and pytest-playwright CLI Options Explained

You can change how your whole test suite runs without touching a single line of test code. pytest-playwright adds options to the pytest command for picking browsers, watching tests run, emulating phones and capturing screenshots, videos and traces when something fails. In this lesson you will try each option on a small test file and see exactly what it produces.

📖 What You’ll Learn: running tests in Chromium, Firefox and WebKit, using installed Chrome or Edge, headed mode and slow motion, device emulation, capturing screenshots, videos and traces only on failure, the test-results folder, –base-url, and the core pytest flags for selecting and reporting tests.


🧪 The Test File for This Lesson

Create tests/test_cli.py. It has one passing test, one test that fails on purpose (so you can see failure artifacts), one test that prints the viewport size, and one test that uses a relative URL:

📄 tests/test_cli.py

from playwright.sync_api import Page, expect


def test_practice_title(page: Page):
    page.goto("https://sqatools.in/automation-practice-page/")
    expect(page).to_have_title("Automation Practice Page - SQA Tools")


def test_broken_heading(page: Page):
    # Fails on purpose: the real heading text is "Text Fields"
    page.goto("https://sqatools.in/automation-practice-page/")
    heading = page.get_by_role("heading", name="Text Fields")
    expect(heading).to_have_text("Text Boxes", timeout=1000)


def test_viewport(page: Page, browser_name: str):
    size = page.viewport_size
    is_mobile = "Mobile" in page.evaluate("navigator.userAgent")
    print(f"\n{browser_name}: viewport {size['width']}x{size['height']}, mobile user agent: {is_mobile}")


def test_relative_url(page: Page):
    # Works only when a base URL is set (see --base-url below)
    page.goto("/automation-practice-page/")
    expect(page).to_have_url("https://sqatools.in/automation-practice-page/")
  • browser_name is another pytest-playwright fixture: a string such as "chromium". Asking for it is as simple as adding a parameter.
  • page.viewport_size returns a dictionary like {'width': 1280, 'height': 720}.
  • timeout=1000 makes the failing assertion give up after 1 second instead of the default 5, so the demo is quick.

🌐 Choose the Browser: –browser and –browser-channel

Without options, tests run in Chromium. Pick another engine with --browser:

⌨️ Terminal

pytest --browser firefox
pytest --browser webkit

Repeat the option to run every test in several browsers in one go:

⌨️ Terminal

pytest --browser chromium --browser firefox --browser webkit -k title

🖥️ Output

...                                                                      [100%]
3 passed, 9 deselected in 9.37s

The single test_practice_title test ran three times, once per browser (that is what the three dots mean). With -v you would see the suffixes [chromium], [firefox] and [webkit].

To run Google Chrome or Microsoft Edge as installed on your machine (instead of Playwright’s own Chromium build), use a channel:

⌨️ Terminal

pytest --browser-channel chrome
pytest --browser-channel msedge

💡 Tip: Testing in branded Chrome or Edge is useful when you must match exactly what your users run. For everyday runs, Playwright’s bundled Chromium is faster to set up and identical across machines.


👀 Watch Tests Run: –headed and –slowmo

⌨️ Terminal

pytest --headed
pytest --headed --slowmo 1000
  • --headed opens visible browser windows. By default tests are headless.
  • --slowmo 1000 waits 1000 milliseconds after every Playwright action, so you can follow each step. Never leave it on in CI: it makes every test much slower.

📱 Emulate a Device: –device

--device applies one of Playwright’s built-in device profiles (screen size, user agent, touch support, pixel ratio) to every test:

⌨️ Terminal

pytest -s --device "iPhone 13" -k viewport

🖥️ Output

chromium: viewport 390x664, mobile user agent: True
1 passed, 3 deselected in 0.49s

Without the option, the same test prints the default desktop viewport:

⌨️ Terminal

pytest -s -k viewport

🖥️ Output

chromium: viewport 1280x720, mobile user agent: False
  • Device names must match Playwright’s list exactly, including capitals and spaces, so put them in quotes. Popular ones include "iPhone 13", "Pixel 7" and "iPad Pro 11".
  • The browser engine is still chosen by --browser (Chromium by default). For the most realistic iPhone emulation, combine --browser webkit --device "iPhone 13".
  • -s turns off pytest’s output capturing so that print() output appears in the terminal.

You will go deeper in Mobile Device Emulation.


📸 Capture Evidence: –screenshot, –video and –tracing

When a test fails in CI, you need evidence. These options record it automatically:

OptionValuesWhat you get
--screenshoton, off, only-on-failureA PNG taken at the end of each test
--full-page-screenshotflagMake those screenshots capture the full scrolling page (use with --screenshot)
--videoon, off, retain-on-failureA .webm recording of each test
--tracingon, off, retain-on-failureA trace.zip you can replay step by step in the Trace Viewer
--outputfolder pathWhere all of these go (default test-results)

The most useful combination keeps evidence only for failed tests:

⌨️ Terminal

pytest -k "not relative" --screenshot only-on-failure --video retain-on-failure --tracing retain-on-failure

🖥️ Output

FAILED tests/test_cli.py::test_broken_heading[chromium] - AssertionError: Loc...
1 failed, 2 passed, 1 deselected in 6.12s

(-k "not relative" skips the relative-URL test for now, because it needs the --base-url option shown later in this lesson.)

The test-results folder now holds one sub-folder, for the failed test only:

🖥️ Output

test-results/
└── tests-test-cli-py-test-broken-heading-chromium/
    ├── test-failed-1.png
    ├── trace.zip
    └── video.webm
  • The folder name is built from the file name, the test name and the browser, so failures in different browsers never overwrite each other.
  • Passing tests left nothing behind, which keeps CI artifacts small.
  • Open the trace with playwright show-trace test-results/tests-test-cli-py-test-broken-heading-chromium/trace.zip. You will learn the Trace Viewer in Module 11.

📝 Note: pytest-playwright clears the output folder at the start of each run, so old artifacts never get mixed with new ones.


🔗 Shorter URLs: –base-url

Typing the full URL in every test is repetitive, and it makes it hard to run the same tests against a staging server. Set a base URL once and use relative paths:

⌨️ Terminal

pytest --base-url https://sqatools.in -k relative

🖥️ Output

.                                                                        [100%]
1 passed, 3 deselected in 1.29s

page.goto("/automation-practice-page/") was resolved against the base URL. In pytest-playwright 0.10 the --base-url option works out of the box. You can also set it with the PYTEST_BASE_URL environment variable or a base_url entry in pytest.ini, which you will do in the next lesson.


🎯 Core pytest Options You Will Use Every Day

These come from pytest itself and combine freely with the Playwright options:

OptionMeaning
-v / -qMore / less output
-k "expression"Run tests whose names match, e.g. -k "login and not locked"
-m markerRun tests with a marker, e.g. -m smoke (Module 3)
-xStop after the first failure
--maxfail=3Stop after three failures
-sShow print() output
--lfRe-run only the tests that failed last time
--collect-onlyList the tests without running them
--durations=5Show the five slowest tests

For example, --collect-only -q lists every test with its browser suffix:

⌨️ Terminal

pytest --collect-only -q

🖥️ Output

tests/test_cli.py::test_practice_title[chromium]
tests/test_cli.py::test_broken_heading[chromium]
tests/test_cli.py::test_viewport[chromium]
tests/test_cli.py::test_relative_url[chromium]

4 tests collected in 0.00s

🧾 Quick Reference

⌨️ Terminal

# All browsers, verbose
pytest --browser chromium --browser firefox --browser webkit -v

# Debug one test visually
pytest tests/test_login.py::test_valid_login_shows_products --headed --slowmo 500

# CI-style run with evidence on failure
pytest --tracing retain-on-failure --screenshot only-on-failure --video retain-on-failure

# Same tests against another environment
pytest --base-url https://staging.example.com

To see every option on your machine, run pytest --help and look for the Playwright section.


❓ Frequently Asked Questions

🤔 How do I avoid typing the same options every time?

Put them in the addopts setting of pytest.ini or pyproject.toml. That is the first thing you will do in the next lesson.

🤔 Can I run tests in parallel?

Yes, with the pytest-xdist plugin (pytest -n auto). It is covered in Parallel Execution with pytest-xdist.

🤔 Do these options affect browsers I create myself?

No. They configure the browser, context and page fixtures from pytest-playwright. A browser you launch yourself with sync_playwright() inside a test is not affected.

🤔 Is there a –retries option?

Not in pytest-playwright. Retries come from the pytest-rerunfailures plugin (pytest --reruns 2), covered in Module 6.


🏁 Summary

  • --browser (repeatable) and --browser-channel choose where tests run; --headed and --slowmo let you watch.
  • --device emulates phones and tablets; -s shows printed output.
  • --screenshot only-on-failure --video retain-on-failure --tracing retain-on-failure keeps evidence for failed tests in test-results.
  • --base-url lets tests use relative URLs; pytest’s own flags (-k, -x, -v, --lf) select and report tests.

Next, you will move these options into configuration files and learn how conftest.py customises the browser and context for the whole suite.

🎬 Watch the Playwright tutorial series on YouTube!

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

Write and Run Your First pytest Playwright Test in Python

Now you will write real tests. With pytest and the pytest-playwright plugin, you never launch or close a browser yourself: you ask for a page, use it, and pytest cleans up. In this lesson you will understand every line of a Playwright test, automate a login page, add negative tests, run them in different ways, and learn to read a failure message.

📖 What You’ll Learn: test anatomy (test function, page fixture, expect), pytest discovery rules, writing a login test with Arrange-Act-Assert, negative tests, why every test gets a fresh page, running tests from the CLI, and reading a failing test’s output.


🧱 Anatomy of a Playwright Test

Create a tests folder in your project and add tests/test_title.py:

📄 tests/test_title.py

from playwright.sync_api import Page, expect                      # 1. imports


def test_practice_page_has_title(page: Page):                    # 2. a test function
    page.goto("https://sqatools.in/automation-practice-page/")   # 3. an action
    expect(page).to_have_title("Automation Practice Page - SQA Tools")   # 4. an assertion
  1. Imports. Page is the type of the page object (used for editor auto-complete) and expect creates Playwright assertions.
  2. A test function. pytest runs any function whose name starts with test_. The parameter page is a fixture: pytest-playwright sees the name, creates a fresh page in a new browser context, passes it in, and closes it after the test. The : Page part is a type hint; it does not change behaviour, but it gives you auto-complete in VS Code.
  3. An action. page.goto() opens the URL.
  4. An assertion. expect(page).to_have_title(...) checks the title. If it does not match yet, it retries for up to 5 seconds before failing. This is called a *web-first assertion*.

Run it:

⌨️ Terminal

pytest tests/test_title.py

🔍 How pytest Finds Your Tests

When you run pytest, it searches the current folder and its subfolders. By default it collects:

  • files named test_*.py or *_test.py,
  • functions named test_* in those files,
  • methods named test_* inside classes named Test* (the class must not have an __init__ method).

So tests/test_login.py with def test_valid_login(page) is found automatically, while tests/login.py or def check_login(page) would be silently ignored. You will learn how to change these rules in pytest Configuration.

⚠️ Watch out: A test file named test_something.py that is never collected is almost always caused by a typo in the file or function name. Run pytest --collect-only to list what pytest found without running it.


✍️ A Real Test: Login

Next, automate the login page of Sauce Demo (https://www.saucedemo.com), a free practice shop that accepts the username standard_user and password secret_sauce. Create tests/test_login.py:

📄 tests/test_login.py

import re

from playwright.sync_api import Page, expect


def test_valid_login_shows_products(page: Page):
    # Arrange: open the login page
    page.goto("https://www.saucedemo.com/")

    # Act: fill the form and submit
    page.get_by_placeholder("Username").fill("standard_user")
    page.get_by_placeholder("Password").fill("secret_sauce")
    page.get_by_role("button", name="Login").click()

    # Assert: we land on the products page
    expect(page).to_have_url(re.compile(r".*/inventory\.html"))
    expect(page.locator(".title")).to_have_text("Products")


def test_wrong_password_shows_error(page: Page):
    page.goto("https://www.saucedemo.com/")

    page.get_by_placeholder("Username").fill("standard_user")
    page.get_by_placeholder("Password").fill("wrong_password")
    page.get_by_role("button", name="Login").click()

    error = page.locator("[data-test='error']")
    expect(error).to_be_visible()
    expect(error).to_contain_text("Username and password do not match")


def test_locked_out_user_cannot_log_in(page: Page):
    page.goto("https://www.saucedemo.com/")

    page.get_by_placeholder("Username").fill("locked_out_user")
    page.get_by_placeholder("Password").fill("secret_sauce")
    page.get_by_role("button", name="Login").click()

    expect(page.locator("[data-test='error']")).to_contain_text("locked out")
    expect(page).to_have_url("https://www.saucedemo.com/")

What the new pieces do:

  • Arrange, Act, Assert. The comments split the first test into setting up, doing the user action, and checking the result. Keeping this shape makes tests easy to read and review.
  • get_by_placeholder("Username") finds an input by its placeholder text; get_by_role("button", name="Login") finds the button by its role and visible name, the way a user sees it.
  • to_have_url(re.compile(...)) accepts either an exact string or a regular expression. The pattern .*/inventory\.html means “any URL ending in /inventory.html”; the backslash makes the dot literal.
  • page.locator(".title") uses a CSS selector. to_have_text("Products") checks the element’s full text, while to_contain_text() checks for a substring.
  • "[data-test='error']" is a CSS attribute selector. Many apps add data-test or data-testid attributes for exactly this purpose.
  • The second and third tests are negative tests: they prove the app rejects bad input. A good suite always tests both the happy path and the failure paths.

Run the file with -v (verbose) to see each test by name:

⌨️ Terminal

pytest tests/test_login.py -v

🖥️ Output

tests/test_login.py::test_valid_login_shows_products[chromium] PASSED    [ 33%]
tests/test_login.py::test_wrong_password_shows_error[chromium] PASSED    [ 66%]
tests/test_login.py::test_locked_out_user_cannot_log_in[chromium] PASSED [100%]

The [chromium] suffix is added by pytest-playwright: it shows which browser ran the test. Chromium is the default, and you will run the same tests in Firefox and WebKit in the next lesson.


🔒 Every Test Gets a Fresh Page

The tests above never log out, yet the second test starts on the login page as a stranger. That is because each test receives a new browser context: new cookies, new storage, nothing left over from the previous test. This file proves it (tests/test_isolation.py):

📄 tests/test_isolation.py

from playwright.sync_api import Page


def test_first_saves_a_value(page: Page):
    page.goto("https://sqatools.in/automation-practice-page/")
    page.evaluate("localStorage.setItem('step', 'done')")
    assert page.evaluate("localStorage.getItem('step')") == "done"


def test_second_starts_clean(page: Page):
    page.goto("https://sqatools.in/automation-practice-page/")
    assert page.evaluate("localStorage.getItem('step')") is None
  • The first test stores a value in the page’s localStorage and checks it with a plain Python assert.
  • The second test opens the same site in its own fresh context and finds nothing there.

💡 Tip: Use plain assert for Python values you already have (numbers, strings, lists). Use expect(...) for anything on the page, because it waits and retries while the page catches up.


💥 Reading a Failure

Tests fail, and reading the failure quickly is a core skill. Here is a test with a deliberate mistake (tests/test_failing.py):

📄 tests/test_failing.py

from playwright.sync_api import Page, expect


def test_title_typo(page: Page):
    page.goto("https://sqatools.in/automation-practice-page/")
    expect(page).to_have_title("Automation Practise Page - SQA Tools")

The expected title says “Practise” instead of “Practice”. Run it:

⌨️ Terminal

pytest tests/test_failing.py

🖥️ Output

================================== FAILURES ===================================
__________________________ test_title_typo[chromium] __________________________

page = <Page url='https://sqatools.in/automation-practice-page/'>

    def test_title_typo(page: Page):
        page.goto("https://sqatools.in/automation-practice-page/")
>       expect(page).to_have_title("Automation Practise Page - SQA Tools")
E       AssertionError: Page title expected to be 'Automation Practise Page - SQA Tools'
E       Actual value: Automation Practice Page - SQA Tools
E       Call log:
E         - Expect "to_have_title" with timeout 5000ms
E           13 × locator resolved to <html lang="en-US" prefix="og: https://ogp.me/ns#">…</html>
E              - unexpected value "Automation Practice Page - SQA Tools"
E
E       Aria snapshot:
E       - link "Skip to content":
E         - /url: "#content"
E       - banner "Site":
E       ...  (the rest of the page structure, cut here)

tests\test_failing.py:6: AssertionError
=========================== short test summary info ===========================
FAILED tests/test_failing.py::test_title_typo[chromium] - AssertionError: Pag...
============================== 1 failed in 7.08s ==============================

How to read it, top to bottom:

  1. **The > marker** points at the line that failed, inside a copy of your test code.
  2. What was expected and what the page actually had (Actual value). Comparing these two lines usually reveals the bug immediately.
  3. The call log shows that Playwright kept checking for 5 seconds (the default assertion timeout) and saw the same wrong value each time, so this is not a timing problem.
  4. The aria snapshot (added by recent Playwright versions) is a text outline of the page at the moment of failure: headings, links, buttons, fields. It helps you see what was really on screen, especially in CI where you cannot watch the browser.
  5. The file and line number (test_failing.py:6) and the short summary list every failed test with its browser.

📝 Note: The exact retry count in the call log (here 13 ×) and the run time vary from run to run. That is normal.


▶️ Running Tests: The Essentials

CommandWhat it does
pytestRun every test pytest can find
pytest tests/test_login.pyRun one file
pytest tests/test_login.py::test_wrong_password_shows_errorRun one test
pytest -k "login and not locked"Run tests whose names match the expression
pytest -vShow each test name and result
pytest -xStop at the first failure
pytest --headedShow the browser window

The next lesson covers every Playwright-specific option, such as --browser, --slowmo, --screenshot and --tracing.


❓ Frequently Asked Questions

🤔 Why do I not need to launch or close the browser?

The page fixture from pytest-playwright does it. It reuses one browser for the whole run, creates a new context and page for each test, and closes them afterwards, even when the test fails.

🤔 Can I put tests in a class?

Yes. Name the class Test..., do not give it an __init__, and add page as a parameter to each method: def test_login(self, page: Page):. Classes are useful for grouping related tests.

🤔 Why use expect instead of assert?

expect waits and retries until the condition is true or the timeout (5 seconds by default) is reached. A plain assert page.title() == "..." checks only once, so it can fail if the page is still loading.

🤔 Why does the test name end with [chromium]?

pytest-playwright parametrizes tests by browser. If you run with --browser firefox --browser webkit, each test runs once per browser and the suffix shows which run you are looking at.


🏁 Summary

  • A test is a test_ function in a test_*.py file that asks for the page fixture.
  • expect() assertions wait and retry; use plain assert only for values you already hold.
  • Structure tests as Arrange, Act, Assert, and always include negative tests.
  • Each test gets a new context and page, so tests never share state.
  • Failures show expected vs actual values, a call log and the failing line.

Next, you will learn every pytest and pytest-playwright command-line option for running tests in different browsers, headed mode, slow motion and more.

🎬 Watch the Playwright tutorial series on YouTube!

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

Your First Playwright Python Script with sync_playwright (Browser, Context and Page)

Before pytest takes over the browser for you, it is worth writing one complete Playwright script by hand. You will see exactly what happens when a test starts: Playwright launches a browser, creates an isolated context, opens a page, performs actions and closes everything down. Once you understand these four objects, every fixture and error message later in the course will make sense.

📖 What You’ll Learn: the Playwright object hierarchy (Playwright, Browser, BrowserContext, Page), launch options like headless and slow_mo, interacting with a real form, reading text and attributes, taking screenshots, proving that contexts are isolated, and closing resources safely with with and try/finally.


🧱 The Four Objects You Always Use

Every Playwright program, including every pytest test you will write, uses the same chain of objects:

ObjectCreated byWhat it represents
Playwrightsync_playwright()The running Playwright driver. Gives you chromium, firefox, webkit and devices.
Browserp.chromium.launch()One browser process (expensive to start, so reuse it).
BrowserContextbrowser.new_context()An isolated, incognito-like session with its own cookies, storage and cache. Cheap to create.
Pagecontext.new_page()One tab inside a context. Almost all actions happen here.

Think of the browser as a building, each context as a separate flat with its own key, and each page as a room in a flat. Two contexts never share cookies or logins, which is why Playwright tests do not leak state into each other.


✍️ A Complete First Script

This script opens the SQA Tools Automation Practice Page, fills in some fields, reads values back and saves a screenshot. Save it as first_script.py:

📄 first_script.py

from playwright.sync_api import sync_playwright


with sync_playwright() as p:
    # 1. Launch a browser and create an isolated context with one page
    browser = p.chromium.launch()
    context = browser.new_context()
    page = context.new_page()

    # 2. Open the practice page
    page.goto("https://sqatools.in/automation-practice-page/")
    print("Page title:", page.title())

    # 3. Type into text fields, located by their placeholder text
    page.get_by_placeholder("Enter username").fill("qa_learner")
    page.get_by_placeholder("Enter password").fill("Secret@123")

    # 4. Choose a radio button, tick a checkbox and pick a dropdown option
    page.get_by_role("radio", name="Female").check()
    page.get_by_role("checkbox", name="Python").check()
    page.locator("select").first.select_option("India")

    # 5. Read the values back from the page
    print("Username field:", page.get_by_placeholder("Enter username").input_value())
    print("Python checked:", page.get_by_role("checkbox", name="Python").is_checked())
    print("Second table row:", page.get_by_role("row").nth(2).inner_text())

    # 6. Save a screenshot of the visible part of the page
    page.screenshot(path="practice-page.png")

    # 7. Close everything
    context.close()
    browser.close()

Run it:

⌨️ Terminal

python first_script.py

🖥️ Output

Page title: Automation Practice Page - SQA Tools
Username field: qa_learner
Python checked: True
Second table row: 2	Rahul	Tester

A file named practice-page.png appears next to your script.

What each part does

  1. Launch and open. launch() starts the browser, new_context() creates a clean session, and new_page() opens a tab in it.
  2. **page.goto(url)** navigates and waits for the page’s load event before the next line runs.
  3. **get_by_placeholder("Enter username") creates a locator**: a description of how to find an element. Nothing is searched yet. fill() then finds the input, waits until it is editable, clears it and types the text.
  4. **get_by_role("radio", name="Female")** finds the element the way a screen reader would: by its role (radio) and accessible name (Female). check() ticks radios and checkboxes. select_option("India") picks the option whose value or label is India from the first <select> on the page.
  5. Reading values. input_value() returns what is typed in a field, is_checked() returns True or False, and inner_text() returns the visible text. nth(2) picks the third match (counting starts at 0, and row 0 is the header row). The cells in the printed row are separated by tab characters.
  6. **page.screenshot(path=...)** saves a PNG. Add full_page=True to capture the whole scrolling page.
  7. Closing. Closing the context closes its pages; closing the browser ends the process. Leaving the with block then stops the Playwright driver.

💡 Tip: Notice there is no time.sleep() anywhere. Every action waits automatically until its element is ready. This auto-waiting is the main reason Playwright tests are stable.


👀 Watch the Browser: headless and slow_mo

By default the browser is headless: it runs without a window, which is faster and works on servers. While learning, it helps to watch. Save this as launch_options.py:

📄 launch_options.py

from playwright.sync_api import sync_playwright


with sync_playwright() as p:
    browser = p.chromium.launch(
        headless=False,   # show the browser window
        slow_mo=500,      # wait 500 ms after each Playwright action
    )
    page = browser.new_page()

    page.goto("https://sqatools.in/automation-practice-page/")
    page.get_by_role("button", name="Hover Over Me").hover()
    print("Hovered on Chromium version", browser.version)

    browser.close()
  • headless=False opens a real window so you can follow along.
  • slow_mo=500 adds a 500 millisecond pause after every action. Use it only for watching or demos, never in real test suites.
  • browser.new_page() is a shortcut that creates a context and a page in one call. It is handy in quick scripts; when you need several pages in the same session, create the context yourself.
  • browser.version returns the browser’s version string.

🖥️ Output

Hovered on Chromium version 156.0.8078.4

To try another engine, change one word: p.firefox.launch(...) or p.webkit.launch(...). The rest of the script stays the same.


🔒 Contexts Are Isolated

This is one of Playwright’s most useful ideas. Two contexts in the same browser behave like two different people on two different computers. Save this as contexts.py:

📄 contexts.py

from playwright.sync_api import sync_playwright


with sync_playwright() as p:
    browser = p.chromium.launch()

    # Two independent sessions in ONE browser process
    alice = browser.new_context()
    bob = browser.new_context()

    alice_page = alice.new_page()
    bob_page = bob.new_page()

    alice_page.goto("https://sqatools.in/automation-practice-page/")
    bob_page.goto("https://sqatools.in/automation-practice-page/")

    # Alice stores a value in her browser's localStorage
    alice_page.evaluate("localStorage.setItem('user', 'alice')")

    # Bob cannot see it: each context has its own storage
    print("Alice sees:", alice_page.evaluate("localStorage.getItem('user')"))
    print("Bob sees:  ", bob_page.evaluate("localStorage.getItem('user')"))

    print("Contexts open:", len(browser.contexts))
    browser.close()

🖥️ Output

Alice sees: alice
Bob sees:   None
Contexts open: 2
  • page.evaluate("...") runs JavaScript inside the page and returns the result to Python. JavaScript null arrives in Python as None.
  • Bob’s context has no idea what Alice stored, even though both pages are in the same browser.

This is exactly what pytest-playwright does for you: one browser for the whole test session, and a brand-new context and page for every test. Tests start fast, and no cookies or logins leak from one test into the next.


🧹 Closing Safely When Something Fails

If a line throws an error, the code after it never runs. The with sync_playwright() block already guarantees that the driver stops. To make sure the browser itself is closed too, wrap your steps in try/finally. Save this as try_finally.py:

📄 try_finally.py

from playwright.sync_api import sync_playwright, TimeoutError as PlaywrightTimeoutError


with sync_playwright() as p:
    browser = p.chromium.launch()
    page = browser.new_page()
    try:
        page.goto("https://sqatools.in/automation-practice-page/")

        # This button does not exist, so the click times out after 2 seconds
        page.get_by_role("button", name="Does Not Exist").click(timeout=2000)

    except PlaywrightTimeoutError as error:
        print("Step failed:", error.message.splitlines()[0])

    finally:
        browser.close()     # always runs, even after an error
        print("Browser closed")

🖥️ Output

Step failed: Locator.click: Timeout 2000ms exceeded.
Browser closed
  • TimeoutError from playwright.sync_api is Playwright’s own timeout error. Importing it with an alias avoids confusing it with Python’s built-in TimeoutError.
  • timeout=2000 overrides the default action timeout of 30 seconds for this one click.
  • error.message holds the full message; the first line names the action and the timeout. The lines after it (the call log) show what Playwright was waiting for.
  • The finally block always runs, so the browser never stays open.

📝 Note: You will rarely write try/finally around tests yourself. pytest fixtures handle cleanup automatically, even when a test fails. You will see this in the next lesson.


❓ Frequently Asked Questions

🤔 What is the difference between browser.new_page() and context.new_page()?

browser.new_page() creates a new context and a page in it in one step, and closing that page also closes its context. context.new_page() adds a page to an existing context, so several pages can share the same cookies and login.

🤔 Why does my script finish but the browser window closes immediately?

The script reached browser.close(). To pause and look around, add page.pause() (it opens the Playwright Inspector) or run with slow_mo. You will learn debugging in Module 11.

🤔 Can I connect to a browser that is already open?

Yes, with p.chromium.connect_over_cdp("http://localhost:9222") for Chromium browsers started with remote debugging enabled. Most test suites launch their own browser instead, because it is more reliable.

🤔 What is the default timeout for actions?

30 seconds for actions such as click() and fill(), and 30 seconds for navigation. You can change it per call with timeout= or for a whole context with context.set_default_timeout().


🏁 Summary

  • sync_playwright() gives you a Playwright object; it launches a Browser, which creates BrowserContexts, which open Pages.
  • Locators such as get_by_placeholder() and get_by_role() describe elements; actions such as fill() and check() wait automatically.
  • headless=False and slow_mo help you watch a script; contexts keep sessions fully isolated.
  • Close what you open, or let with blocks and pytest fixtures do it for you.

Next, you will turn this knowledge into your first real pytest test, where the page fixture does all of this setup and cleanup for you.

🎬 Watch the Playwright tutorial series on YouTube!

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

Install Playwright for Python and pytest (Virtual Environment and VS Code Setup)

Before you write a single test you need a clean, repeatable setup. In this lesson you will install Python, create a virtual environment for your test project, install Playwright and pytest with one command, download the browsers, and set up VS Code so it can discover and run your tests.

📖 What You’ll Learn: checking your Python version, creating and activating a virtual environment on Windows, macOS and Linux, installing pytest-playwright, downloading browsers with playwright install, freezing versions in requirements.txt, and running tests from VS Code.


🧰 What You Need

  • Python 3.10 or newer. Playwright 1.64 and pytest-playwright 0.10 both require it.
  • About 1 GB of free disk space for the three browsers.
  • A code editor. This course uses VS Code, which is free.
  • An internet connection for the first install.

You do not need Node.js, Java or a separate WebDriver download.


🐍 Step 1: Install and Check Python

Download Python from python.org/downloads. On Windows, tick “Add python.exe to PATH” on the first installer screen. Then open a new terminal and check the version:

⌨️ Terminal

python --version

On macOS and Linux the command is usually python3:

⌨️ Terminal

python3 --version

You should see Python 3.10 or higher. If you see an older version, or “command not found”, install a current version before continuing.

💡 Tip: In this course, commands use python. If your system only has python3, use that instead everywhere.


📁 Step 2: Create a Project Folder and a Virtual Environment

A virtual environment is a private folder of Python packages for one project. It keeps this project’s Playwright and pytest versions separate from other projects and from the system Python, so upgrading one project never breaks another. You will learn more in Python Virtual Environments and pip.

Create a folder and a virtual environment named .venv inside it:

⌨️ Terminal

mkdir playwright-python-demo
cd playwright-python-demo
python -m venv .venv
  • mkdir creates the project folder and cd moves into it.
  • python -m venv .venv runs Python’s built-in venv module and creates a .venv folder containing a private copy of Python and pip.

Activate the virtual environment

Activation points your terminal’s python and pip at the .venv folder. Use the command for your operating system:

Operating system / shellActivate command
Windows PowerShell.venv\Scripts\Activate.ps1
Windows Command Prompt.venv\Scripts\activate.bat
macOS / Linux (bash, zsh)source .venv/bin/activate

When it works, your prompt starts with (.venv). Run deactivate to leave it.

⚠️ Watch out: If PowerShell says that running scripts is disabled, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned once, then activate again. This changes the policy only for your user account.


📦 Step 3: Install pytest-playwright

With the virtual environment active, install the official plugin:

⌨️ Terminal

pip install pytest-playwright

This one command installs three things:

  1. pytest-playwright, the plugin that adds the page fixture and Playwright command-line options.
  2. playwright, the browser automation library (installed automatically as a dependency).
  3. pytest, the test runner (also a dependency).

Check the installed versions:

⌨️ Terminal

playwright --version
pytest --version

🖥️ Output

Version 1.64.1
pytest 9.2.0

Your numbers may be newer. That is fine: everything in this course works with Playwright 1.64 and later.


🌐 Step 4: Download the Browsers

The Python package does not contain the browsers themselves. Download the versions that match your Playwright release:

⌨️ Terminal

playwright install

This downloads Chromium, Firefox and WebKit into a shared cache folder (on Windows %USERPROFILE%\AppData\Local\ms-playwright, on macOS ~/Library/Caches/ms-playwright, on Linux ~/.cache/ms-playwright). Every project on your machine that uses the same Playwright version reuses them.

Useful variations:

  • playwright install chromium downloads only Chromium (faster for a first try).
  • playwright install --with-deps also installs the operating-system libraries the browsers need. Use it on Linux and in CI containers; it may ask for administrator (sudo) permission.
  • playwright install --dry-run shows what would be downloaded and where, without downloading.

📝 Note: Each Playwright version needs its own browser builds. After pip install --upgrade playwright, run playwright install again.


✅ Step 5: Verify the Setup with a Test

Create a folder named tests and a file tests/test_setup.py inside it:

📄 tests/test_setup.py

from playwright.sync_api import Page, expect


def test_setup_works(page: Page):
    # Open the SQA Tools practice page
    page.goto("https://sqatools.in/automation-practice-page/")

    # The test passes if the page title matches
    expect(page).to_have_title("Automation Practice Page - SQA Tools")

Run it from the project folder:

⌨️ Terminal

pytest

🖥️ Output

.                                                                        [100%]
1 passed, 1 warning in 1.98s

The single dot means one test passed. pytest found the test because the file name starts with test_ and the function name starts with test_. You will learn these discovery rules in Lesson 4.

📝 Note: With pytest 9.2 and pytest-playwright 0.10.0 you may also see a PytestRemovedIn10Warning about “Session-scoped fixtures defined as instance methods” for the base_url fixture. It comes from inside the plugin, not from your code, and it does not affect your results.


📌 Step 6: Pin Your Versions in requirements.txt

So that teammates and CI install exactly the same versions, save them to a file:

⌨️ Terminal

pip freeze > requirements.txt

pip freeze lists every installed package with its exact version. On a fresh Windows install in November 2026 the file contained these lines (your versions may be newer, and colorama only appears on Windows):

🖥️ Output

colorama==0.4.6
greenlet==3.5.6
iniconfig==2.3.1
packaging==26.3
playwright==1.64.1
pluggy==1.7.0
pyee==14.0.0
Pygments==2.21.0
pytest==9.2.0
pytest-playwright==0.10.0
python-slugify==9.1.3
text-unidecode==1.3
typing_extensions==4.16.0

Anyone can now recreate your environment with:

⌨️ Terminal

pip install -r requirements.txt
playwright install

💡 Tip: Add .venv/ to your .gitignore file. Commit requirements.txt, never the virtual environment folder itself. See our guide to .gitignore.


💻 Step 7: Set Up VS Code

  1. Open the project folder in VS Code (File > Open Folder).
  2. Install the Python extension by Microsoft from the Extensions view (Ctrl+Shift+X).
  3. Press Ctrl+Shift+P, run Python: Select Interpreter, and choose the interpreter inside .venv.
  4. Open the Testing view (the flask icon), click Configure Python Tests, choose pytest, then choose the tests folder.
  5. Your test appears in the tree. Click the ▶ button next to it to run it, or the debug icon to debug it with breakpoints.

📝 Note: The “Playwright Test for VS Code” extension is built for the Node.js test runner (Playwright Test). For Python projects, run and debug tests through the Python extension’s Testing view as shown above.


🗂️ Your Project So Far

🖥️ Output

playwright-python-demo/
├── .venv/                  # virtual environment (not committed)
├── tests/
│   └── test_setup.py       # your first test
└── requirements.txt        # pinned versions

❓ Frequently Asked Questions

🤔 pip says “command not found”. What should I do?

Use python -m pip install pytest-playwright. Running pip through python -m always uses the pip that belongs to the active Python.

🤔 Do I have to install all three browsers?

No. If you only test in Chromium, playwright install chromium is enough. You can add the others later.

🤔 Can I use Google Chrome or Microsoft Edge instead of Playwright’s Chromium?

Yes. Install the browser normally (or with playwright install chrome / playwright install msedge) and run tests with pytest --browser-channel chrome. You will learn this in Lesson 5.

🤔 Is pytest-playwright different from playwright?

Yes. playwright is the automation library. pytest-playwright is the plugin that connects it to pytest. Installing the plugin installs the library for you.


🏁 Summary

  • Use Python 3.10+, and give every test project its own virtual environment.
  • pip install pytest-playwright installs the plugin, Playwright and pytest together.
  • playwright install downloads the matching browsers; add --with-deps on Linux and CI.
  • Freeze versions with pip freeze > requirements.txt, and run tests from VS Code’s Testing view.

Next, you will write a complete Playwright script with sync_playwright to understand browsers, contexts and pages before pytest takes over.

🎬 Watch the Playwright tutorial series on YouTube!

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

What Is Playwright for Python? Sync API, Async API and pytest Explained

Playwright is a free, open-source browser automation library from Microsoft. Its Python package lets you drive real Chromium, Firefox and WebKit browsers from Python code, and the pytest-playwright plugin turns it into a complete test framework. This first lesson explains what Playwright for Python is, how it works under the hood, and which of its two APIs you will use in this course.

📖 What You’ll Learn: what Playwright for Python is, the browsers it supports, how the Python package talks to the browser, the sync API vs the async API, why tests use pytest with the pytest-playwright plugin, and how the Python version differs from Playwright with TypeScript.


🎭 What Is Playwright?

Playwright is a library for automating web browsers. With a few lines of code you can open a page, click buttons, type into fields, read text and check that the page behaves the way users expect. Testers use it for end-to-end (E2E) UI testing, API testing, and browser tasks like taking screenshots or saving PDFs.

Microsoft maintains Playwright and publishes it for four languages: JavaScript/TypeScript, Python, Java and .NET. All four share the same browser engine and the same concepts (pages, locators, auto-waiting, assertions), so what you learn here carries over to the other languages.

The Python package is called playwright on PyPI. At the time of writing, the current version is 1.64.1 (released October 2026), and it needs Python 3.10 or newer.


🌐 Which Browsers Does It Support?

One Python API controls all three major browser engines:

  • Chromium, the engine behind Google Chrome and Microsoft Edge. You can also run the installed branded Chrome or Edge through a *channel*.
  • Firefox (Gecko engine).
  • WebKit, the engine behind Safari. This lets you test Safari behaviour even on Windows and Linux.

Playwright works on Windows, macOS and Linux, in headless mode (no visible window, the default) or headed mode (you watch the browser).


⚙️ How Playwright for Python Works

When you run a Playwright script, three pieces work together:

  1. Your Python code calls methods such as page.goto() and locator.click().
  2. The Playwright driver, which ships inside the Python package (it includes its own Node.js runtime, so you do not install Node yourself), receives those calls.
  3. The browser is controlled by the driver over a fast, persistent connection, not over separate HTTP requests per command like classic WebDriver.

Because of this design, Playwright can listen to browser events (network requests, dialogs, console messages, new tabs) as they happen and wait for elements intelligently. This is where its famous auto-waiting comes from: before clicking, Playwright checks that the element is attached, visible, stable, enabled and able to receive the click.

📝 Note: The browsers are separate downloads. After installing the Python package you run playwright install once to download the browser builds that match your Playwright version. You will do this in the next lesson.


🔀 Sync API vs Async API

The Python package offers two complete, equivalent APIs. They have the same methods; the difference is how you call them.

The sync API

The sync API (playwright.sync_api) reads like normal Python: each line runs and finishes before the next one starts. Save this as sync_title.py:

📄 sync_title.py

from playwright.sync_api import sync_playwright


with sync_playwright() as p:                     # 1. start Playwright
    browser = p.chromium.launch()                # 2. launch headless Chromium
    page = browser.new_page()                    # 3. open a new tab

    page.goto("https://sqatools.in/automation-practice-page/")   # 4. navigate
    print("Title:", page.title())                # 5. read the page title

    browser.close()                              # 6. close the browser

Line by line:

  1. sync_playwright() starts the Playwright driver. Using it in a with block makes sure it stops cleanly when the block ends, even if an error happens.
  2. p.chromium.launch() starts a Chromium browser. Swap chromium for firefox or webkit to change browser.
  3. browser.new_page() opens a fresh tab and returns a Page object, the object you use for almost everything.
  4. page.goto(url) loads the URL and waits for the page’s load event.
  5. page.title() returns the text of the <title> tag.
  6. browser.close() closes the browser and all its tabs.

Run it with python sync_title.py:

🖥️ Output

Title: Automation Practice Page - SQA Tools

The async API

The async API (playwright.async_api) is built on Python’s asyncio. Every Playwright call is awaited. Save this as async_title.py:

📄 async_title.py

import asyncio

from playwright.async_api import async_playwright


async def main():
    async with async_playwright() as p:          # start Playwright (async version)
        browser = await p.chromium.launch()
        page = await browser.new_page()

        await page.goto("https://sqatools.in/automation-practice-page/")
        print("Title:", await page.title())

        await browser.close()


asyncio.run(main())                              # run the async function

What changed compared to the sync version:

  • The code lives inside an async def function, and asyncio.run(main()) runs it.
  • with becomes async with, and every Playwright call gets await in front of it.
  • The result is the same: Title: Automation Practice Page - SQA Tools.

Which one should you learn?

Sync APIAsync API
Importplaywright.sync_apiplaywright.async_api
StylePlain Python, no awaitasync def + await everywhere
Best forTest automation with pytest, scriptsApps already built on asyncio, running many pages concurrently
Used in this courseModules 1 to 12Module 13

For UI testing, the sync API is the standard choice. The pytest-playwright plugin is built on it, the official docs show it first, and the code is easier to read. You will learn the async API later, once the core skills are solid.

⚠️ Watch out: Do not mix the two APIs in one script. Calling the sync API inside a running asyncio event loop raises an error that tells you to use the async API instead.


🧪 Playwright + pytest: The Test Framework

The playwright package is a library, not a test runner. It does not discover tests, produce pass/fail results or create reports by itself. In Python, that job belongs to pytest, the most popular Python testing framework.

Microsoft publishes an official pytest plugin, pytest-playwright (current version 0.10.0). Installing it gives you:

  • Ready-made fixtures such as page, context and browser. Each test receives a fresh, isolated page without writing any launch or close code.
  • Command-line options like --browser firefox, --headed, --slowmo, --tracing, --video and --screenshot.
  • Web-first assertions through expect(), which wait and retry until the condition is true.

Here is a preview of what a test looks like. You will write and run it yourself in Lesson 4:

📄 test_preview.py

from playwright.sync_api import Page, expect


def test_practice_page_title(page: Page):
    page.goto("https://sqatools.in/automation-practice-page/")
    expect(page).to_have_title("Automation Practice Page - SQA Tools")

Notice what is missing: no sync_playwright(), no launch(), no close(). The page fixture handles all of that.


🆚 Playwright Python vs Playwright TypeScript

If you have seen our Playwright TypeScript tutorial, the ideas are identical but some names and tools differ:

TopicTypeScriptPython
Test runnerPlaywright Test (npx playwright test)pytest with pytest-playwright (pytest)
Configplaywright.config.tspytest.ini / pyproject.toml + conftest.py fixtures
Method namescamelCase: getByRole, toHaveTextsnake_case: get_by_role, to_have_text
Asyncawait on every callNot needed with the sync API
Retries and parallel runsBuilt inPlugins: pytest-rerunfailures, pytest-xdist
Screenshot comparisonBuilt-in toHaveScreenshotNo built-in matcher (covered in Module 5)
AI test agents (planner, generator, healer)AvailableNot available for Python yet

✅ Why Testers Choose Playwright for Python

  • Auto-waiting removes most time.sleep() calls and flaky waits.
  • One API for three engines, including WebKit on any operating system.
  • Isolated browser contexts give every test a fresh, cookie-free session in milliseconds.
  • Powerful tooling: Codegen records tests for you, the Inspector debugs them, and the Trace Viewer replays failures step by step.
  • Network control to mock APIs, block resources or record traffic.
  • Python’s ecosystem: pytest, data libraries, and easy reading of JSON, CSV and Excel test data.

❓ Frequently Asked Questions

🤔 Is Playwright for Python free?

Yes. Playwright and pytest-playwright are open source under the Apache 2.0 license.

🤔 Do I need Node.js to use Playwright with Python?

No. The Python package bundles the driver and the runtime it needs. You only install Python packages and then the browsers with playwright install.

🤔 Is Playwright better than Selenium for Python?

Both are good tools. Playwright gives you auto-waiting, browser contexts, network mocking and tracing out of the box, while Selenium has a longer history and wider grid support. Read Playwright vs Selenium vs Cypress for a detailed comparison.

🤔 Should I learn the sync or async API first?

Start with the sync API. It is what pytest-playwright uses and what you will use for day-to-day testing. The async API uses the same methods with await, so switching later is easy.


🏁 Summary

  • Playwright for Python automates Chromium, Firefox and WebKit from Python 3.10+.
  • The package bundles its own driver; browsers are installed separately with playwright install.
  • It has a sync API (used for testing in this course) and an equivalent async API.
  • pytest plus the official pytest-playwright plugin provides fixtures, CLI options and the test runner.

Next, you will set up Python, a virtual environment, Playwright, pytest and VS Code on your machine.

🎬 Watch the Playwright tutorial series on YouTube!

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

Python Polymorphism

Polymorphism means same function name, different behaviors, depending on class or input.

Two types in Python:

TypeMeaning
Compile-time (Overloading)Same function, different parameters (simulated in Python)
Runtime (Overriding)Child class modifies parent method

Each object responds differently to the same speak() call.


Python doesn’t support true overloading, so we use default parameters.


Same function → different behavior based on object type.


Child classes provide unique logic for inherited structure.


Explanation

  • Admin & Customer override the login() method.
  • Same method name → different behaviors based on object type.

FeatureWhat It DoesExample
InheritanceReuse parent class featuresclass B(A)
Multiple InheritanceChild from multiple parentsclass C(A,B)
PolymorphismSame method, different behaviorOverriding
OverridingModify parent methodRedefine in child
OverloadingSame name, different argumentsDefault parameters

Python Inheritance

Inheritance allows a class (child) to acquire properties and methods of another class (parent).

This promotes:

  • Code reusability
  • Hierarchy / structure
  • Cleaner and scalable programs

Explanation

  • Animal is the parent/base class.
  • Dog is the child/derived class.
  • Dog gets the speak() method without writing it again.

📝 Child gets access to both:
➡ show() (from Parent)
➡ display() (its own)


📝 Chain of inheritance: A → B → C
Class C inherits everything.


📝 Child receives methods of both parents.


📝 One parent → Multiple children.


📝 super() is used to call parent methods/constructors.

Python Generators

Python Tutorial › Functions › Generators

Python Generators

IntermediateLesson 29 of 4710 min read8 runnable examples

A generator is a function that produces a sequence of values lazily, one at a time, using yield. Generators make iterators easy to write and let you process huge or endless data without loading it all into memory.

In this lesson
  • Write generator functions with yield
  • Understand how a generator pauses and resumes
  • Use generator expressions
  • Chain generators into pipelines

Your first generator

A function that contains yield is a generator function. Calling it does not run the body; it returns a generator object. Each next() runs the body until the next yield.

Python
def countdown(n):    while n > 0:        yield n        n -= 1 gen = countdown(3)print(gen)print(next(gen), next(gen), next(gen))print(list(countdown(5)))
Output
<generator object countdown at 0x000001D0A798A800>
3 2 1
[5, 4, 3, 2, 1]

Compare with the iterator class in the Iterators lesson: the same behaviour in four lines.

yield pauses, it does not end

return ends a function. yield hands out a value and pauses, keeping all local variables, until the next request.

Python
def steps():    print("start")    yield 1    print("after first yield")    yield 2    print("finished") for value in steps():    print("got", value)
Output
start
got 1
after first yield
got 2
finished

Why generators save memory

A list holds every item at once. A generator holds only its current state.

Python
import sys numbers_list = [n * n for n in range(1_000_000)]numbers_gen = (n * n for n in range(1_000_000)) print(sys.getsizeof(numbers_list) > 1_000_000)print(sys.getsizeof(numbers_gen) < 1_000)print(sum(numbers_gen))
Output
True
True
333332833333500000
Note

The exact byte sizes depend on the Python version, which is why the example prints comparisons. The list needs megabytes; the generator needs a few hundred bytes whatever the range.

Generator expressions

Round brackets around a comprehension make a generator expression. When it is the only argument to a function, you can drop the extra brackets:

Python
words = ["python", "generators", "save", "memory"]print(max(len(w) for w in words))print(any(w.startswith("g") for w in words))lengths = (len(w) for w in words)print(next(lengths), list(lengths))
Output
10
True
6 [10, 4, 6]

Infinite generators

Because values are produced on demand, a generator can run forever. Take only what you need:

Python
def fibonacci():    a, b = 0, 1    while True:        yield a        a, b = b, a + b fib = fibonacci()print([next(fib) for _ in range(10)])
Output
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]

Generator pipelines

Chain generators so each one processes the output of the previous one. Only one item flows through the pipeline at a time, which is how large log files are processed efficiently.

Python
log_lines = [    "INFO start",    "ERROR db timeout",    "INFO request ok",    "ERROR disk full",] def errors(lines):    for line in lines:        if line.startswith("ERROR"):            yield line def messages(lines):    for line in lines:        yield line.split(" ", 1)[1] for msg in messages(errors(log_lines)):    print(msg)
Output
db timeout
disk full

yield from

yield from hands out every item of another iterable, which is perfect for recursive generators:

Python
def flatten(items):    for item in items:        if isinstance(item, list):            yield from flatten(item)        else:            yield item print(list(flatten([1, [2, [3, 4]], 5])))
Output
[1, 2, 3, 4, 5]

Generators are single-use

Python
gen = (x for x in range(3))print(list(gen))print(list(gen))
Output
[0, 1, 2]
[]

Call the generator function again (or rebuild the expression) when you need the values a second time.

Check your understanding

Predict the output

def g():    for i in range(3):        yield i * 10    yield "done" it = g()print(next(it), next(it), list(it))

Quick quiz

1. What does calling a generator function return?

2. What happens to local variables at a yield?

3. Which creates a generator expression?

Exercise: read in chunks

Write a generator chunks(items, size) that yields lists of size items, with a shorter final chunk if needed.

Input
chunks(list(range(1, 11)), 4)
Expected output
[1, 2, 3, 4]
[5, 6, 7, 8]
[9, 10]
Hint 1

Loop with range(0, len(items), size) and yield a slice.

Program Practice

20 Python Iterator & Generator Programs

Every program has a step-by-step explanation, colour-coded code and its real output.

Start practising →
Related sets:All practice sets

Python generators FAQ

What is the difference between yield and return?

return sends back one value and ends the function. yield sends back a value and pauses, so the function can continue and yield more values later.

When should I use a generator instead of a list?

When the data is large, endless, or only needed once in order, such as reading a big file line by line or streaming API results.

Can a generator be reused?

No. Once exhausted it stays empty. Call the generator function again to get a fresh one.

Python Decorators

Python Tutorial › Functions › Decorators

Python Decorators

AdvancedLesson 30 of 4712 min read7 runnable examples

A decorator adds behaviour to a function without changing its code: logging, timing, retrying, checking permissions. You meet decorators everywhere in test frameworks, such as @pytest.fixture and @pytest.mark.parametrize. This lesson builds one from scratch.

In this lesson
  • Why decorators work: functions are objects
  • Write a wrapper and apply it with @
  • Keep the function’s name with functools.wraps
  • Write decorators that take arguments

Functions are objects

A decorator relies on two facts: a function can be passed to another function, and a function can create and return a new function.

Python
def shout(text):    return text.upper() def call_twice(func, value):    return func(value), func(value) print(call_twice(shout, "hi"))
Output
('HI', 'HI')

A decorator by hand

A decorator is a function that takes a function and returns a new function, usually a wrapper that runs extra code around the original:

Python
def log_calls(func):    def wrapper(*args, **kwargs):        print(f"calling {func.__name__} with {args}")        result = func(*args, **kwargs)        print(f"{func.__name__} returned {result}")        return result    return wrapper def add(a, b):    return a + b add = log_calls(add)      # replace add with the wrapped versionadd(2, 3)
Output
calling add with (2, 3)
add returned 5

The @ syntax

@log_calls above a definition is a shortcut for add = log_calls(add):

Python
def log_calls(func):    def wrapper(*args, **kwargs):        print(f"calling {func.__name__}{args}")        return func(*args, **kwargs)    return wrapper @log_callsdef multiply(a, b):    return a * b print(multiply(4, 5))
Output
calling multiply(4, 5)
20
Note

The wrapper uses *args, **kwargs so it can pass along whatever arguments the decorated function takes.

Keep the original name with functools.wraps

Without help, the decorated function reports the wrapper’s name and loses its docstring. functools.wraps copies them over:

Python
from functools import wraps def plain(func):    def wrapper(*args, **kwargs):        return func(*args, **kwargs)    return wrapper def proper(func):    @wraps(func)    def wrapper(*args, **kwargs):        return func(*args, **kwargs)    return wrapper @plaindef first():    """First docstring.""" @properdef second():    """Second docstring.""" print(first.__name__, first.__doc__)print(second.__name__, second.__doc__)
Output
wrapper None
second Second docstring.

Always use @wraps in your decorators.

A timing decorator

Python
import timefrom functools import wraps def timer(func):    @wraps(func)    def wrapper(*args, **kwargs):        start = time.perf_counter()        result = func(*args, **kwargs)        elapsed = time.perf_counter() - start        print(f"{func.__name__} took {'under' if elapsed < 0.5 else 'over'} 0.5 s")        return result    return wrapper @timerdef slow_sum(n):    time.sleep(0.1)    return sum(range(n)) print(slow_sum(1000))
Output
slow_sum took under 0.5 s
499500

Decorators with arguments

To pass settings such as @retry(times=3), add one more outer function. retry(times=3) runs first and returns the actual decorator.

Python
from functools import wraps def retry(times):    def decorator(func):        @wraps(func)        def wrapper(*args, **kwargs):            for attempt in range(1, times + 1):                try:                    return func(*args, **kwargs)                except ValueError as e:                    print(f"attempt {attempt} failed: {e}")            raise RuntimeError(f"{func.__name__} failed {times} times")        return wrapper    return decorator calls = {"n": 0} @retry(times=3)def flaky():    calls["n"] += 1    if calls["n"] < 3:        raise ValueError("server busy")    return "success" print(flaky())
Output
attempt 1 failed: server busy
attempt 2 failed: server busy
success

Stacking decorators

Several decorators apply from the bottom up: the one closest to the function wraps it first.

Python
def bold(func):    return lambda: "<b>" + func() + "</b>" def italic(func):    return lambda: "<i>" + func() + "</i>" @bold@italicdef label():    return "Python" print(label())
Output
<b><i>Python</i></b>

Built-in decorators you will meet

DecoratorWhat it does
@staticmethod, @classmethodMethods that do not need an instance (see Classes & Objects)
@propertyCall a method like an attribute
@functools.lru_cacheRemember results of expensive calls
@dataclasses.dataclassGenerate __init__, __repr__ and more for a class
@pytest.fixture, @pytest.mark.parametrizeSet up test data and run a test with many inputs

Check your understanding

Predict the output

def twice(func):    def wrapper(x):        return func(func(x))    return wrapper @twicedef add3(x):    return x + 3 print(add3(10))

Quick quiz

1. What is @deco above def f(): equivalent to?

2. Why use functools.wraps?

3. With @a above @b above def f, which wraps f first?

Exercise: count calls

Write a decorator count_calls that counts how many times a function has been called and stores the number in wrapper.calls.

Input
call greet("Asha"), greet("Ravi"), then print greet.calls
Expected output
Hello Asha
Hello Ravi
calls: 2
Hint 1

Set wrapper.calls = 0 before returning the wrapper, and add 1 inside it.

Program Practice

Python decorators FAQ

What is a decorator in Python?

A decorator is a function that takes another function and returns a modified version of it, usually a wrapper that adds behaviour before or after the original runs.

What does functools.wraps do?

It copies the wrapped function’s name, docstring and other metadata onto the wrapper, so debugging and documentation tools see the original function.

Can a decorator take arguments?

Yes. Write a function that takes the arguments and returns the decorator, giving three levels: def retry(times): def decorator(func): def wrapper(...).