Next-Gen App & Browser
Testing Cloud
Trusted by 2 Mn+ QAs & Devs to accelerate their release cycles
Master pytest-BDD for Python and write readable, maintainable BDD tests with Gherkin & align development with user needs.
Published on: September 15, 2025
pytest-BDD improves test automation by making tests readable, maintainable, and scalable. With pytest-BDD, test scenarios are defined in plain language using Gherkin, while each step maps to a Python function executing the test logic. This approach enhances coverage, aligns tests with requirements, and accelerates feedback during development
What Is pytest-BDD Framework?
pytest-BDD is a pytest plugin that enables Behavior-Driven Development (BDD) in Python. It bridges business-readable Gherkin scenarios with executable Python tests. By writing steps in plain language and mapping them to Python functions, teams can ensure clarity, collaboration, and automation within the same framework.
Steps to Run pytest-BDD
Pro Tip: LambdaTest KaneAI
With LambdaTest KaneAI, you can author, plan, and evolve pytest-BDD scenarios in plain English. Instead of manually writing every step, KaneAI generates feature files and step definitions, helping you scale test automation faster while keeping scenarios business-readable.
Behavior-Driven Development (BDD) allows teams to align what they build with what users actually need. Instead of writing tests just using code, the team defines how the system should respond in specific situations using plain, structured, and natural language. The focus changes to the user actions and expected outcomes.
Scenarios are written using a simple format:
BDD, as implemented in frameworks like Cucumber testing, brings developers, testers, and business members to the same table. Everyone speaks the same language when scenarios are written using BDD, reducing misunderstandings and ensuring the product behaves correctly. These scenarios can also double as automated tests.
You can use And and But to expand steps when scenarios need more detail. Starting with behavior keeps teams from wasting time fixing issues that could have been spotted early. If a feature changes later, you update the wording of the scenario and the related code, maintaining flexibility and avoiding test rewrites.
BDD works naturally in Agile environments, keeping the team focused on how the software should work for the user, leading to smoother collaboration, faster issue detection, and tests that reflect real user behavior. Cucumber testing allows teams to execute these Gherkin-based scenarios as automated tests in a structured and maintainable way.
pytest-BDD is a plugin that adds behavior-driven capabilities to the pytest framework. It allows describing application behavior using plain-text scenarios while retaining the full control and flexibility of pytest.
System behavior is written in .feature files using Given, When, Then. Each statement maps to a Python function that executes the step, keeping behavior separate from test code.
pytest-BDD integrates seamlessly with pytest: you can reuse fixtures, apply plugins, and manage tests with markers without changing the existing test setup.
By using pytest-BDD, test intent becomes clearer, making scenarios easier to understand for all team members. To build a solid foundation before exploring pytest-BDD, check out this detailed pytest tutorial.
Note: Run pytest tests at scale across 3000+ browsers and OS combinations. Try LambdaTest Now!
Before writing tests using pytest-BDD, take time to set up your environment. Install the tools you will need, arrange the project structure, and add a few settings to help everything work properly. A proper setup avoids headaches later, especially as your test files increase.
pip install pytest pytest-bdd
pip install selenium
You need to structure your project in order to have a good organization and maintainability. A common layout looks like this:
Gherkin is the standard for describing how your application should behave in different situations. It is written in plain text and stored in .feature files, which translate requirements into tests that connect directly to Python code. These Gherkin test cases act as living documentation, ensuring both technical and non-technical team members understand the expected behavior.
Every Gherkin scenario follows the same structure. You begin by describing the starting point with Given. Then, you explain the user’s action using When. Finally, you write the expected outcome under Then.
Example:
Feature: Login
Scenario: Logging in with correct credentials
Given the user is on the login screen
When they enter valid login details
Then the my account dashboard should appear
For more complex scenarios, you can extend with And or But:
Feature: Login
Scenario: Logging in with correct credentials
Given the user is on the login screen
And their account is active
When they enter valid login details
Then the my account dashboard should appear
But admin options should stay hidden
Each step connects to a Python function that defines what happens when the test runs.
A clear structure in your feature files improves readability and long-term maintainability. Following these practices ensures scenarios remain easy to manage as your project grows.
With proper organization, Gherkin testing becomes more efficient, as scenarios are easier to reuse, scale, and keep consistent.
After your feature files are written, you need to link each step to real test actions in Python. This link is made through step definitions. Each line in your .feature file connects to a function that tells pytest what to do when the test runs.
Step definitions are simple functions that use decorators of the pytest-BDD library. You should use @given, @when, and @then to mark each one. The text you write inside the quotes must match the step you wrote in the Gherkin file.
For example:
# Load the feature file
scenarios("features/login.feature")
@given("the user is on the login screen")
def user_on_login_screen(driver):
driver.get("https://ecommerce-playground.lambdatest.io/index.php?route=account/login")
@when("they enter valid login details")
def enter_valid_login_details(driver):
driver.find_element(By.ID, "input-email").send_keys("lambdatestblogs@gmail.com")
driver.find_element(By.ID, "input-password").send_keys("test123@")
driver.find_element(By.CSS_SELECTOR, "input[type='submit'][value='Login']").click()
@then("the my account dashboard should appear")
def dashboard_should_appear(driver):
header = driver.find_element(By.CSS_SELECTOR, "h2.card-header.h5")
assert header.is_displayed()
assert "route=account/account" in driver.current_url
The scenarios() function loads the .feature file. Each decorator (@given, @when, @then) matches a line in the file to a specific function that runs when pytest executes that step.
Fixtures and hooks in pytest-BDD give you flexibility to manage test setup, teardown, and execution flow. They act as building blocks that keep your test code clean, reusable, and easier to maintain.
Fixtures:
Fixtures help you share common setup and teardown logic across tests. You can define them inside a conftest.py file (recommended for reuse) or directly in your test module.
Example: WebDriver fixture:
import pytest
from selenium import webdriver
@pytest.fixture
def driver():
driver = webdriver.Chrome()
driver.implicitly_wait(10)
yield driver
driver.quit()
This fixture opens the Chrome browser, sets an implicit wait of 10 seconds, and ensures the browser is closed once the test finishes.
Hooks:
Hooks are special functions that let you run custom logic at specific points during test execution. With pytest-BDD, you can hook into scenarios and steps.
Example: print scenario name before execution:
def pytest_bdd_before_scenario(request, feature, scenario):
print(f"
==> Running scenario: {scenario.name}")
Common pytest-BDD Hooks
pytest-BDD offers some useful hooks to help you customize and control the test flow:
These hooks are useful for preparing environments, capturing logs, cleaning up resources, or tracking test progress.
Running these tests on a cloud-based platform addresses the limitations of local testing. You can scale across multiple browsers, operating systems, and devices without managing your own infrastructure.
Cloud grids also enable faster execution, parallel testing, and access to real environments, helping teams catch issues sooner and deliver more reliable applications. One such platform is LambdaTest.
LambdaTest is a GenAI-native test execution platform that lets you run pytest testing online at scale across 3000+ browsers and OS combinations.
You can execute both manual and automated tests using the LambdaTest Selenium Grid, accelerating your software development while ensuring consistent coverage across multiple environments.
To get started with it, let's take a simple test scenario to understand how pytest-BDD with Selenium makes it easier to automate browser-based test scenarios while keeping them readable and maintainable.
Test Scenarios:
Test Scenario 1 - Scenario: Logging in with correct credentials
Test Scenario 2 - Scenario: Logging in with a specific username and password
Test Scenario 3 - Scenario Outline: Logging in with different credentials
Code Implementation:
Feature: Login
Scenario: Logging in with correct credentials
Given the user is on the login screen
When they enter valid login details
Then the my account dashboard should appear
Scenario Outline: Logging in with different credentials
Given the user is on the login screen
When the user logs in with "<username>" and "<password>"
Then my account dashboard should appear
Examples:
| username | password |
| lambdatestblogs@gmail.com | test123@ |
| lambdatestblogs2@gmail.com | 123456@test |
Scenario: Logging in with a specific username and password
Given the user is on the login screen
When the user logs in with "lambdatestblogs@gmail.com" and "test123@"
Then my account dashboard should appear
Code Walkthrough for features/login.feature:
Code Implementation:
import pytest
from selenium import webdriver
from selenium.webdriver.common.by import By
from pytest_bdd import scenarios, given, when, then, parsers
scenarios("../features/login.feature")
@pytest.fixture
def driver():
gridUrl = "hub.lambdatest.com/wd/hub"
username = "your-username"
accessKey = "your-access-key"
lt_options = {
"user": username,
"accessKey": accessKey,
"build": "Python BDD Build",
"name": "Test Case X",
"platformName": "Windows 11",
"w3c": True,
"browserName": "Chrome",
"browserVersion": "latest",
"selenium_version": "latest"
}
web_driver = webdriver.ChromeOptions()
options = web_driver
options.set_capability('LT:Options', lt_options)
url = f"https://{username}:{accessKey}@{gridUrl}"
driver = webdriver.Remote(
command_executor=url,
options=options
)
driver.maximize_window()
driver.implicitly_wait(10)
yield driver
driver.quit
@given("the user is on the login screen")
def user_on_login_screen(driver):
driver.get("https://ecommerce-playground.lambdatest.io/index.php?route=account/login")
@when("they enter valid login details")
def enter_valid_login_details(driver):
driver.find_element(By.ID, "input-email").send_keys("lambdatestblogs@gmail.com")
driver.find_element(By.ID, "input-password").send_keys("test123@")
driver.find_element(By.CSS_SELECTOR, "input[type='submit'][value='Login']").click()
@then("the my account dashboard should appear")
def dashboard_should_appear(driver):
header = driver.find_element(By.CSS_SELECTOR, "h2.card-header.h5")
assert header.is_displayed()
assert "route=account/account" in driver.current_url
@when(parsers.parse('the user logs in with "{username}" and "{password}"'))
def do_login(driver, username, password):
driver.find_element(By.ID, "input-email").send_keys(username)
driver.find_element(By.ID, "input-password").send_keys(password)
driver.find_element(By.CSS_SELECTOR, "input[type='submit'][value='Login']").click()
Code Walkthrough for test/test_login.py:
After setting up your feature files and linking them to step definition functions, you are ready to execute your tests. Running tests with pytest-BDD feels the same as using pytest commands.
Since it runs on top of pytest, you still get all the built-in options like filtering, markers, and custom output. For more details, check this guide on Selenium testing using Gherkin.
Running All Tests
To run your full set of tests, just open a terminal and run the command below:
pytest
pytest will run each scenario it detects. Every scenario is handled like a separate test and will show up in the terminal output.
Running a Specific File
If you want to run the tests of a specific file, run the command:
pytest tests/test_login.py
You’ll see only the results related to the scenarios and steps inside that file.
After executing the tests, you can see the results below.
Output:
LambdaTest Result:
To get started, refer to this guide on Selenium Cucumber testing on LambdaTest.
After setting up and running your pytest-BDD tests on LambdaTest, you can further simplify your test automation workflow using LambdaTest KaneAI.
KaneAI allows you to create and execute BDD-style test scenarios directly from plain English. Instead of manually writing step definitions, KaneAI interprets natural language into Gherkin test cases and generates executable automation code.
This approach accelerates test creation, reduces maintenance effort, and complements your existing Selenium + pytest-BDD workflow by letting you scale tests even faster across multiple browsers and devices.
To explore this, check out the support documentation on getting started guide on KaneAI.
When you need to test the same behavior with different input data, you don’t have to duplicate scenarios. A Scenario Outline lets you define the logic once and run it multiple times with different values.
In a .feature file, you declare a Scenario Outline with placeholders (inside < >) such as <username> or <password> . You then provide an Examples table with the values to substitute.
Example:
Scenario Outline: Logging in with different credentials
Given the user is on the login screen
When the user logs in with "<username>" and "<password>"
Then the my account dashboard should appear
Examples:
| username | password |
| lambdatestblogs@gmail.com | test123@ |
| lambdatestblogs2@gmail.com | 123456@test |
This generates two separate test cases:
Both should pass if the credentials are valid.
In your step definitions, use parsers.parse to capture dynamic values:
@when(parsers.parse('the user logs in with "{username}" and "{password}"'))
def do_login(driver, username, password):
driver.find_element(By.ID, "input-email").send_keys(username)
driver.find_element(By.ID, "input-password").send_keys(password)
driver.find_element(By.CSS_SELECTOR, "input[type='submit'][value='Login']").click()
Here, username and password are passed automatically from the Examples table.
Scenario Outline in pytest-BDD makes your tests more efficient and maintainable. Instead of writing multiple scenarios for different inputs, you define the logic once and run it across multiple data sets.
Parameterization in pytest-BDD allows you to reuse the same step functions with different input values, even outside full Scenario Outlines.
This reduces redundancy, keeps test code clean, and makes it more adaptable. It’s ideal for small variations or single-step changes rather than entire scenarios.
Instead of writing separate step functions for similar actions, define a step with placeholders using angle brackets. These placeholders are passed as arguments to the Python function when the test runs.
Example from a .feature file:
When the user logs in with "your-username" and "your-password"
Here, "your-username" and "your-password" are dynamic and can be reused across multiple scenarios.
In your test file, define the step function to accept the parameters:
@when(parsers.parse('the user logs in with "{username}" and "{password}"'))
def do_login(driver, username, password):
driver.find_element(By.ID, "input-email").send_keys(username)
driver.find_element(By.ID, "input-password").send_keys(password)
driver.find_element(By.CSS_SELECTOR, "input[type='submit'][value='Login']").click()
Key Points of Parameterization in pytest-BDD
Parameterization in pytest-BDD lets you run the same step functions with different input values, reducing repetition and keeping your test code clean. It’s particularly useful for small variations or dynamic inputs without rewriting entire scenarios.
Parameterization helps you test smarter by focusing on what changes, not rewriting what stays the same. It’s a simple way to get more coverage with less duplication.
It is always important to follow a few best practices that will help you keep the tests readable, scalable, and easy to manage. It also improves team collaboration and reduces the time spent on maintainability.
Using clear and consistent naming in your pytest-BDD project helps everyone understand what is being tested and reduces confusion. Proper names improve readability, maintainability, and collaboration across the team.
Scenario: User logs in with valid credentials
@when('the user adds an item to the cart')
def add_item_to_cart():
pass
Organize your project into clear sections. A common pattern looks like this:
Keep related tests and feature files together to make navigation easier for the entire team.
Reusing code effectively in pytest-BDD helps reduce duplication, keeps tests maintainable, and ensures step definitions remain consistent across the project. Leveraging parameters and fixtures makes your tests more flexible and easier to update.
@when('the user logs in with "<username>" and "<password>"')
def login(username, password):
pass
Collaboration between QA, developers, and business stakeholders ensures that scenarios are accurate, understandable, and aligned with requirements. Early teamwork reduces rework and improves overall test quality.
pytest-BDD is more than just a framework for automating scenarios, it bridges the gap between technical and non-technical team members. Developers, testers, and business stakeholders work from the same natural specifications, ensuring everyone understands the desired system behavior.
By using structured, human-readable Gherkin syntax, teams reduce confusion during development. Each Gherkin step connects directly to Python functions, allowing easy automation while keeping documentation and tests in sync. When business rules change, scenarios can be updated immediately.
pytest-BDD helps maintain a test suite that is readable, easy to update, and powerful enough to validate complex systems, delivering automation benefits without losing team alignment.
Did you find this page helpful?