Skip to main content

Automatic retry for failed Behave scenarios — tag overrides, exception filtering, flakiness stats

Project description

behave-retry

CI PyPI Python License Coverage

Automatic retry for failed Behave scenarios — real re-execution, tag overrides, exception filtering, and flakiness stats.

Why?

Behave has no built-in retry. When a scenario fails due to flakiness (timing, network, race conditions), there's no way to re-run it automatically. Cucumber has --retry natively. Behave doesn't.

behave-retry fills that gap by patching Behave's Scenario.run to re-execute failed scenarios automatically — with tag overrides, exception filtering, and flakiness stats.

Table of contents

Install

pip install behave-retry

For development:

pip install -e ".[dev]"

Quick start

# environment.py
from behave_retry import setup_retry, after_scenario_hook, retry_report

def before_all(context):
    setup_retry(context, max_retries=3)

def after_scenario(context, scenario):
    after_scenario_hook(context, scenario)

def after_all(context):
    print(retry_report(context))

That's it. Failed scenarios will now be re-executed up to 3 times automatically.

Features

Global retry

setup_retry(context, max_retries=3)

Retry every failed scenario up to 3 times.

Tag-filtered retry

setup_retry(context, max_retries=3, retry_tags=["@flaky"])

Only retry scenarios tagged with @flaky.

@flaky
Scenario: Login with slow network
  Given the server is slow
  ...

Exception-filtered retry

setup_retry(
    context,
    max_retries=3,
    retry_on=[AssertionError, TimeoutError],
)

Only retry when the scenario fails with specific exception types. Subclasses of the listed exceptions also match.

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.

You can also set @retry:N at the Feature level — scenarios without their own @retry:N tag inherit it from the Feature:

@retry:3
Feature: Flaky scenarios

  Scenario: A  # inherits retry:3
    ...

  @retry:0
  Scenario: B  # override local, no retry
    ...

  @retry:5
  Scenario: C  # override local, retry 5
    ...

Scenario-level tags always take precedence over Feature-level tags.

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.

Logging

behave-retry logs via the standard logging module under the behave_retry logger:

  • INFO on setup_retry — configuration summary
  • INFO on each retry — Retrying "scenario name" (attempt 1/3) after AssertionError
  • INFO on retry_report — full summary

To enable logging, configure the logger in your environment.py:

import logging

def before_all(context):
    logging.basicConfig(level=logging.INFO)
    setup_retry(context, max_retries=3)

Or use a dedicated handler:

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

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.

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) where attempt is the 1-based number of the failed attempt and exception is the exception that caused the failure (or None).

API reference

setup_retry(context, max_retries=0, retry_tags=None, retry_on=None, retry_delay=0.0, backoff_factor=1.0, on_retry=None, max_total_retries=None)

Configure retry on the behave context. Call this in before_all. This patches behave.model.Scenario.run to re-execute failed scenarios automatically.

Parameter Type Default Description
context Any Behave context object
max_retries int 0 Maximum retries per scenario (0 = no retry)
retry_tags list[str] | None None Only retry scenarios with these tags
retry_on list[type[Exception] | str] | None None Only retry on these exception types or names (strings like "AssertionError" or "mymod.MyError")
retry_delay float 0.0 Seconds to wait before each retry (0 = no delay)
backoff_factor float 1.0 Multiplier applied to retry_delay after each retry (must be >= 1.0)
on_retry Callable | None None Callback invoked before each retry with (context, scenario, attempt, exception)
max_total_retries int | None None Global budget for total retries across all scenarios (None = unlimited)

after_scenario_hook(context, scenario)

Track retry attempts. Call this in after_scenario. The actual retry loop is handled automatically by the Scenario.run patch installed by setup_retry.

retry_report(context) -> str

Get a human-readable retry summary. Call this in after_all.

RetryConfig

Frozen dataclass for configuration.

Attribute Type Default Description
max_retries int 0 Maximum retries per scenario (must be >= 0)
retry_tags list[str] [] Tag filter (empty = retry all)
retry_on list[type[Exception] | str] [] Exception filter — classes or string names (empty = retry on any)
retry_delay float 0.0 Seconds to wait before each retry (must be >= 0)
backoff_factor float 1.0 Multiplier applied to retry_delay (must be >= 1.0)
on_retry Callable[[Any, Any, int, Exception | None], None] | None None Callback invoked before each retry
max_total_retries int | None None Global budget for total retries (None = unlimited)

Methods:

  • should_retry_tag(tags) -> bool — Check if scenario tags allow retry
  • should_retry_exception(exc) -> bool — Check if exception type allows retry
  • get_scenario_retries(tags, feature_tags=None) -> int — Get max retries, checking @retry:N on scenario then feature tags
  • get_retry_delay(attempt) -> float — Calculate delay for a given retry attempt (1-based)

RetryCallback

Type alias for the on_retry callback: Callable[[Any, Any, int, Exception | None], None].

ExceptionFilter

Type alias for a single retry_on entry: type[Exception] | str. Accepts exception classes or string names (e.g. "AssertionError", "mymod.MyError").

RetryStats

Aggregate retry statistics.

Property Type Description
total_retries int Total retry attempts across all scenarios
scenarios_retried list[ScenarioRetry] Per-scenario retry records
scenarios_passed_on_retry int Scenarios that passed after retry
scenarios_failed_after_retry int Scenarios that still failed after retry

Methods:

  • add_retry(scenario, attempts, final_status, exceptions) -> None — Record a new retry
  • update_retry(scenario, attempts, final_status, exceptions) -> None — Update existing or create new
  • summary() -> str — Human-readable summary
  • to_dict() -> dict — Serialize to a dictionary for CI/CD reporting

ScenarioRetry

Record of retry attempts for a single scenario.

Attribute Type Description
scenario str Scenario name
attempts int Total attempts (including first run)
final_status str "passed" or "failed"
exceptions list[str] Exception types encountered
Property Description
was_retried True if retried at least once
passed_on_retry True if passed after retry

Methods:

  • to_dict() -> dict — Serialize to a dictionary for CI/CD reporting

parse_retry_tag(tags) -> int | None

Parse @retry:N tag from scenario tags. Returns N if found, None otherwise. Also available as behave_retry.parse_retry_tag.

Configuration

Python version

behave-retry requires Python 3.11+.

Dependencies

Zero required dependencies. behave is only needed as a dev dependency for running tests.

Linting and formatting

The project uses Ruff with the following rule sets: E, F, W, I, N, UP, B, SIM.

ruff check .

Testing

pytest tests/ -v --cov

Coverage threshold is 90%.

Examples

Full environment.py

from behave_retry import setup_retry, after_scenario_hook, retry_report

def before_all(context):
    setup_retry(
        context,
        max_retries=3,
        retry_tags=["@flaky"],
        retry_on=[AssertionError, TimeoutError],
        retry_delay=2.0,
        backoff_factor=2.0,
        on_retry=lambda ctx, sc, att, exc: print(f"Retry {sc.name} #{att}: {exc}"),
    )

def after_scenario(context, scenario):
    after_scenario_hook(context, scenario)

def after_all(context):
    print(retry_report(context))

Feature file with retry tags

@flaky
@retry:5
Feature: Flaky scenarios

  @retry:0
  Scenario: Never retry this
    Given a stable condition
    Then it should pass

  Scenario: Retry up to 5 times
    Given a flaky condition
    Then it might fail

Contributing

Contributions are welcome! See CONTRIBUTING.md for guidelines.

License

MIT — Copyright (c) 2026 Mathias Paulenko

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

behave_retry-1.8.1.tar.gz (36.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

behave_retry-1.8.1-py3-none-any.whl (15.1 kB view details)

Uploaded Python 3

File details

Details for the file behave_retry-1.8.1.tar.gz.

File metadata

  • Download URL: behave_retry-1.8.1.tar.gz
  • Upload date:
  • Size: 36.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for behave_retry-1.8.1.tar.gz
Algorithm Hash digest
SHA256 fed18645fa5384f2e3eb3b02033f1debc990ea4bb74827496d786c6caa4988ae
MD5 f01c7d23f60aa0a16d562954c56e756d
BLAKE2b-256 271bb8e02831da10569624ef3d518879c453713521728c8d9686b0d09e3a7533

See more details on using hashes here.

Provenance

The following attestation bundles were made for behave_retry-1.8.1.tar.gz:

Publisher: release.yml on MathiasPaulenko/behave-retry

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file behave_retry-1.8.1-py3-none-any.whl.

File metadata

  • Download URL: behave_retry-1.8.1-py3-none-any.whl
  • Upload date:
  • Size: 15.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for behave_retry-1.8.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e60985260a1e3b1fc9be8c535f1c675aca06a79bdfbc83b008cbe28dfec8951e
MD5 9f73c46e65afc3ec1dcbc52c01faa33c
BLAKE2b-256 42897942452135997d16bd8b20ffaf1e7fa8a650139d14e061efdd9585b0994d

See more details on using hashes here.

Provenance

The following attestation bundles were made for behave_retry-1.8.1-py3-none-any.whl:

Publisher: release.yml on MathiasPaulenko/behave-retry

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page