Skip to main content

behave-kit

The Swiss-army knife for Behave — soft assertions, typed context, conditional skip, environment management, fixtures and more.

CI Coverage PyPI Python 3.11+ License: MIT

Why behave-kit?

Behave is great for BDD, but real test suites need more than Given/When/Then. You need to:

  • Collect multiple assertion failures without stopping at the first one
  • Skip steps based on environment, OS, or missing dependencies
  • Read environment variables with type conversion and validation
  • Load test data from CSV, JSON, YAML, or Excel files
  • Manage fixtures with automatic setup/teardown by tag
  • Get IDE autocompletion for context attributes
  • Debug failures with automatic context dumps
  • Poll for conditions with configurable timeout and interval
  • Assert expected exceptions as part of soft assertion collections
  • Run data-driven steps from CSV, JSON, YAML, or Excel files
  • Isolate environment variables with snapshot/restore context managers
  • Navigate nested dicts with dot notation
  • Assert execution time with assert_under and @timed
  • Create temp directories for filesystem-isolated tests
  • Continue after failed steps for comprehensive test reporting
  • Execute sub-steps with outline substitution and state isolation
  • Class-based steps — define Given/When/Then as methods on a class with self.context, lifecycle hooks, and per-step matchers
  • Per-scenario timeout — @timeout:N tags override a default, platform-aware (SIGALRM on Unix, threading.Timer on Windows)

behave-kit provides all of these as independent, opt-in utilities — no monkey-patching, no breaking changes.

Installation

pip install behave-kit

With optional extras:

pip install "behave-kit[yaml,excel,dotenv]"
Extra Provides Dependency
yaml YAML data loading pyyaml>=6.0
excel XLSX data loading openpyxl>=3.1
dotenv .env file loading python-dotenv>=1.0
docs Sphinx documentation build sphinx, furo, myst-parser
dev Development tools pytest, ruff, mypy, build, pre-commit

Quickstart

Level 1 — Automatic wiring

Add two lines to your environment.py and every feature is wired automatically:

from behave_kit import setup, teardown, teardown_feature


def before_all(context):
    setup(context, env="staging")


def before_scenario(context, scenario):
    context.kit_fixtures.setup_for_scenario(context, scenario)


def after_step(context, step):
    context.kit_suggestions(context, step)  # "did you mean?" hints


def after_scenario(context, scenario):
    teardown(context)  # runs fixture teardowns, scoped cleanup, soft-assert report


def before_feature(context, feature):
    context.kit_fixtures.setup_for_feature(context, feature)


def after_feature(context, feature):
    teardown_feature(context)  # FEATURE-scoped fixtures and attributes

Level 2 — Cherry-pick

Import only what you need:

from behave_kit import assert_soft, env, load_data


@then("the response should be valid")
def step(context):
    assert_soft(context.response.status_code == 200)
    api_key = env("API_KEY", required=True)
    users = load_data("tests/data/users.csv")

Level 3 — Namespace

import behave_kit as bk


@then("the response should be valid")
def step(context):
    bk.assert_soft(context.response.status_code == 200)

Features

Soft assertions

Collect multiple failures and report them all at once:

from behave_kit import assert_soft

assert_soft(response.status_code == 200)
assert_soft(response.body["count"] > 0)
assert_soft("error" not in response.body)
# All failures reported together at teardown

TypedContext

Schema-validated proxy with IDE autocompletion and mypy support:

from behave_kit import TypedContext


class MySchema:
    driver: str
    base_url: str


ctx = TypedContext(context, MySchema)
ctx.setup(driver="chrome", base_url="https://test.com")
print(ctx.driver)  # IDE autocompletion works

Conditional skip

Skip steps by environment, OS, or missing dependency:

from behave_kit import skip_if_env, skip_if_no_browser, skip_on_os, skip_if_missing


@when("I run the staging-only step")
@skip_if_env("production")
def step(context): ...


@when("I open the browser")
@skip_if_no_browser
def step(context): ...


@when("I run the unix-only step")
@skip_on_os("windows")
def step(context): ...


@when("I use selenium")
@skip_if_missing("selenium")
def step(context): ...

Note: behave-kit step decorators must be placed inside (below) the Behave @given/@when/@then decorator — only then is the wrapped function what Behave registers.

Environment variables

Typed reads with defaults, validation, and config file fallback:

from behave_kit import env

api_key = env("API_KEY", required=True)  # str
timeout = env("TIMEOUT", var_type=int, default=30)  # int
debug = env("DEBUG", var_type=bool, default=False)  # bool

Data loading

Single API for CSV, JSON, YAML, and Excel:

from behave_kit import load_data

users = load_data("tests/data/users.csv")  # list[dict]
config = load_data("tests/data/config.json")  # dict
sheet = load_data("tests/data/sheet.xlsx")  # list[dict] (requires [excel])

Fixtures

Tag-based setup/teardown with dependency resolution:

from behave_kit import fixture


@fixture("browser")
def browser_fixture(context):
    def setup(ctx):
        ctx.browser = start_browser()

    def teardown(ctx):
        ctx.browser.quit()

    return (setup, teardown)


@fixture("database", requires="browser")
def database_fixture(context): ...

Context dump

Automatic context snapshot on scenario failure for easier debugging:

from behave_kit import dump_context_on_failure
# Wired automatically by setup(), or call manually

Step suggestions

"Did you mean?" hints for undefined steps:

from behave_kit import setup_suggestions
# setup() attaches the hook as context.kit_suggestions;
# call it from after_step(context, step)

Scoped attributes

Automatic cleanup of context attributes per scenario:

from behave_kit import scoped


@when("I start the driver")
@scoped("driver")
def step(context):
    context.driver = start_driver()


# "driver" is automatically deleted after the scenario

Conditional steps

Run a step only when a condition holds:

from behave_kit import when_if
from behave_kit.skip.conditions import is_env


@when("I run the staging-only step")
@when_if(lambda ctx: is_env(ctx, "staging"))
def step(context): ...

Parameter types

Register custom Behave parameter types:

from behave_kit import parameter_type


@parameter_type("User", r"[\w.]+@[\w.]+\.[a-z]+")
def parse_user(text):
    return User(name=text)


@when('I log in as "{user:User}"')
def step(context, user):
    # user is a User instance — converted automatically
    context.current_user = user

Class-based steps

Define step implementations as methods on a class, with per-scenario instances, lifecycle hooks, and per-step matcher selection:

from behave_kit import step_impl_base

Base = step_impl_base()


class AccountSteps(Base):
    @Base.given("I have a balance of {amount:d}")
    def set_balance(self, amount):
        self.balance = amount

    @Base.when("I deposit {amount:d}")
    def deposit(self, amount):
        self.balance += amount

    @Base.then("the balance should be {expected:d}")
    def check_balance(self, expected):
        assert self.balance == expected

    @property
    def balance(self):
        return getattr(self.context, "balance", 0)

    @balance.setter
    def balance(self, value):
        self.context.balance = value

    def setup(self):
        # Called once when the instance is created for a scenario
        pass

    def teardown(self):
        # Called after the scenario ends (via teardown_steps / teardown)
        pass


AccountSteps.register()

self.context is bound automatically — no context parameter needed. Subclass to extend or override steps; register only the most-derived class. Pass matcher=RegexMatcher to a decorator for per-step matcher selection.

Soft exception assertions

Check that a callable raises an expected exception, collected as a soft failure:

from behave_kit import assert_soft_raises

assert_soft_raises(ValueError, lambda: int("abc"))
assert_soft_raises(KeyError, lambda: {}["missing"])
# Failures are collected and reported together at teardown

Data-driven steps

Run a step once per row of a data file, with column names injected as keyword arguments:

from behave_kit import data_driven


@when("I login as {username}")
@data_driven("tests/data/users.csv")
def step(context, username=None, password=None):
    login(username, password)


# Runs once per row in users.csv

Environment variable snapshot

Save and restore os.environ so tests don't leak environment variable changes:

from behave_kit import env_snapshot

with env_snapshot():
    os.environ["API_KEY"] = "test"
    # ... test ...
# API_KEY restored automatically

Dict navigation with get_path

Extract values from nested dicts using dot notation:

from behave_kit import get_path

city = get_path(response, "user.address.city")  # "Berlin"
name = get_path(response, "users.0.name", default="?")  # "Alice"

Time assertions

Assert that a callable completes within a time limit:

from behave_kit import assert_under, timed

assert_under(2.0, lambda: client.get("/health"))


@when("I fetch the data")
@timed(1.5)
def step(context): ...

Wait until

Poll a condition until it becomes true or a timeout is reached:

from behave_kit import wait_until

wait_until(lambda: context.response.status_code == 200, timeout=10, interval=0.5)

Temp workspace

Create an isolated temporary directory and restore the CWD on exit:

from behave_kit import temp_workspace

with temp_workspace() as tmp:
    config_path = tmp / "config.json"
    config_path.write_text("{}")
# Directory is cleaned up automatically

Continue after failed step

Control whether scenarios keep running remaining steps after a failure:

from behave_kit import continue_after_failed, continue_on_failure

# Enable globally
continue_after_failed(True)

# Or temporarily via context manager
with continue_on_failure():
    # scenarios inside this block continue after failed steps
    ...

Or wire it through setup():

from behave_kit import setup


def before_all(context):
    setup(context, continue_after_failed=True)

Sub-step execution with isolation

Execute Gherkin sub-steps with Scenario Outline variable substitution and guaranteed table/text restoration:

from behave_kit import run_steps


@when("I complete the checkout flow")
def step_impl(context):
    run_steps(
        context,
        """
        Given I have items in my cart
        When I enter shipping info for "<city>"
        Then I should see the order confirmation
    """,
    )


# context.table and context.text are preserved after execution

Per-scenario timeout

Set a default timeout and override it per scenario or feature with @timeout:N tags:

from behave_kit import setup_timeout
from behave_kit.timeout import timeout_before_scenario, timeout_after_scenario


def before_all(context):
    setup_timeout(context, default_timeout=30)


def before_scenario(context, scenario):
    timeout_before_scenario(context, scenario)


def after_scenario(context, scenario):
    timeout_after_scenario(context, scenario)
@timeout:10
Scenario: Fast scenario with tag override
  When I do something quick

@timeout:0
Scenario: Disable timeout for this scenario
  When I do something slow

@timeout:60
Feature: Feature-level timeout inherits to all scenarios
  • Unix: uses signal.SIGALRM for immediate interruption.
  • Windows: uses threading.Timer fallback — it cannot interrupt running code, so the timeout is detected and raised when the scenario ends.
  • @timeout:0 disables the timeout for that scenario.
  • Feature-level tags inherit to all scenarios; scenario tags override feature tags.

You can also set the default via the BEHAVE_SCENARIO_TIMEOUT environment variable. If you call setup_timeout(context) without default_timeout, the value is read from the environment (default 0):

BEHAVE_SCENARIO_TIMEOUT=30 behave

Documentation

Full documentation is available at mathiaspaulenko.github.io/behave-kit.

To build docs locally:

pip install "behave-kit[docs]"
cd docs && sphinx-build -b html . _build/html

Development

See CONTRIBUTING.md for development setup, coding standards, and contribution guidelines.

License

MIT — see LICENSE.

Metadata

Release files for behave-kit 1.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for behave-kit 1.6.0
File Size Uploaded
behave_kit-1.6.0.tar.gz 128.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for behave-kit 1.6.0
File Interpreter ABI Platform
behave_kit-1.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 194.7 kB

Release files / behave_kit-1.6.0.tar.gz

Download URL behave_kit-1.6.0.tar.gz
Size 128.3 kB
Tags Source
SHA-256 checksum
How to use checksums
ce09e50d1a60fc8d65280fca5a2106e289cb0fe883fe5486862ca3b643570dc0
BLAKE2b-256 checksum
How to use checksums
0abd781924c12b4522b61c484499651136a2356403162c7d101acd8753e1560e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release files / behave_kit-1.6.0-py3-none-any.whl

Download URL behave_kit-1.6.0-py3-none-any.whl
Size 66.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
8243d0099f66defc89b613d5549058cc56c70a1eaa375eda1c58caea8fefc141
BLAKE2b-256 checksum
How to use checksums
a03d8ac09ef5956af84eb424161df4767cb0ea7919de8c4bac962a9e10c39ed4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 2, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.6.0 This release

2 release files

1.5.0

2 release files

1.4.0

2 release files

1.2.0

2 release files

1.1.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page