Getting Started with Behave: BDD for Python

Getting Started with Behave: BDD for Python

Behave is the most widely used BDD framework for Python. It lets you write tests in plain English using Gherkin syntax, then back them with Python step definitions. If your team wants living documentation that non-engineers can read and contribute to, Behave is a solid choice.

This guide covers everything you need to go from zero to a running test suite: installation, project structure, feature files, step definitions, tags, and output formats.

Installation

Behave requires Python 3.6 or later. Install it with pip:

pip install behave

That's it. No additional test runner needed — behave is the runner. For most projects you'll also want:

pip install behave[toml]   # if you prefer pyproject.toml config

Verify the install:

behave --version
# behave 1.2.6

Project Structure

Behave expects a specific directory layout:

myproject/
├── features/
│   ├── steps/
│   │   └── shopping_steps.py
│   ├── environment.py        # optional hooks file
│   └── shopping.feature
└── src/
    └── shop.py

The features/ directory is the root of your test suite. Feature files go directly in features/ (or subdirectories). Step definition files go in features/steps/. The environment.py file is optional but essential once you add hooks.

Writing Feature Files

Feature files use Gherkin — a structured natural language format. Here's a realistic example:

# features/shopping_cart.feature

Feature: Shopping cart
  As a customer
  I want to add items to my cart
  So that I can purchase them later

  Background:
    Given the store has the following products:
      | name        | price | stock |
      | Laptop      | 999   | 5     |
      | Headphones  | 79    | 20    |
      | USB Cable   | 12    | 100   |

  Scenario: Add a single item to the cart
    Given I have an empty cart
    When I add "Laptop" to the cart
    Then the cart should contain 1 item
    And the cart total should be 999

  Scenario: Add multiple items
    Given I have an empty cart
    When I add "Headphones" to the cart
    And I add "USB Cable" to the cart
    Then the cart should contain 2 items
    And the cart total should be 91

  Scenario: Cannot add out-of-stock items
    Given the product "Laptop" has 0 stock
    When I try to add "Laptop" to the cart
    Then I should see an error "Item out of stock"
    And the cart should be empty

Key Gherkin keywords:

  • Feature — describes the capability being tested
  • Background — steps that run before every scenario in the file
  • Scenario — a single test case
  • Given — precondition
  • When — action
  • Then — expected outcome
  • And / But — continuation of the previous step type

Writing Step Definitions

Step definitions connect Gherkin text to Python code. Each step is a decorated function:

# features/steps/shopping_steps.py

from behave import given, when, then
from src.shop import Cart, ProductCatalog


@given('the store has the following products')
def step_store_has_products(context):
    context.catalog = ProductCatalog()
    for row in context.table:
        context.catalog.add_product(
            name=row['name'],
            price=int(row['price']),
            stock=int(row['stock'])
        )


@given('I have an empty cart')
def step_empty_cart(context):
    context.cart = Cart(context.catalog)


@when('I add "{product_name}" to the cart')
def step_add_to_cart(context, product_name):
    context.cart.add(product_name)


@when('I try to add "{product_name}" to the cart')
def step_try_add_to_cart(context, product_name):
    try:
        context.cart.add(product_name)
        context.last_error = None
    except Exception as e:
        context.last_error = str(e)


@then('the cart should contain {count:d} item')
def step_cart_item_count_singular(context, count):
    assert len(context.cart.items) == count, \
        f"Expected {count} items, got {len(context.cart.items)}"


@then('the cart should contain {count:d} items')
def step_cart_item_count(context, count):
    assert len(context.cart.items) == count, \
        f"Expected {count} items, got {len(context.cart.items)}"


@then('the cart total should be {total:d}')
def step_cart_total(context, total):
    assert context.cart.total() == total, \
        f"Expected total {total}, got {context.cart.total()}"


@then('I should see an error "{message}"')
def step_see_error(context, message):
    assert context.last_error == message, \
        f"Expected error '{message}', got '{context.last_error}'"


@then('the cart should be empty')
def step_cart_empty(context):
    assert len(context.cart.items) == 0

The context object is Behave's shared state container. It's passed to every step function and persists for the duration of a scenario.

Step decorators accept plain strings or regex patterns. The {product_name} syntax in the string above is a named parameter — Behave extracts it and passes it as a function argument.

Running Tests

Run all tests:

behave

Run a specific feature file:

behave features/shopping_cart.feature

Run a specific scenario by line number:

behave features/shopping_cart.feature:14

Run scenarios matching a name substring:

behave --name "Add a single item"

Behave prints results in real time:

Feature: Shopping cart

  Background:   # features/shopping_cart.feature:7

  Scenario: Add a single item to the cart     # features/shopping_cart.feature:14
    Given I have an empty cart                # features/steps/shopping_steps.py:12
    When I add "Laptop" to the cart           # features/steps/shopping_steps.py:17
    Then the cart should contain 1 item       # features/steps/shopping_steps.py:28
    And the cart total should be 999          # features/steps/shopping_steps.py:35

1 feature passed, 0 failed, 0 skipped
3 scenarios passed, 0 failed, 0 skipped
12 steps passed, 0 failed, 0 skipped, 0 undefined

Using Tags

Tags let you label scenarios and run subsets of your test suite. Add them directly above Feature or Scenario:

@smoke @cart
Feature: Shopping cart

  @happy-path
  Scenario: Add a single item to the cart
    ...

  @error-handling @regression
  Scenario: Cannot add out-of-stock items
    ...

Run only scenarios with a specific tag:

behave --tags=smoke
behave --tags=happy-path
behave --tags="smoke and regression"
behave --tags="smoke or cart"
behave --tags="not error-handling"

Tag expressions support and, or, not. This is how you separate fast smoke tests from slow regression tests in CI:

# In CI: fast feedback first
behave --tags=smoke

# Nightly: full suite
behave --tags="not manual"

You can also use tags to skip known failures:

@wip
Scenario: Feature under development
  ...
behave --tags="not wip"

Output Formats

Behave ships with several formatters. Set them with --format:

# Default pretty output
behave --format pretty

# Plain text (no colors, good for log files)
behave --format plain

# Progress dots (compact, shows only pass/fail)
behave --format progress

# JSON output (for tooling)
behave --format json --outfile results.json

# JUnit XML (for CI systems like Jenkins)
behave --format json.pretty --outfile results.json
behave --junit --junit-directory reports/

Use multiple formatters at once with multiple --format and --outfile flags:

behave \
  --format pretty \
  --format json --outfile reports/results.json \
  --junit --junit-directory reports/junit/

Store your preferred settings in behave.ini so you don't repeat them:

[behave]
format   = pretty
junit    = true
junit_directory = reports/junit
outfile  = reports/results.json

Or in pyproject.toml:

[tool.behave]
format = ["pretty"]
junit = true
junit_directory = "reports/junit"

Scenario Outlines: Data-Driven Tests

When you need to run the same scenario with multiple data sets, use Scenario Outline:

Scenario Outline: Cart total with discount
  Given I have an empty cart
  And a <discount>% discount code is applied
  When I add "Laptop" to the cart
  Then the cart total should be <expected_total>

  Examples:
    | discount | expected_total |
    | 0        | 999            |
    | 10       | 899            |
    | 20       | 799            |
    | 50       | 499            |

Behave generates one scenario per row in the Examples table. Each runs independently.

Step Definition Discovery

Behave automatically imports all Python files in features/steps/. You can organize step files however you like:

features/steps/
├── common_steps.py      # shared steps across features
├── cart_steps.py        # cart-specific steps
├── checkout_steps.py    # checkout-specific steps
└── api_steps.py         # API interaction steps

There's no import needed between step files — Behave loads them all and makes every step available globally.

Undefined Steps

If a step has no matching definition, Behave marks it as undefined and prints a snippet:

Scenario: Add multiple items          # features/shopping_cart.feature:22
  Given I have an empty cart          # features/steps/shopping_steps.py:12
  When I add "Headphones" to the cart # UNDEFINED
  ...

You can implement step definitions for undefined steps with these snippets:

@when(u'I add "Headphones" to the cart')
def step_impl(context):
    raise NotImplementedError(u'STEP: When I add "Headphones" to the cart')

Use --dry-run to check for undefined steps without executing anything:

behave --dry-run

This is useful in CI to catch step definition gaps before running the full suite.

Common Configuration Options

[behave]
# Stop on first failure
stop = true

# Show all output including passing steps
show_skipped = true
show_timings = true

# Include/exclude paths
paths = features/

# Log level for captured output
log_level = INFO
log_capture = true
stdout_capture = true
stderr_capture = true

What's Next

This covers the basics. Once your first scenarios are green, the next areas to explore are:

  • Step parameters — typed parameters, regex steps, reusing steps across features
  • Hooksbefore_scenario, after_scenario, before_all for setup and teardown
  • Fixtures — Behave's built-in fixture system for resource management
  • Reporting — Allure integration, custom formatters, screenshot capture on failure

The key discipline with Behave is keeping step definitions thin. Steps should delegate to actual application code or service clients — not contain business logic themselves. That separation keeps your test suite readable and maintainable as it grows.

Read more

Start now free