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 behaveThat'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 configVerify the install:
behave --version
# behave 1.2.6Project Structure
Behave expects a specific directory layout:
myproject/
├── features/
│ ├── steps/
│ │ └── shopping_steps.py
│ ├── environment.py # optional hooks file
│ └── shopping.feature
└── src/
└── shop.pyThe 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 emptyKey Gherkin keywords:
Feature— describes the capability being testedBackground— steps that run before every scenario in the fileScenario— a single test caseGiven— preconditionWhen— actionThen— expected outcomeAnd/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) == 0The 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:
behaveRun a specific feature file:
behave features/shopping_cart.featureRun a specific scenario by line number:
behave features/shopping_cart.feature:14Run 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 undefinedUsing 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.jsonOr 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 stepsThere'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-runThis 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 = trueWhat'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
- Hooks —
before_scenario,after_scenario,before_allfor 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.