Skip to content

Quickstart⚓︎

This quickstart builds a small UserTable contract and uses it to parse a real Gherkin data table with pytest-bdd.

By the end, the table text from a feature file will become Python records with an integer age, a list of roles, a strict boolean flag, source-aware errors, and a clear choice between parse() and parse_as().

The examples assume Talika is already available in your test environment.

Start with a feature table⚓︎

A Gherkin data table is friendly in a feature file:

users.feature
Given the users exist
  | name  | age | roles              | active |
  | Akash | 27  | Developer, Manager | true   |
  | Badal | 25  | Tester             | false  |

Inside the step function, pytest-bdd passes the same table as plain nested strings:

What pytest-bdd passes to the step
datatable = [
    ["name", "age", "roles", "active"],
    ["Akash", "27", "Developer, Manager", "true"],
    ["Badal", "25", "Tester", "false"],
]

That shape is useful because it is simple, but it has no contract. Python does not know that age should become an int, that active should be a strict boolean, or that name is required.

The mental model

Talika sits between the authored table and your test setup code. The feature file stays readable, while the Python side receives records with parsed values and source-aware validation.

Write the first contract⚓︎

Use RowTable when the first row contains field labels and each later row is one record.

users_table.py
from talika import RowTable, boolean, field, split


class UserTable(RowTable):
    name = field("name", required=True)
    age: int = field("age", required=True)
    roles = field("roles", parser=split(","))
    active = field("active", parser=boolean(), default=True)

There are two names to keep in your head:

  • The string passed to field(...) is the table label. It must match the text written in the feature table.
  • The Python attribute name is what your code reads after parsing.

So name = field("name", required=True) means: find a column labelled name, reject the table if that field is missing or empty, then expose the parsed value as record.name.

The same pattern scales to typed fields:

  • age: int lets Talika infer the integer parser from the annotation.
  • roles = field("roles", parser=split(",")) turns one cell into a list.
  • active = field("active", parser=boolean(), default=True) accepts explicit true and false values and supplies True only when the whole active column is absent.

Missing is different from empty

default=True is used when the column is not present in the table. An explicit empty cell is still authored data, so Talika treats it according to the field's empty-cell policy instead of silently replacing it with the default.

Parse it⚓︎

Call UserTable.parse(datatable) at the boundary where your step receives the datatable.

Parse the datatable
users = UserTable.parse(datatable)

assert users[0].name == "Akash"
assert users[0].age == 27
assert users[0].roles == ["Developer", "Manager"]
assert users[0].active is True

Here is the same example as a small file you can run:

users_table.py
from pprint import pprint

from talika import RowTable, boolean, field, split


class UserTable(RowTable):
    name = field("name", required=True)
    age: int = field("age", required=True)
    roles = field("roles", parser=split(","))
    active = field("active", parser=boolean(), default=True)


datatable = [
    ["name", "age", "roles", "active"],
    ["Akash", "27", "Developer, Manager", "true"],
    ["Badal", "25", "Tester", "false"],
]

users = UserTable.parse(datatable)

pprint(users)
print(users[0].as_dict())
print(type(users[0].age))
Run the first table
$ python users_table.py
[UserTable(name='Akash', age=27, roles=['Developer', 'Manager'], active=True),
 UserTable(name='Badal', age=25, roles=['Tester'], active=False)]
{'name': 'Akash', 'age': 27, 'roles': ['Developer', 'Manager'], 'active': True}
<class 'int'>

The important part is not the pretty repr. It is the data boundary:

  • users[0].age is 27, not "27".
  • users[0].roles is ["Developer", "Manager"], not one comma-separated string.
  • users[0].active is True, parsed by a strict boolean parser.
  • users[0].as_dict() gives you a normal dictionary when you want to pass the parsed values into a factory or fixture.

Why strict boolean parsing matters

Python's bool("no") is True, because every non-empty string is truthy. Talika's boolean() parser does not use Python truthiness. By default it accepts only true and false (case-insensitively) and rejects anything else. If yes/no is part of your domain language, declare it explicitly with true_values and false_values.

Defaults are for absent fields⚓︎

If the feature table does not mention active at all, the schema default is used:

Missing optional column
minimal_datatable = [
    ["name", "age", "roles"],
    ["Chinmay", "30", "Developer"],
]

users = UserTable.parse(minimal_datatable)

assert users[0].active is True

That makes optional columns useful for feature files. Authors can keep simple tables short, while the schema still gives Python a complete value.

Parse records or convert them⚓︎

parse() has one predictable return shape: schema records. Configuring an output model does not change that return type.

Public output versus Talika records
from dataclasses import dataclass

from talika import RowTable, field


@dataclass(frozen=True)
class User:
    name: str
    age: int


class UserTable(RowTable):
    output_model = User

    name = field("name", required=True)
    age: int = field("age", required=True)


datatable = [
    ["name", "age"],
    ["Akash", "27"],
]

records = UserTable.parse(datatable)
users = UserTable.parse_as(datatable)

assert isinstance(records[0], UserTable)
assert records[0].as_dict() == {"name": "Akash", "age": 27}
assert users == [User(name="Akash", age=27)]

Use parse() when the step wants Talika records. Records keep as_dict() and source metadata. Use parse_as() when the step wants a dataclass, Pydantic model, or custom output object. With no argument, it uses the schema's configured output_model or build_output() hook.

Inspecting source metadata
record = records[0]
name_cell = record.source_for("name")

assert name_cell.source_row == 2
assert name_cell.source_column == 1
assert name_cell.source_value == "Akash"

A practical rule

Start with parse(). Add parse_as() only at a boundary that needs a project model. This keeps parsing and object construction visible in code.

Use the contract in pytest-bdd⚓︎

In a real test, the schema usually lives beside the step module or in a small tables.py module shared by related steps.

test_users.py
from pytest_bdd import given


@given("the users exist", target_fixture="users")
def users(datatable):
    return UserTable.parse(datatable)

That is the whole integration. pytest-bdd still owns scenarios and step matching. Talika only owns the table boundary.

If you like a consistent parsing facade in pytest code, Talika also provides a talika fixture:

The talika fixture
@given("the users exist", target_fixture="users")
def users(datatable, talika):
    return talika.parse(datatable, schema=UserTable)

Both examples call the same schema parser. Pick the style that keeps your test suite easiest to read.

See a useful failure⚓︎

Now change the authored table so three cells are wrong:

Bad input
bad_datatable = [
    ["name", "age", "roles", "active"],
    ["", "old", "Developer", "maybe"],
]

UserTable.parse(bad_datatable, error_mode="collect")

error_mode="collect" asks Talika to report independent problems in one pass. That is useful while writing feature files because the author can fix several cells before running the scenario again.

Collected table errors
$ python users_table.py
Table contains 3 errors:
  1. Required field has an empty value (code=empty_required, schema=UserTable, field='name', row=2, column=1, value=''). Hint: Fill the cell, or remove required=True if an explicit empty value should be valid.
  2. Field parser failed: invalid literal for int() with base 10: 'old' (code=parser_failed, schema=UserTable, field='age', row=2, column=2, value='old'). Hint: Check the cell value or adjust the field parser for this syntax.
  3. Field parser failed: Expected one of ['false', 'true'] (code=parser_failed, schema=UserTable, field='active', row=2, column=4, value='maybe'). Hint: Check the cell value or adjust the field parser for this syntax.

Read one error from left to right:

  • code=empty_required is stable enough for tooling.
  • schema=UserTable tells you which contract rejected the table.
  • field='name' tells you which table field failed.
  • row=2, column=1 points back to the authored cell.
  • value='' preserves the original bad value.
  • the hint tells the author what kind of fix makes sense.

That is the main point of Talika: your step code stops hand-parsing strings, and table authors get failures that point to the table they wrote.