Class-based steps

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

Class-based steps are an alternative to Behave’s function-based @given, @when, @then decorators. Related steps live together on a class, state is kept on self instead of context, and standard Python inheritance (subclassing, overriding, mixins) works as expected.

How it works

  1. Call behave_kit.step_impl_base() to create a fresh base class with its own local step registry.

  2. Subclass it and decorate methods with @Base.given, @Base.when, @Base.then (or @Base.step).

  3. Call register() on the class to add the steps to Behave’s global registry.

  4. Wire teardown_steps(context) (or behave_kit.teardown()) in after_scenario so teardown() hooks run.

Basic example

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

AccountSteps.register()

self.context is bound automatically — no context parameter is needed on step methods. A fresh instance is created per scenario, so state on self is isolated between scenarios.

Lifecycle hooks

Define setup() and teardown() methods on the class to run code once per scenario per class:

class DatabaseSteps(Base):
    def setup(self):
        self.connection = connect_to_test_db()

    def teardown(self):
        self.connection.close()

    @Base.given("a clean database")
    def clean_db(self):
        self.connection.execute("DELETE FROM users")

setup() runs lazily — only when the first step of the class is called in a scenario. teardown() runs when teardown_steps(context) is called (typically in after_scenario).

Wiring teardown

Automatic (via ``setup()`` / ``teardown()``):

from behave_kit import setup, teardown

def before_all(context):
    setup(context)

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

Manual:

from behave_kit import teardown_steps

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

Subclassing and overriding

Subclass a step class to add, override, or extend steps. Method overrides are picked up automatically when the subclass is registered:

from some_library.steps import AccountSteps as Base

class ExtendedAccountSteps(Base):
    def withdraw(self, amount):
        if amount > self.balance:
            raise ValueError("Insufficient funds")
        super().withdraw(amount)

    @Base.then("the balance should be positive")
    def check_positive(self):
        assert self.balance > 0

# Register only the most-derived class.
ExtendedAccountSteps.register()

Register only the most-derived class — its register() includes all inherited steps with overrides applied. Registering both parent and child raises AmbiguousStep.

Per-step matcher

Each step can use a different matcher without changing global state. Pass matcher= to the decorator:

from behave.matchers import RegexMatcher

class Steps(Base):
    @Base.then(
        r"the balance should be (less|greater) than (\d+)",
        matcher=RegexMatcher,
    )
    def compare(self, operator, amount):
        ...

Accepts a matcher class (e.g. RegexMatcher, ParseMatcher) or a name string ("parse", "re", "cfparse").

A default matcher for all steps in a base can be set via step_impl_base(default_matcher="re").

Idempotent registration

register() is idempotent — calling it twice on the same class is a no-op. clear() removes previously registered steps from Behave’s global registry:

AccountSteps.register()
AccountSteps.register()  # no-op
AccountSteps.clear()     # removes the steps

Independent libraries

Each call to step_impl_base() creates an isolated local registry, so independent step libraries can coexist without interfering with each other:

BaseA = step_impl_base()
BaseB = step_impl_base()

class StepsA(BaseA): ...
class StepsB(BaseB): ...

StepsA.register()
StepsB.register()  # no conflict — separate registries

Limitations

  • Async step methods (async def) are not supported. Use the function-based @given / @when / @then decorators for async steps.

  • __init__ must not require arguments (use default values for any configuration).

API reference

behave_kit.steps.classes.step_impl_base(default_matcher: Any = None) type[source]

Return a new base class for class-based step implementations.

Each call creates an isolated local step registry, so independent step libraries can coexist without interfering with each other. Subclasses share the registry of the base they derive from, which is what enables extension via subclassing and method overriding.

Parameters:

default_matcher – Default matcher for steps that don’t specify one. Accepts None (Behave’s current default), a string name ("parse", "re", "cfparse", "simplified", "cucumber"), or a Matcher subclass.

Returns:

A base class with given, when, then, step decorator factories and register() / clear() methods.

Raises:

StepError – If default_matcher is not a valid matcher reference.

behave_kit.steps.classes.teardown_steps(context: Context) None[source]

Call teardown() on every live step-instance for context and clear them.

Safe to call when no class-based steps have run (no-op). Wired automatically by behave_kit.setup() / behave_kit.teardown().

Iterates over a snapshot of the instances dict so that a teardown() method that triggers additional step execution (which could add new instances) does not cause RuntimeError: dictionary changed size during iteration.