Configuration

setup_retry parameters

setup_retry is the main entry point. It creates a RetryConfig and patches Scenario.run.

setup_retry(
    context,
    max_retries=3,
    retry_tags=["@flaky"],
    retry_on=[AssertionError, "TimeoutError"],
    retry_delay=2.0,
    backoff_factor=2.0,
    on_retry=on_retry_callback,
    max_total_retries=20,
)

Parameter reference

Parameter

Type

Default

Description

context

Any

Behave context object (required)

max_retries

int

0

Maximum retries per scenario (0 = no retry)

retry_tags

list[str] | None

None[]

Only retry scenarios with these tags (empty = retry all)

retry_on

list[type[Exception] | str] | None

None[]

Only retry on these exception types or names (empty = retry on any)

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)

RetryConfig

The core configuration dataclass. It is frozen (immutable) and validated on creation. You can also construct it directly:

from behave_retry import RetryConfig

config = RetryConfig(
    max_retries=3,
    retry_tags=["@flaky"],
    retry_on=[AssertionError, "TimeoutError"],
    retry_delay=2.0,
    backoff_factor=2.0,
    max_total_retries=20,
)

Methods

Method

Returns

Description

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)

Validation

RetryConfig raises ValueError on invalid inputs:

Condition

Error message

max_retries < 0

max_retries must be >= 0, got N

retry_delay < 0

retry_delay must be >= 0, got N

backoff_factor < 1.0

backoff_factor must be >= 1.0, got N

max_total_retries < 0 (when not None)

max_total_retries must be >= 0 or None, got N

from behave_retry import RetryConfig

RetryConfig(max_retries=-1)  # raises ValueError
RetryConfig(retry_delay=-1.0)  # raises ValueError
RetryConfig(backoff_factor=0.5)  # raises ValueError
RetryConfig(max_total_retries=-5)  # raises ValueError

Since RetryConfig is frozen, attempting to modify an attribute raises AttributeError:

config = RetryConfig(max_retries=3)
config.max_retries = 5  # raises AttributeError

Tag override precedence

The @retry:N tag is resolved in this order:

  1. Scenario-level @retry:N tag — highest priority

  2. Feature-level @retry:N tag — fallback if scenario has none

  3. Global max_retries from RetryConfig — default

@retry:3          ← Feature-level
Feature: Example

  Scenario: A     ← No @retry tag → inherits 3 from Feature

  @retry:5
  Scenario: B     ← Scenario-level @retry:5 → overrides to 5

  @retry:0
  Scenario: C     ← Scenario-level @retry:0 → disables retry

Negative values from tags are clamped to 0:

@retry:-5
Scenario: X  # clamped to 0, no retry

The first valid @retry:N tag wins. Subsequent @retry:N tags on the same scenario are ignored:

@retry:3
@retry:5
Scenario: X  # retry:3 wins, retry:5 is ignored

Exception filter resolution

String entries in retry_on are resolved to exception classes on first use and cached for subsequent calls:

  • Bare names ("AssertionError") — looked up in builtins

  • Dotted paths ("mymod.MyError") — split into module path and class name, imported via importlib

# These are equivalent:
retry_on=[AssertionError]
retry_on=["AssertionError"]

# Dotted path for custom exceptions:
retry_on=["requests.exceptions.ConnectionError"]

Exception matching

Matching uses issubclass, so subclasses of the listed exceptions also match:

class MyTimeoutError(TimeoutError):
    ...

# MyTimeoutError matches because it's a subclass of TimeoutError
setup_retry(context, retry_on=[TimeoutError])

Error handling

  • If a string cannot be resolved to an exception class, ImportError is raised on first use (not at config time).

  • If an entry is neither a class nor a string, TypeError is raised.

Retry delay calculation

The delay for attempt N is:

delay = retry_delay * (backoff_factor ** (attempt - 1))

Attempt

retry_delay=2.0, backoff_factor=2.0

retry_delay=1.0, backoff_factor=1.0

retry_delay=0.5, backoff_factor=1.5

1

2.0s

1.0s

0.5s

2

4.0s

1.0s

0.75s

3

8.0s

1.0s

1.125s

4

16.0s

1.0s

1.6875s

If retry_delay=0.0, all delays are 0.0 regardless of backoff_factor.

Python version

behave-retry requires Python 3.11+.

Dependencies

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

Type checking

behave-retry ships with a py.typed marker (since v1.8.0), enabling type checkers like mypy and pyright to recognize type information:

from behave_retry import RetryConfig, RetryStats

config: RetryConfig = RetryConfig(max_retries=3)
# mypy knows the type of config.max_retries is int