Understanding Pytest BDD

Explore pytest BDD and learn how to implement behavior-driven development in Python for cleaner, more organized test automation.

Written by Vinayak Mirani Vinayak Mirani
Reviewed by Bhumika Babbar Bhumika Babbar
Last updated: 6 August 2026 12 min read

Key Takeaways

  • Pytest-BDD combines the readability of BDD with the flexibility of Pytest. Feature files describe user behaviour in plain language, while step definitions and fixtures connect those scenarios to executable Python tests.
  • Well-structured BDD tests are easier to maintain and scale. Reusable step definitions, shared fixtures, clear project organization, and focused scenarios help keep automation reliable as applications evolve.
  • The biggest value comes from keeping requirements and tests aligned. Treating feature files as living documentation allows developers, testers, and product teams to collaborate around the same expected behaviour, reducing misunderstandings and improving release confidence.

The entire BDD framework relies on you writing scenarios in plain language and then connecting them to tests. The trouble starts when feature files become disconnected from the code or teams spend more time maintaining step definitions than discussing product behavior.

Pytest-BDD is a Python-based testing framework that integrates Behavior-Driven Development (BDD) into the Pytest ecosystem. It allows testers to write test scenarios in Gherkin syntax (Given-When-Then) and map them to Python functions.

In this guide, you’ll learn how Pytest-BDD works, how to set it up, write feature files and step definitions, run tests, and avoid the maintenance problems that often make BDD suites difficult to scale.

What is Behavior Driven Development (BDD)?

Behavior-Driven Development (BDD) helps you describe how an application should behave before you start thinking about test implementation. Instead of writing tests directly in code, you define expected user behavior in plain language so developers, testers, and business stakeholders can all contribute to the same conversation.

These requirements are usually written as Gherkin scenarios using the familiar Given–When–Then format. Because the scenarios are easy to read, they can serve as both product documentation and automated tests once they’re connected to step definitions.

What I’ve found most useful is that BDD keeps everyone focused on user outcomes instead of implementation details. Rather than discussing methods or APIs, teams talk about what the user is trying to achieve, making requirements easier to understand, review, and automate.

Why Teams Choose Pytest-BDD

I’ve found that Pytest-BDD is most valuable when teams want their requirements and automated tests to stay in sync. Instead of maintaining separate user stories, documentation, and test cases, the same Gherkin scenarios become a shared reference for product discussions and test automation.

Some of the biggest advantages include:

  • Keeps requirements aligned with implementation: Feature files describe expected behavior in a format that both technical and non-technical teams can understand, reducing misunderstandings before development begins.
  • Improves release confidence: Every automated scenario confirms that important user journeys continue to work as new features and code changes are introduced.
  • Reduces duplicate documentation: Feature files act as both living documentation and executable tests, so teams spend less time updating separate specifications and test cases.
  • Fits naturally into existing Pytest projects: If your team already uses Pytest, you can introduce BDD without rebuilding your automation framework or changing your development workflow.
  • Makes large test suites easier to maintain: Reusable fixtures, shared step definitions, and the broader Pytest ecosystem help keep automation organized as the application grows.

Using Pytest Fixtures to Reuse Test Setup

As your test suite grows, you’ll quickly notice the same setup code appearing in multiple tests. Creating test data, initializing objects, or connecting to a database repeatedly makes tests harder to maintain. Pytest fixtures solve this by letting you define the setup once and reuse it wherever it’s needed.

One of the reasons I like fixtures is that they keep test cases focused on the behavior being tested instead of the preparation required to run them. You can also control how often a fixture runs using scopes such as function, class, module, or session, which helps reduce unnecessary setup and speeds up larger test suites.

import pytest



@pytest.fixture(scope="module")

def sample_data():

    return {

        "name": "Alice",

        "age": 30

    }



def test_sample_1(sample_data):

    assert sample_data["name"] == "Alice"



def test_sample_2(sample_data):

    assert sample_data["age"] == 30

Here, the sample_data fixture is created only once for the entire module. Both tests receive the same data without recreating it each time, making the suite more efficient and keeping the setup logic in one place.

When you’re using Pytest-BDD, fixtures become even more useful because the same setup can be shared across multiple Gherkin scenarios. Instead of repeating login steps, test users, or configuration data in every scenario, you define them once as fixtures and reuse them throughout your feature files.

Setting Up Your Environment

Getting started with Pytest-BDD doesn’t take much if you already use Pytest. I normally set up a clean virtual environment first so project dependencies stay isolated, then install only the packages needed for the test framework before adding browser or API libraries later.

1. Install Python

Make sure Python is installed and available from your terminal.

python --version

2. Create a Virtual Environment

Using a virtual environment keeps your project’s dependencies separate from other Python installations.

python -m venv venv

Activate it:

Linux/macOS

source venv/bin/activate

Windows

.\venv\Scripts\activate

3. Install Pytest and Pytest-BDD

Install the testing framework together with the BDD plugin.

pip install pytest pytest-bdd

4. Add Project-Specific Libraries

Install any additional packages your tests depend on. For example, if you’re automating browser interactions:

pip install selenium

Similarly, you might install libraries such as requests for API testing or playwright for browser automation, depending on your project.

5. Organize Your Project

I like keeping feature files separate from Python test code because the structure remains easy to navigate as the project grows.

project/

├── features/

│   └── login.feature

├── tests/

│   └── test_login.py

├── conftest.py

└── requirements.txt

With this setup in place, you’re ready to start writing Gherkin feature files, connect them to step definitions, and execute your first Pytest-BDD tests.

Writing Your First Pytest-BDD Test

A Pytest-BDD test has three parts: a feature file that describes the expected behavior, step definitions that connect each step to Python code, and the test execution that runs everything together. Once you understand this flow, adding new scenarios becomes much easier.

Step 1: Write a Feature File

I always start with the feature file because it captures the expected behavior before writing any automation. These files use Gherkin syntax, making them easy for developers, testers, and product owners to read.

A feature file typically contains:

  • Feature: The functionality you’re testing.
  • Scenario: A specific user journey or business case.
  • Given: The starting condition.
  • When: The action performed.
  • Then: The expected outcome.

For example:

Feature: Login functionality

 Scenario: Successful login

   Given the user is on the login page

   When the user enters valid credentials

   Then the user should be logged in

This scenario describes the behaviour without exposing any implementation details. The automation comes next.

Step 2: Connect the Scenario to Python

Each Gherkin step is linked to a Python function using decorators such as @given, @when, and @then. This is where the actual automation happens.

import pytest

from selenium import webdriver

from selenium.webdriver.common.by import By

from pytest_bdd import scenarios, given, when, then

scenarios('../features\login.feature')

@pytest.fixture

def driver():

   browser = webdriver.Chrome()

   yield browser

   browser.quit()

@given('the user is on the login page')

def navigate_to_login(driver):

   driver.get("https://bstackdemo.com/signin")

@when('the user enters valid credentials')

def enter_credentials(driver):

   #select username

   driver.find_element(by=By.ID, value="username").click()

   driver.find_element(by=By.CSS_SELECTOR,value="#react-select-2-option-0-0").click()

   #select password

   driver.find_element(by=By.ID,value="password").click()

   driver.find_element(by=By.CSS_SELECTOR, value="#react-select-3-option-0-0").click()

   #click submit

   driver.find_element(By.ID,"login-btn").click()

@then('the user should be logged in')

def verify_login(driver):

   assert driver.title=="StackDemo"

One thing I like about this approach is that the feature file stays readable while the Python code handles the implementation. If the login process changes, you usually update the step definition rather than rewriting the scenario itself.

Step 3: Run the Test

Once the feature file and step definitions are in place, running the test is no different from running any other Pytest suite.

Pytest

Pytest automatically discovers the feature file, matches each Gherkin step with its corresponding Python function, and executes the scenario. If a step fails, the output clearly shows which part of the user journey broke, making failures much easier to understand than traditional test logs.

This separation between requirements (feature files) and implementation (step definitions) is what makes Pytest-BDD easier to maintain as the number of scenarios grows.

How to Use Tags in Pytest?

Tags are useful when you want to selectively run certain tests based on their labels. You can assign tags to scenarios in feature files and use pytest to run tests associated with specific tags.

Example of using tags in a feature file:

Feature: Login functionality

 @smoke

 Scenario: Successful login

   Given the user is on the login page

   When the user enters valid credentials

   Then the user should be logged in

To run tests with a specific tag, use the -m flag with the tag name:

pytest -m smoke

This command will only execute scenarios marked with the @smoke tag, allowing you to organize your tests into different categories such as smoke, regression, or feature-based testing.

How to Use Scenario Outlines and Parameterization

Pytest-BDD allows you to efficiently test multiple input-output combinations using scenario outlines. This is done by defining variables in the Examples table within the .feature file and referencing them in your steps.

Feature File Example (login.feature):

gherkin

Scenario Outline: Successful login with valid credentials

  Given the user navigates to the login page

  When the user logs in with username "<username>" and password "<password>"

  Then the login should be successful



Examples:

  | username | password |

  | user1    | pass123  |

  | user2    | pass456  |

Step Definition Example (test_login.py):

python

@when(parsers.parse('the user logs in with username "{username}" and password "{password}"'))

def login_user(username, password):

    # Perform login logic here

    assert username.startswith('user')
This structure avoids code duplication and helps you test multiple cases with one reusable scenario.

How to Use Tags and Selective Test Execution

Tags in Pytest-BDD allow you to label scenarios or features for selective execution. This is especially useful for running subsets of tests (e.g., smoke, regression).

Feature File Example:

gherkin

@smoke

Scenario: Check homepage title

  Given the user opens the homepage

  Then the title should be "Welcome"

To run only the @smoke tagged tests, use:

bash

pytest --gherkin-terminal-reporter -m smoke

To run all except a certain tag:

bash

pytest -m "not regression"

Make sure your pytest.ini includes:

ini

[pytest]

markers =

    smoke: smoke tests

    regression: regression tests

How to Use Hooks and Fixtures

Pytest-BDD supports Pytest hooks and fixtures for powerful test setup and teardown management. You can define shared resources, authentication, or data cleanup logic using fixtures.

Example: Defining a fixture for setup:

python

import pytest




@pytest.fixture

def browser():

    # Setup code here (e.g., launching browser)

    yield "browser_instance"

    # Teardown code here (e.g., closing browser)

Using a BDD hook:

python

from pytest_bdd import hooks




@hooks.hookimpl

def pytest_bdd_before_scenario(request, feature, scenario):

    print(f"\n[Setup] Starting scenario: {scenario.name}")

Combining with Step Definition:

python

@given("the user is logged in")

def user_logged_in(browser):

    # Use the browser fixture to log in

    assert browser == "browser_instance"

Hooks give you fine-grained control over the test lifecycle, while fixtures help keep the code modular and maintainable.

Writing Maintainable BDD Tests

As your BDD suite grows, maintaining it often becomes harder than writing the initial scenarios. I’ve found that keeping feature files concise, reusing common steps, and organizing the project well makes a much bigger difference than adding more automation.

Keep these practices in mind:

  • Write scenarios for people first: Use simple Gherkin statements that describe user behaviour rather than implementation details. If a product owner can’t understand the scenario, it’s probably too technical.
  • Reuse step definitions: Avoid creating multiple step definitions that perform the same action. Shared steps reduce duplication and make updates much easier when the application changes.
  • Organize tests with tags: Use tags such as @smoke, @regression, or @login to group related scenarios and run only the tests you need during development or CI/CD.
  • Move setup into fixtures: Store common setup tasks, test data, and reusable objects in Pytest fixtures instead of repeating them across multiple step definitions.
  • Separate requirements from implementation: Keep feature files focused on user behaviour and place the automation logic in Python step definition files. This keeps both easier to read and maintain.
  • Keep scenarios focused: Each scenario should test one business behaviour. Trying to cover multiple workflows in a single scenario makes failures harder to diagnose.
  • Keep execution fast: Remove unnecessary UI interactions, reuse fixtures where appropriate, and run tests in parallel when possible. Fast feedback makes BDD much more practical in CI/CD pipelines.

Conclusion

I’ve found that Pytest-BDD works best when teams want to keep product requirements and automated tests closely aligned without giving up the flexibility of Pytest. Feature files make user behaviour easy to discuss, while step definitions and fixtures keep the automation organized as the test suite grows.

Like any testing approach, success depends on how well it’s maintained rather than the framework itself. Keeping scenarios readable, reusing common steps, and treating feature files as living documentation helps teams build BDD suites that remain valuable long after the first release.

Version History

  1. Aug 05, 2026 Current Version

    Improved the guide with practical workflows, best practices for maintainable BDD tests, and real-world examples to simplify adoption and long-term maintenance.

    Bhumika Babbar
    Reviewed by Bhumika Babbar Principal Engineer
Tags
Automation Testing Website Testing
Vinayak Mirani
Vinayak Mirani

Lead Solution Engineer

Vinayak is a software engineer who has 5+ years working closely with customers on real engineering problems. He brings hands-on experience in diagnosing how software behaves across different environments and what it takes to fix it right.

Scale Your pytest BDD Tests with BrowserStack
Run pytest BDD tests on real devices and browsers with BrowserStack Automate. Achieve scalable testing with powerful cloud infrastructure.