Python BDD has a quiet problem: the frameworks that defined the style are mostly abandoned, and the one that is actively maintained is the one that looks least like Cucumber. lettuce — the library that introduced “feature files in Python” to a generation of teams — has not had a commit since December 2020. Its last PyPI release, 0.2.23, predates Python 3.10. Yet it still has 1,275 GitHub stars and still appears in tutorials that rank on the first page for “python bdd”.
That gap between popularity and maintenance is exactly where engineering time gets wasted. So I built one feature file with 200 scenarios, implemented the same steps twice — once for behave, once for pytest-bdd — and measured both on the same machine. Then I read the maintenance and Python-support metadata for all four candidates.
TL;DR: The Verdict in 40 Seconds
Use pytest-bdd if your team already runs pytest — which is almost every Python team in 2026. You get the entire pytest plugin ecosystem for free: pytest-xdist for parallelism, pytest-cov for coverage, pytest-rerunfailures, and fixture scoping that behave cannot express. Version 9.0.1 requires Python 3.10+, which is a real constraint on legacy projects.
Use behave if you want the Cucumber model to be the primary interface — its own runner, its own environment.py hooks, context object, built-in --jobs parallelism, and 3,544 stars of community step definitions. It starts faster than pytest-bdd on tiny suites (0.13 s versus 0.38 s wall for 200 trivial scenarios) and ships a JUnit/JSON formatter without plugins.
Do not use lettuce. No release since 2020, no maintenance, no Python 3.12+ guarantees. Migration to either of the above is mechanical: the Gherkin files are compatible.
radish-bdd is alive but tiny — 191 stars, v0.18.4, last push September 2026. It has genuinely nice features (scenario preconditions, --with-threads), but the community is too small to bet a company’s test suite on.
The 2026 Comparison Table
Live GitHub and PyPI data, pulled 2026-11-06:
| behave | pytest-bdd | lettuce | radish-bdd | |
|---|---|---|---|---|
| Stars | 3,544 | 1,473 | 1,275 | 191 |
| Last push | 2026-11-05 | 2026-11-02 | 2020-12-29 | 2026-09-28 |
| Latest release | 1.3.3 | 9.0.1 | 0.2.23 | 0.18.4 |
| Python requirement | ≥2.7 (legacy supported) | ≥3.10 | unspecified | ≥3 |
| Runtime dependencies | cucumber-expressions, cucumber-tag-expressions, parse, parse-type | mako, parse, parse-type, pytest, typing-extensions | none declared | docopt, colorful, tag-expressions, parse-type |
| Test runner | its own (behave) | pytest | its own | its own |
| Parallel execution | built in (--jobs N) | via pytest-xdist | no | threads |
| Fixtures / dependency injection | context object only | full pytest fixture graph | world object | scenario preconditions |
| Tag filtering | --tags=@wip | -m markers plus --bdd-features | limited | tags |
| JUnit XML | built in (-f junit) | via --junitxml | plugin | partial |
| Actively maintained | yes | yes | no | yes, small team |
| License | BSD-3-Clause | MIT | MIT | MIT |
Two rows are worth reading twice. Python requirement: pytest-bdd 9 dropped everything below 3.10, so if you still run 3.9 anywhere you are pinned to pytest-bdd 7.x. Fixtures: behave’s context is a mutable bag shared across a scenario’s steps; pytest-bdd gives you pytest fixtures, which means factories, teardown, scoping and autouse — a substantially different testing model.
Real Timings: 200 Scenarios, Identical Steps
Method: a single generated feature file with 200 scenarios, each with three steps (Given an invoice with N widget at PRICE, When tax of R percent is applied, Then the total is X), implemented with decimal.Decimal arithmetic in both frameworks. Same interpreter, same machine, cold process each run, three runs per framework.
| Framework | Wall clock (3 runs) | Framework-reported | Notes |
|---|---|---|---|
| behave 1.3.3 | 0.18 s / 0.14 s / 0.13 s | 0.019 s | behave -f plain |
| pytest-bdd 9.0.1 | 0.55 s / 0.38 s / 0.38 s | 0.15 s | pytest -q, 0.03 s collection |
The interesting number is not the total — both are fast enough that it never matters at 200 scenarios. It is the constant overhead: pytest-bdd spends roughly 0.2–0.4 s more per invocation booting pytest, importing plugins and collecting. On a 200-scenario suite that is 60% of the runtime. On a 5,000-scenario suite it is invisible.
So do not choose on speed. Choose on what happens at 5,000 scenarios, which is where parallelism and fixture reuse decide your CI duration. behave --jobs 8 works out of the box; pytest-bdd’s pytest -n 8 requires pytest-xdist but distributes far more evenly because pytest fixtures can be session-scoped and shared per worker.
Decision Matrix: Pick by Use Case
| Use case | Pick | Why |
|---|---|---|
| Existing pytest suite, want Gherkin for a subset | pytest-bdd | Same runner, same fixtures, same coverage config |
| Business stakeholders read and edit feature files | behave | Standalone runner, cleaner CLI, Cucumber-like output |
| Need parallel BDD with no extra plugins | behave | --jobs N is built in |
Need BDD results inside pytest --junitxml for CI | pytest-bdd | Native pytest reporting |
| Python 3.9 or older in production | behave | pytest-bdd 9 requires 3.10+ |
| Contract/acceptance tests that must share DB fixtures | pytest-bdd | Session-scoped fixtures beat a context bag |
| A codebase already on lettuce | migrate to behave | Feature files port unchanged; steps need rewriting |
| Want Gherkin without the Cucumber ceremony | radish-bdd | Lighter model, but a 191-star bus factor |
behave — The Standalone Cucumber Runner
behave owns the whole process: it discovers .feature files, imports step modules, and runs scenarios in its own runner. The layout is fixed by convention, which is either a feature or a straitjacket depending on your taste.
| |
| |
| |
| |
Hooks live in environment.py and are the closest thing behave has to fixtures:
| |
Run it with tags, parallelism and a CI-friendly report:
| |
The context object is behave’s biggest liability. It is a mutable namespace with no scoping rules: before_all values are visible in every scenario, before_scenario values are lost after it, and nothing stops one step from overwriting another’s key. Teams that grow past ~200 scenarios usually end up writing their own context.reset() in environment.py and a naming convention for keys. pytest fixtures solve this problem properly, which is why most teams eventually move.
pytest-bdd — Gherkin on Top of the pytest You Already Have
pytest-bdd translates each scenario into a pytest test item. You keep your conftest.py, your fixtures, your markers, your coverage config, your xdist workers — and gain feature files.
| |
| |
| |
For a single named scenario, use the decorator form:
| |
And for typed parsing beyond {qty:d}, cfparse handles converters:
| |
The real payoff is that everything else is pytest:
| |
| |
That autouse transactional fixture is the thing behave cannot do cleanly: it isolates every scenario automatically, with no per-scenario bookkeeping in environment.py, and it works identically for your unit tests. Our pytest plugin guide covers xdist, timeout and asyncio configuration in more depth, and the Python benchmarking tooling comparison is the place to start if you want to measure the suite itself.
lettuce and radish-bdd — What to Know Before You Gamble
lettuce was the original Python BDD tool and it is effectively finished. Last commit 2020-12-29, last release 0.2.23, no declared Python version support, and its world object predates modern fixture patterns. It still installs and still runs simple suites, which is exactly what makes it dangerous: green tests on Python 3.9 say nothing about whether it works on 3.13. If you inherit a lettuce suite, port the .feature files to behave — the Gherkin syntax is compatible — and rewrite the step modules, which is a day of mechanical work.
| |
radish-bdd is the opposite case: alive, maintained (v0.18.4, pushed 2026-09-28), and genuinely interesting — scenario preconditions, tag expressions, colorful output, and a thread-based runner. But it has 191 stars and a single maintainer. Its features are attractive enough that you might choose it for a side project; it is not a reasonable dependency for a team that needs someone else to have hit the bug first.
The Pitfalls That Cost Real Days
1. Step-definition collisions are silent in behave, fatal in pytest-bdd. If two modules define the same step text, pytest-bdd raises “Step … is already defined” at collection; behave simply uses whichever module was imported last. In a large suite the behave behaviour means a refactor can quietly start running a different implementation. Keep one step module per domain and grep for the step text before adding a new one.
2. scenarios() must point at a real directory. pytest-bdd binds scenarios at import time. A path typo produces no tests rather than an obvious failure in some configurations, and an empty run looks exactly like a passing run in CI. Assert the count: pytest --collect-only -q | tail -1 should match the number of scenarios you expect.
3. The context bag leaks between scenarios. behave does not reset context attributes you set in before_scenario unless you do it, and attributes set in a step persist into the next scenario’s context object. Any test that passes only because a previous scenario populated a value is a test that will fail in a different order. Always pair before_scenario with after_scenario cleanup.
4. Parse expressions versus Cucumber expressions. {qty:d} is typed; {qty} is a string. Mixing them produces assertion failures that look like arithmetic bugs. Use parsers.parse in pytest-bdd and the plain quoted string in behave, and write one scenario that exercises the type boundary.
5. Parallel execution changes your fixtures. behave --jobs 4 forks scenarios across processes, so before_all runs per process, not once per run — a session-scoped database setup will run four times. With pytest-xdist the same applies: session fixtures are per worker. Budget for it when you migrate a slow suite to parallel.
6. Gherkin that is really a test script. The most common failure mode is not technical: teams write 40-step scenarios that encode implementation details, then discover a UI refactor breaks 300 feature files. Keep scenarios at the business level (three to five steps), push detail into step implementations, and use Scenario Outline for data variation instead of copying scenarios.
7. No tag hygiene. Tags are the only lever you have for suite partitioning. Establish them early (@smoke, @slow, @wip, @integration) and enforce them in CI, because retrofitting tags onto 800 untagged scenarios is a week nobody enjoys.
8. HTML reports that nobody opens. behave’s JSON output and pytest’s JUnit XML are what CI actually consumes. Decide the report format before you write the first scenario; converting historical results later is impossible.
FAQ
Is pytest-bdd or behave better in 2026?
pytest-bdd, for most teams. It runs inside pytest, so you keep fixtures, xdist parallelism, coverage and the entire plugin ecosystem, and it is actively maintained (v9.0.1, pushed 2026-11-02). Choose behave instead if you need a standalone runner, built-in --jobs parallelism, or Python 3.9 support — pytest-bdd 9 requires Python 3.10 or newer.
Is lettuce still maintained?
No. The last commit to gabrielfalcao/lettuce was on 2020-12-29 and the last PyPI release is 0.2.23. It has no declared Python version support and no recent compatibility work. Port the feature files to behave or pytest-bdd; the Gherkin syntax carries over, only the step modules need rewriting.
How much slower is pytest-bdd than behave?
In my measurement of 200 identical scenarios, behave completed in 0.13–0.18 s wall clock and pytest-bdd in 0.38–0.55 s, of which pytest’s own report attributed 0.15–0.16 s to test execution. The difference is pytest’s startup and collection cost, roughly 0.2–0.4 s per invocation. At 5,000 scenarios it is irrelevant; what matters at that scale is parallel distribution, where pytest-xdist is stronger.
Can I run behave and pytest-bdd in the same repository?
Yes, and it is a reasonable migration strategy: keep the existing behave suite running while new scenarios are written against pytest-bdd, with both sharing the same .feature files. The risk is step-definition drift — two implementations of the same step text that behave differently. Migrate feature by feature and delete the old step module as each one moves.
How do I get JUnit XML out of a Python BDD suite for CI?
With behave, use its built-in formatter: behave -f junit -o reports/. With pytest-bdd, pytest’s own flag applies: pytest --junitxml=reports/bdd.xml. Both produce standard JUnit XML that Jenkins, GitLab CI and GitHub Actions can parse without extra plugins.
Does BDD work for API tests, or only for browser tests?
Both frameworks are runner-agnostic — steps are plain Python. The common pattern for API suites is a requests/httpx client in a fixture, a database transaction per scenario, and steps that assert on response bodies. See our HTTP mocking comparison for the record/replay side of that setup.
💰 想测试你的市场判断力?我用 Polymarket 做预测市场交易——这是全球最大的预测市场平台,从大选结果到技术监管时间线,什么都可以押注。和赌博不同,这是真正的信息市场:你懂的信息越多,胜率越高。我靠预测技术相关事件的走向已经赚了不少。用我的邀请链接注册:Polymarket.com