Features¶
Global retry¶
Retry every failed scenario up to N times:
setup_retry(context, max_retries=3)
With max_retries=3, a scenario gets up to 4 total executions (1 initial + 3 retries). Set max_retries=0 (default) to disable retry entirely.
Tag-filtered retry¶
Only retry scenarios with specific tags:
setup_retry(context, max_retries=3, retry_tags=["@flaky"])
@flaky
Scenario: Login with slow network
Given the server is slow
...
Multiple tags work as OR — a scenario needs at least one matching tag to be eligible:
setup_retry(context, max_retries=3, retry_tags=["@flaky", "@unstable"])
Behave strips the leading @ from tags, so both @flaky and flaky are accepted in retry_tags.
Exception-filtered retry¶
Only retry when the scenario fails with specific exception types. Subclasses also match:
setup_retry(
context,
max_retries=3,
retry_on=[AssertionError, TimeoutError],
)
String names are also supported — useful for configuration files or when you want to avoid importing the exception class:
setup_retry(
context,
max_retries=3,
retry_on=["AssertionError", "mymod.MyError"],
)
Dotted paths are resolved via importlib:
setup_retry(
context,
max_retries=3,
retry_on=["requests.exceptions.ConnectionError"],
)
How exception filtering interacts with retries¶
When retry_on is set and a scenario fails:
The wrapper extracts the exception type from the last failed step.
If the exception type matches any entry in
retry_on(viaissubclass), the scenario is retried.If the exception type does not match, the scenario fails immediately — no retry.
If no exception can be found on the failed step (e.g. behave didn’t attach one), the scenario is not retried.
Per-scenario override¶
Override the global retry count per scenario using the @retry:N tag:
@retry:5
Scenario: Very flaky test
...
@retry:0
Scenario: Never retry this
...
The first @retry:N tag wins. @retry:0 disables retry for that scenario. Negative values are clamped to 0.
Global retry budget¶
Limit the total number of retries across all scenarios:
setup_retry(context, max_retries=5, max_total_retries=20)
Once the global budget is exhausted, no more retries are attempted — remaining scenarios run only once. None (default) means unlimited.
The budget is shared across all scenarios in the test run. Each retry decrements the budget by 1. When the budget reaches 0, the current scenario fails immediately and no further retries are attempted on any scenario.
# With max_total_retries=2:
# Scenario A fails → retry 1 (budget: 1)
# Scenario A fails → retry 2 (budget: 0)
# Scenario A fails → no more retries, fails
# Scenario B fails → budget exhausted, no retry, fails immediately
Retry delay and backoff¶
Add a configurable delay between retries, with optional exponential backoff:
setup_retry(
context,
max_retries=3,
retry_delay=2.0,
backoff_factor=2.0,
)
With retry_delay=2.0 and backoff_factor=2.0, delays between retries are 2s, 4s, 8s. With backoff_factor=1.0 (default), the delay is fixed at retry_delay seconds.
The delay is calculated as:
delay = retry_delay * (backoff_factor ** (attempt - 1))
If retry_delay=0.0 (default), no delay is applied regardless of backoff_factor.
On-retry callback¶
Run custom logic before each retry — clean up state, take screenshots, log, etc.:
def on_retry(context, scenario, attempt, exception):
print(f"Retrying {scenario.name} (attempt {attempt}): {exception}")
setup_retry(
context,
max_retries=3,
on_retry=on_retry,
)
The callback receives (context, scenario, attempt, exception):
Parameter |
Type |
Description |
|---|---|---|
|
|
Behave context object |
|
|
Behave scenario object that failed |
|
|
1-based number of the failed attempt |
|
|
Exception that caused the failure, or |
The callback is called before the delay and scenario reset, so you can inspect the failed state.
Logging¶
behave-retry logs via the standard logging module under the behave_retry logger:
INFO on
setup_retry— configuration summaryINFO on each retry —
Retrying "scenario name" (attempt 1/3) after AssertionErrorINFO on
retry_report— full summary
import logging
def before_all(context):
logging.basicConfig(level=logging.INFO)
setup_retry(context, max_retries=3)
Or use a dedicated handler with custom formatting:
import logging
def before_all(context):
handler = logging.StreamHandler()
handler.setFormatter(logging.Formatter("[behave-retry] %(message)s"))
logging.getLogger("behave_retry").addHandler(handler)
logging.getLogger("behave_retry").setLevel(logging.INFO)
setup_retry(context, max_retries=3)
Retry stats¶
from behave_retry import retry_report
def after_all(context):
report = retry_report(context)
print(report)
Output:
Retry Summary:
Total retries: 5
Scenarios retried: 3
Passed on retry: 2
Failed after retry: 1
- "Login with invalid credentials" — 3 attempts, failed (AssertionError)
- "Search products" — 2 attempts, passed
- "Checkout flow" — 2 attempts, passed
Machine-readable stats¶
For CI/CD integration, export stats as JSON:
import json
def after_all(context):
stats = getattr(context, "_behave_retry_stats", None)
if stats:
with open("retry_report.json", "w") as f:
json.dump(stats.to_dict(), f, indent=2)
See RetryStats and ScenarioRetry for the full API.
Scenario Outline support¶
Scenario Outline examples are uniquely identified by filename:line:name, preventing key collisions between examples that share the same file and line but have different names after placeholder substitution.
Scenario Outline: Login with <user>
Given a user "<user>"
When they log in with password "<pass>"
Then the login should <result>
Examples:
| user | pass | result |
| alice | secret123 | succeed |
| bob | wrong | fail |
| carol | expired | fail |
Each example gets its own independent retry count. If “alice” passes on the first try but “bob” fails and is retried, only “bob” consumes retry budget.
Combining features¶
All features can be combined. The retry decision flow for a failed scenario is:
Retry count check —
max_for_scenario == 0? → no retryTag filter check —
retry_tagsset and scenario doesn’t match? → no retryRun scenario — if it passes, done
Exception filter check —
retry_onset and exception doesn’t match? → no retry, failRetry limit check —
attempt > max_for_scenario? → no retry, failBudget check —
max_total_retriesset and budget exhausted? → no retry, failRetry — increment budget, call
on_retry, sleepdelay, reset state, go to step 3