Skip to main content

behave-tables

PyPI version Python versions CI status Coverage License Pydantic

Polished API for Behave Data Tables — convert to dicts, Pydantic models, CSV, JSON & JSON Lines. Zero deps · Type-safe · Python 3.11+ · 100% test coverage


Why?

Behave's Table requires manual iteration:

# Without behave-tables — repetitive, error-prone
for row in context.table.rows:
    name = row["name"]
    age = row["age"]
    # No dicts, no models, no CSV, no JSON...

Every project reimplements the same helpers. behave-tables fixes this with a single wrap() call.


Install

pip install behave-tables

# Optional: pydantic for as_models() with validation & type coercion
pip install behave-tables[pydantic]

Quick start

from behave_tables import TableWrapper, wrap

@then("the users should be")
def step_impl(context):
    table = wrap(context.table)

    # Convert to dicts
    users = table.as_dicts()
    # [{"name": "Alice", "age": "30"}, {"name": "Bob", "age": "25"}]

    # Convert to models (Pydantic v2 or dataclass)
    users = table.as_models(User)

    # Extract a single column
    names = table.column("name")  # ["Alice", "Bob"]

    # Find the first matching row
    alice = table.find_row(name="Alice")  # {"name": "Alice", "age": "30"}

    # Find all matching rows
    thirty_somethings = table.find_all_rows(age="30")

    # Validate columns (strict mode detects extras too)
    table.validate_columns("name", "age", strict=True)

    # Export
    csv_str = table.to_csv(delimiter=";")
    json_str = table.to_json(sort_keys=True)

    # Transpose rows and columns
    transposed = table.transpose()

    # Transform
    selected = table.select("name", "age")
    cleaned = table.drop("internal_id")
    renamed = table.rename_columns({"name": "full_name"})
    sorted_wt = table.sort("age", reverse=True)
    deduped = table.distinct()

    # Query
    cities = table.unique("city")
    count = table.count(age="30")
    first = table.first()
    last = table.last()

    # Export & import (round-trip)
    jsonl = table.to_jsonl()
    restored = TableWrapper.from_csv(table.to_csv())
    restored = TableWrapper.from_json(table.to_json())

    # Iterate, index, and measure
    for row in table:
        print(row["name"])

    table[0]     # {"name": "Alice", "age": "30"}
    table[0:2]   # [{"name": "Alice", "age": "30"}, {"name": "Bob", "age": "25"}]
    len(table)   # 2

    # Compare and check membership
    assert table == wrap(other_table)
    assert {"name": "Alice", "age": "30"} in table

API

wrap(table)

Wrap a behave.model.Table (or any table-like object) with TableWrapper.

TableWrapper

Method Returns Description
as_dicts() list[dict[str, str]] All rows as dicts (copies)
as_models(model) list[Model] Rows as Pydantic v2 or dataclass instances
column(name) list[str] Values for a single column
find_row(**filters) dict[str, str] | None First row matching all filters
find_all_rows(**filters) list[dict[str, str]] All rows matching all filters (copies)
validate_columns(*expected, strict=False) None Raise ColumnMismatchError if columns missing or unexpected
transpose() TableWrapper New wrapper with rows and columns swapped
to_csv(delimiter=",", quoting=QUOTE_MINIMAL) str CSV string with configurable delimiter and quoting
to_json(indent=2, sort_keys=False, default=None) str JSON string (list of objects)
to_jsonl() str JSON Lines string (one object per line)
select(*columns) TableWrapper New wrapper with only the specified columns
drop(*columns) TableWrapper New wrapper without the specified columns
rename_columns(mapping) TableWrapper New wrapper with renamed columns
sort(key, reverse=False) TableWrapper New wrapper with rows sorted by column or callable
unique(column) list[str] Unique values for a column (first-seen order)
distinct() TableWrapper New wrapper with duplicate rows removed
count(**filters) int Count rows matching all filters
first() dict[str, str] | None First row as dict copy, or None if empty
last() dict[str, str] | None Last row as dict copy, or None if empty
from_csv(csv_string, delimiter=",") TableWrapper Classmethod: create wrapper from CSV string
from_json(json_string) TableWrapper Classmethod: create wrapper from JSON string
headers list[str] Column names
__iter__ Iterator[dict] Iterate rows as dicts
__getitem__(i) dict[str, str] | list[dict[str, str]] Row at index as dict, or list of dicts for a slice
__len__ int Number of rows
__eq__ bool Compare by headers and rows
__contains__ bool Check if a row dict is present
__repr__ str Unambiguous representation

ColumnMismatchError

Raised by validate_columns() when expected columns are missing or unexpected columns are found. Subclass of ValueError.

from behave_tables import wrap, ColumnMismatchError

table = wrap(context.table)
try:
    table.validate_columns("name", "email")
except ColumnMismatchError as e:
    print(e.missing)  # ["email"]

# Strict mode: also detects unexpected columns
try:
    table.validate_columns("name", strict=True)
except ColumnMismatchError as e:
    print(e.missing)  # []
    print(e.extra)   # ["age", "email"]

Examples

With dataclasses

from dataclasses import dataclass

@dataclass
class User:
    name: str
    age: int

@then("the users should be")
def step_impl(context):
    users = wrap(context.table).as_models(User)
    assert users[0].name == "Alice"

With Pydantic v2

from pydantic import BaseModel

class User(BaseModel):
    name: str
    age: int

@then("the users should be")
def step_impl(context):
    users = wrap(context.table).as_models(User)
    # Pydantic validates and coerces types automatically
    assert users[0].age == 30  # int, not str

Validate columns

@then("the user list should be")
def step_impl(context):
    table = wrap(context.table)
    table.validate_columns("name", "email", "role")
    # Proceed with confidence that columns exist

# Strict mode: ensure no extra columns
@then("the user list has exactly these columns")
def step_impl(context):
    table = wrap(context.table)
    table.validate_columns("name", "email", strict=True)

Transpose

@then("the data should be")
def step_impl(context):
    table = wrap(context.table)
    transposed = table.transpose()
    # Original headers become the first column ("_column")
    # Original row indices become the new headers ("0", "1", ...)

Find rows

@then("users aged 30 should exist")
def step_impl(context):
    table = wrap(context.table)
    users = table.find_all_rows(age="30")
    assert len(users) == 2

    # Find first match
    alice = table.find_row(name="Alice")

Compare and check membership

@then("the table matches expected data")
def step_impl(context):
    table = wrap(context.table)
    other = wrap(other_table)
    assert table == other

    # Check if a row exists
    assert {"name": "Alice", "age": "30"} in table

Export & import

@then("the report is generated")
def step_impl(context):
    table = wrap(context.table)
    csv_output = table.to_csv(delimiter=";")
    json_output = table.to_json(indent=4, sort_keys=True)
    jsonl_output = table.to_jsonl()  # one JSON object per line

# Round-trip: export then re-import
restored = TableWrapper.from_csv(csv_output)
restored = TableWrapper.from_json(json_output)

Transform

@then("the user names are extracted")
def step_impl(context):
    table = wrap(context.table)
    names_only = table.select("name")
    without_id = table.drop("internal_id")
    renamed = table.rename_columns({"name": "full_name"})
    sorted_by_age = table.sort("age", reverse=True)
    deduped = table.distinct()

Query

@then("the statistics are computed")
def step_impl(context):
    table = wrap(context.table)
    cities = table.unique("city")
    count = table.count(age="30")
    first_user = table.first()
    last_user = table.last()

Design principles

  • Zero dependencies — no required packages. as_models() works with stdlib dataclasses out of the box. Install pydantic optionally for validation and type coercion.
  • Immutable returns — all public methods return copies of internal data. Modifying results never affects the wrapper state.
  • Type-safe — ships with py.typed (PEP 561) for full type checker support (mypy, pyright).
  • Defensive by defaultto_csv() handles missing values and extra keys gracefully. is_pydantic_model() won't crash on non-class input.
  • Protocol-basedTableLike protocol accepts any object with headings and rows, not just behave.model.Table.

Development

make dev            # install with dev dependencies
make test           # run tests
make test-cov       # run tests with coverage
make lint           # check code style
make lint-fix       # auto-fix lint issues
make format         # format code with ruff
make format-check   # verify code is formatted

See CONTRIBUTING.md for details.


License

MIT

Release files for behave-tables 1.3.2

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-tables 1.3.2
File Size Uploaded
behave_tables-1.3.2.tar.gz 23.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for behave-tables 1.3.2
File Interpreter ABI Platform
behave_tables-1.3.2-py3-none-any.whl Python 3 none any Details

Total release size: 37.4 kB

Release files / behave_tables-1.3.2.tar.gz

Download URL behave_tables-1.3.2.tar.gz
Size 23.8 kB
Tags Source
SHA-256 checksum
How to use checksums
a7b73b61ae7342069a8b638020688a027a91695242c4750b07cc17209540aeca
BLAKE2b-256 checksum
How to use checksums
9fcc607a528e8935572de6ee91e6c2a43d176290e3ba17d50a657dbd857e3abe
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 Aug 6, 2026.

Transparency log

Release files / behave_tables-1.3.2-py3-none-any.whl

Download URL behave_tables-1.3.2-py3-none-any.whl
Size 13.6 kB
Tags Python 3
SHA-256 checksum
How to use checksums
762d1ebeca620d3431663c35520359638bc1c69ddbd192d8eb8f5f324c89eed0
BLAKE2b-256 checksum
How to use checksums
779f03e9b608232b8939035866a35b14d191c3328289d0f3c7fb18b235e6ae45
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 Aug 6, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.3.2 This release

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.0

2 release files

1.0.0

2 release files

0.1.0

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