Why Talika?⚓︎
We built Talika because we needed Gherkin data tables to stay simple for authors and still be reliable for test code.
The core package has zero runtime dependencies. You can start with plain Python
schemas, parse ordinary list[list[str]] tables, and add only the optional
extras you need for CLI checks or Pydantic output. At the same time, Talika
gives you the pieces to build your own table language: schema validation,
typed cell parsers, custom cell DSL rules, source-aware errors, variants,
references, table transforms, and output models.
In pytest-bdd, a Gherkin data table reaches the step function as a raw
list[list[str]]. That is simple and flexible, but it means every project has
to decide what the table means:
- which row or column contains labels
- which fields are required
- how strings become
int,bool,Decimal, enums, or lists - what an empty cell means
- whether old labels are still accepted
- what custom words like
random,today, or3 Articlesmean - where to point the user when a value is wrong
For one small table, hand-written parsing is fine. In a real test suite, that glue starts to spread across step definitions.
headers, *rows = datatable
users = []
for row in rows:
raw = dict(zip(headers, row, strict=True))
users.append({
"name": raw["name"],
"age": int(raw["age"]),
"active": raw["active"].lower() == "true",
})
The problem is not the code above. The problem is having many versions of it:
different boolean words, different defaults, different error messages, and
tracebacks that point to Python code instead of the cell in the .feature
file.
The missing layer⚓︎
Cucumber-JVM has a DataTableType layer for converting Gherkin tables into
objects. Python teams using pytest-bdd get the raw table and must build that
layer themselves.
That missing layer is where most of the important table decisions actually live. It is not just "turn rows into dictionaries". A useful table layer should know the expected labels, choose the right orientation, parse cell values, apply defaults, reject unknown or misspelled fields, support project vocabulary, and return errors that point back to the feature file.
Without that layer, these rules usually end up scattered across step definitions:
- one step accepts
yesandno, another acceptstrueandfalse - one table treats an empty cell as
None, another keeps it as"" - one parser supports old labels, another breaks when wording changes
- one failure says which cell is wrong, another only raises
ValueError
Talika fills this gap for Python. It sits between the BDD framework and your test setup code
The idea is deliberately small: keep the feature table readable, move table rules into one reusable contract, and let the rest of the test work with normal Python objects.
Given The following users are present
| name | age | roles | active |
| Akash | 27 | Developer,Manager | True |
| Badal | 25 | Tester,Scrum Master | False |
from talika import RowTable, boolean, field, split
class UserTable(RowTable):# (1)!
name = field("name", required=True) # (2)!
age: int = field("age", required=True) # (3)!
roles = field("roles", parser=split(",")) # (4)!
active = field("active", parser=boolean(), default=True) # (5)!
users = UserTable.parse(datatable) # (6)!
-
Defines a new row-oriented table schema. When parsed, each data row in the Gherkin data table will be validated and converted into a
UserTablerecord instance. -
required=TrueThis column must be present in the table and its cells cannot be empty.
-
Because it is annotated with :
int, talika's metaclass automatically infers and assigns an integer parser to this field. You don't need to explicitly passparser=integer(). -
parser=split(",")Takes the raw string from the table cell and splits it by commas into a list
[str]. By default, it automatically strips whitespace around each item and ignores empty segments. -
parser=boolean()Strictly converts
trueandfalseinto a Boolean, using case-insensitive matching by default. Other vocabularies must be declared explicitly. Unknown values raise a validation error instead of relying on Python's truthiness.default=TrueIf the "active" column is entirely missing from the Gherkin data table, the value for all parsed records will default to True.
-
Takes raw rows (or source-aware
TableData) fromdatatable, matches the headers to your declared fields,runs the specified parsers (like
splitandboolean), and validates everything.Returns a list of validated
UserTablerecord objects.
>> users
[
UserTable(name='Akash', age=27, roles=['Developer', 'Manager'], active=True),
UserTable(name='Badal', age=25, roles=['Tester', 'Scrum Master'], active=False)
]
>> users[0]
UserTable(name='Akash', age=27, roles=['Developer', 'Manager'], active=True)
>> users[0].name
'Akash'
>> users[0].age
27
>> type(users[0].age)
<class 'int'>
>> users[0].roles
['Developer', 'Manager']
Now the table rules live in one reusable contract. The step receives typed records, and bad data fails with the field, row, column, item ID when present, original value, stable error code, and a human-readable hint.
What Talika gives you⚓︎
Talika is useful because the pieces work together:
RowTableandColumnTablefor the two common table shapesfield(),id_field(), aliases, defaults, and empty-cell policies- parser factories for booleans, numbers, choices, lists, and composition
CellDSLfor project-owned cell vocabulary- variants when one table contains different record types
- references between records in the same scenario
- source-aware diagnostics with stable error codes
- optional static checks for
.featurefiles - optional output as dataclasses, Pydantic models, or custom objects
That source-aware part matters. A useful failure should tell the author where to look:
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.
The error points to the authored table, not only to the parser function.
Custom table language⚓︎
Talika does not force one DSL on every team. It gives you safe hooks to create the vocabulary your feature files need.
For example, a compact content table might let authors write 1-3 for three
IDs and 3 Articles to repeat one value across those IDs:
from talika import ColumnGroupExpander, NumericRange, PrefixRepeat
table_transformer = ColumnGroupExpander(
key_row="IDs",
range_rule=NumericRange("-"),
repeat_rule=PrefixRepeat(" "),
)
Talika can expand that into three logical records while preserving the original
source cell. If 3 Articles is wrong, the diagnostic can still point to that
exact cell.
For cell-level vocabulary, use CellDSL:
from talika import CellDSL
cells = CellDSL()
@cells.token("random", fields=("headline",))
def random_headline(context):
return context.user_data["faker"].headline()
@cells.pattern(r"(?P<count>\d+) words", fields=("body",))
def generated_words(match, context):
return context.user_data["faker"].words(int(match["count"]))
Now your tables can use simple words like random or 20 words, and your
project decides exactly what those words mean.
How Talika compares⚓︎
| Tool | What it is good at | What Talika adds |
|---|---|---|
pytest-bdd datatables |
Passing Gherkin table text into Python steps | A schema contract, typed records, validation, source-aware errors, and static checking |
Cucumber-JVM DataTableType |
Converting Cucumber data tables in the JVM ecosystem | A Python table-conversion layer designed around pytest-bdd, source metadata, row and column tables, variants, and table transforms |
| Pydantic | Validating Python data models after the data already has shape | The earlier step: reading a two-dimensional Gherkin table, matching labels, parsing cells, preserving source coordinates, then optionally building Pydantic models |
factory_boy |
Creating test objects and ORM fixtures from Python factories | Parsing human-authored feature tables before fixture creation |
| Scenario Outlines | Running the same scenario with different example values | Structured multi-record data inside one scenario |
| Hand-written dict parsing | Quick one-off conversion | Reusable parsing rules, consistent validation, better diagnostics, and less drift across steps |
These tools can work together. Talika is not trying to replace them.
records = UserTable.parse(datatable)
users = [
UserFactory(**record.as_dict())
for record in records
]
from pydantic import BaseModel
from talika import RowTable, field
class User(BaseModel):
name: str
age: int
class UserTable(RowTable):
output_model = User
name = field("name", required=True)
age: int = field("age", required=True)
users: list[User] = UserTable.parse_as(datatable)
Pydantic validates the model. factory_boy can build the fixture. Talika owns
the table boundary: labels, cells, source locations, table shape, and table
authoring rules.
Where Talika shines⚓︎
Talika is most useful when table data is part of the product language of your tests:
- QA, product, or developers edit
.featurefiles directly. - You want one reusable contract instead of repeated parsing glue.
- You need strict schema validation without giving up readable tables.
- You want a zero-dependency core and optional integrations only when needed.
- You need custom cell syntax such as
random,20 words,today,1-3, or3 Articles. - You want failures to point to the original
.featurecell, even after table transforms. - You have column-oriented tables, variants, local references, or table-level validation.
- CI or editor tooling should check feature tables before the scenario runs.
What Talika is not⚓︎
Talika is intentionally narrow.
It is not a test runner, not a replacement for pytest-bdd, not a fixture
factory, not a business workflow engine, and not a general object validation
library. It also does not force one universal Gherkin table DSL.
Talika owns the mechanics of turning authored tables into useful Python objects. Your project owns the meaning of the table. That is the point: simple tables for humans, strong contracts for code, and enough extension points to grow with your test suite.