Step Definitions
Step definitions are Python functions that execute Gherkin steps. Good steps are reusable, thin wrappers over application code, and share state through fixtures or context objects.
Recipe
from pytest_bdd import given, when, then, parsers
@given(parsers.parse('a user with email "{email}"'))
def user_with_email(email, user_factory):
return user_factory(email=email)
@when("they log in")
def login(user_with_email, auth_client):
auth_client.login(user_with_email)
@then(parsers.parse('they see "{message}"'))
def see_message(message, page):
assert message in page.contentWhen to reach for this:
- Mapping Gherkin text to Python execution
- Building a library of reusable steps
- Parsing parameters from natural-language step text
Working Example
# features/steps/common.py
from pytest_bdd import given, when, then, parsers
import httpx
class APIContext:
def __init__(self):
self.client = None
self.response = None
self.token = None
@given("an authenticated API client")
def auth_client(context: APIContext, test_token):
context.client = httpx.Client(
base_url="http://testserver",
headers={"Authorization": f"Bearer {test_token}"},
)
context.token = test_token
@when(parsers.parse('I send a {method} request to "{path}"'))
def send_request(context: APIContext, method, path):
context.response = context.client.request(method, path)
@then(parsers.parse("the response status is {code:d}"))
def check_status(context: APIContext, code):
assert context.response.status_code == code
@then(parsers.cfparse("the response contains {count:d} items"))
def check_count(context: APIContext, count):
data = context.response.json()
assert len(data) == countWhat this demonstrates:
- Context object holds per-scenario state
parsers.parseandparsers.cfparseextract typed parameters- Reusable steps shared across feature files
- Steps delegate to application code, not reimplement it
Deep Dive
Step Matching
- Exact text match first
parsers.parsefor{name},{name:d},{name:f},{name:w}(word)- Longest match wins when multiple steps could apply
Layering
Feature file (what)
-> Step definition (thin glue)
-> Application service (how)
-> Domain logic
Gotchas
- Fat step definitions - business logic in steps. Fix: call service functions; steps are one-liner glue.
- Ambiguous step text - multiple matches. Fix: make steps more specific or use regex anchors.
- Shared mutable context - scenario A affects scenario B. Fix: fresh context per scenario (fixture scope=function).
- Stringly-typed parameters -
"100"vs100. Fix: use{amount:f}or{amount:d}typed parsers. - Duplicate step definitions - registration error. Fix: one definition per step text; share via imports.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Generic steps | Highly parametrized domains | Steps become unreadable |
| Page Object Model | UI testing | API-only projects |
| Direct pytest | No Gherkin needed | Stakeholder collaboration required |
FAQs
How do I pass data between steps?
Context object, pytest fixtures, or behave's context attribute.
Can steps call other steps?
Avoid. Extract shared logic into helper functions instead.
How do I handle tables in steps?
pytest-bdd: target_fixture and datatable parameter. behave: context.table.
What is parsers.cfparse?
Parse format with more type specifiers than basic parse. Part of pytest-bdd.
How do I reuse steps across projects?
Publish a shared steps/ package pip-installed as a dev dependency.
How do I test step definitions?
Call the Python function directly in unit tests with mock context.
Given steps as fixtures?
pytest-bdd supports target_fixture=True on @given to inject as fixture.
How do I handle async steps?
pytest-bdd with pytest-asyncio: async def step functions.
Step not found error?
Gherkin text must match exactly. Check for trailing spaces and quote style.
How many shared steps?
10-20 common steps (auth, API client, assertions). Feature-specific steps stay local.
Related
- Gherkin Syntax - step text authoring
- behave / pytest-bdd Setup - project wiring
- Gherkin to Code - implementation workflow
- Fixtures - pytest fixture patterns
Stack versions: This page was written for Python 3.14.0, FastAPI 0.115+, Django 5.2, Flask 3.1, Pydantic 2, PyTorch 2.6+, pandas 2.2+, Polars 1.x, ruff 0.9+, and uv 0.6+.