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:
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:
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.
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: intlets 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 explicittrueandfalsevalues and suppliesTrueonly when the wholeactivecolumn 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.
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:
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))
$ 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].ageis27, not"27".users[0].rolesis["Developer", "Manager"], not one comma-separated string.users[0].activeisTrue, 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:
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.
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.
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.
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:
@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_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.
$ 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_requiredis stable enough for tooling.schema=UserTabletells you which contract rejected the table.field='name'tells you which table field failed.row=2, column=1points 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.