pytest-bdd⚓︎
Talika does not replace pytest-bdd. It parses the datatable that
pytest-bdd already passes to a step function.
That keeps the integration simple:
- the feature file owns the authored Gherkin table
pytest-bddpasses that table to the step asdatatable- Talika parses the datatable through your schema
- the rest of the test uses parsed records or output objects
Feature: User setup
Scenario: Create users from a table
Given the users exist:
| username | age |
| alice | 34 |
Then alice can sign in
Parse at the boundary
Parse the datatable in the step that receives it. After that point, pass parsed users, content records, or domain objects through fixtures and test helpers.
Define the Schema Outside the Step⚓︎
Keep the schema as ordinary Python code. The schema should be importable by pytest, by helper modules, and by tooling that wants to inspect or validate feature tables.
from talika import RowTable, field, integer
class UserTable(RowTable):
username = field("username", required=True)
age = field("age", parser=integer())
role = field("role", default="viewer")
This schema is not tied to pytest-bdd. It can parse any compatible datatable,
including the one received by a BDD step.
datatable = [
["username", "age"],
["alice", "34"],
]
users = UserTable.parse(datatable)
The same schema can be used in unit tests, fixture setup, static checking, or
plain helper functions. pytest-bdd is only one place where the datatable
originates.
Do not put schema declarations inside step functions
Step functions should describe test flow. Schema classes describe table contracts. Keeping them separate makes the schema reusable and easier to inspect.
Parse Directly in a Step⚓︎
The simplest integration is to call UserTable.parse(datatable) in the step
that receives the table.
from pytest_bdd import given, scenario, then
@scenario("users.feature", "Create users from a table")
def test_create_users():
pass
@given("the users exist:", target_fixture="users")
def users(datatable):
return UserTable.parse(datatable)
@then("alice can sign in")
def alice_can_sign_in(users):
assert users[0].username == "alice"
assert users[0].age == 34
assert users[0].role == "viewer"
The target_fixture="users" argument makes the parsed result available to
later steps as the users fixture.
>> users[0]
UserTable(username='alice', age=34, role='viewer')
>> users[0].as_dict()
{'username': 'alice', 'age': 34, 'role': 'viewer'}
This style is direct and explicit. It is often the clearest choice when the step uses one schema and there is no need for an additional parser facade.
Keep raw datatables short-lived
A raw datatable is just nested strings. Parse it before handing data to the rest of the test so later code works with meaningful records.
Use the talika Fixture⚓︎
Talika registers a talika pytest fixture through the package's pytest plugin.
The fixture is a small facade around schema methods.
@given("the users exist:", target_fixture="users")
def users(datatable, talika):
return talika.parse(datatable, schema=UserTable)
This does the same parsing work as:
The fixture does not create another schema registry or another parsing
lifecycle. It simply delegates to the schema passed with schema=....
During a pytest-bdd step, the plugin binds the feature's absolute filename and
Gherkin cell coordinates to the exact raw datatable object. Calling
talika.parse(...), talika.parse_as(...), or talika.validate(...)
upgrades that object to source-aware TableData, then clears the binding after
the step succeeds or fails. Each fixture instance keeps its own binding, so
parallel tests do not share provenance.
Direct schema calls cannot infer a feature filename
UserTable.parse(datatable) still parses correctly, but a raw nested list
carries no pytest-bdd metadata. Use the talika fixture when diagnostics
should automatically include the feature URI and absolute Gherkin cell
coordinates. Scenario-outline runtime locations point to pytest-bdd's
rendered template cell.
Use the fixture for dependency-injection style
Some teams prefer step functions where parsing always flows through a
fixture argument. Use talika.parse(...) when that style makes your BDD
steps more consistent.
Pass Parse Context from Steps⚓︎
Step functions are a natural place to pass scenario-specific project data into Talika.
def parse_username(value, context):
return context.user_data["prefix"] + value
class ContextUserTable(RowTable):
username = field("username", parser=parse_username)
@given("the imported users exist:", target_fixture="users")
def imported_users(datatable, talika):
return talika.parse(
datatable,
schema=ContextUserTable,
context={"prefix": "QA-"},
)
The context mapping becomes context.user_data for parsers, default factories,
record validators, table validators, and output builders.
>> users[0]
ContextUserTable(username='QA-alice')
>> users[0].as_dict()
{'username': 'QA-alice'}
Use this for values that belong to the current scenario or test setup: configured domains, allowed roles, generated prefixes, service handles, or fixture-provided data.
Context is copied into read-only user_data
Parser and validation code can read context.user_data, but should not
mutate it. Treat it as parse-time configuration.
Shared Parser Context Fixtures⚓︎
Instead of defining parse-time context inside every step function, use ordinary pytest fixtures for shared configuration.
This is a good fit for data that belongs to the test environment rather than to one table: prefixes, allowed roles, fake service handles, validation thresholds, or project lookup data. The step still decides when parsing happens, but the fixture owns how the shared context is assembled.
import pytest
@pytest.fixture
def parser_context():
return {"prefix": "DEV-"}
@given("the dev users exist:", target_fixture="users")
def dev_users(datatable, talika, parser_context):
return talika.parse(
datatable,
schema=ContextUserTable,
context=parser_context,
)
This pattern keeps step definitions focused on the authored table. It also prevents small configuration dictionaries from being copied across many steps, where they become hard to update consistently.
Fixture context is still parse context
The fixture does not create a separate Talika mechanism. It simply returns
a mapping that is passed as context=..., then read through
context.user_data by parsers and validators.
Choose parse or parse_as⚓︎
Both schema methods and the talika fixture expose two clear forms.
Use parse(...) when the step should receive Talika schema records.
Use parse_as(...) when the step should receive a dataclass, Pydantic model,
or custom output object. Pass a callable explicitly, or omit it to use a
configured output_model or build_output().
@given("the user records exist:", target_fixture="user_records")
def user_records(datatable, talika):
return talika.parse(datatable, schema=UserTable)
The difference matters when a schema uses output conversion:
from dataclasses import dataclass
@dataclass(frozen=True)
class User:
username: str
age: int
class UserOutputTable(RowTable):
output_model = User
username = field("username", required=True)
age = field("age", parser=integer())
records = UserOutputTable.parse(datatable)
public_users = UserOutputTable.parse_as(datatable)
assert public_users == [User(username="alice", age=34)]
assert isinstance(records[0], UserOutputTable)
>> public_users
[User(username='alice', age=34)]
>> records[0]
UserOutputTable(username='alice', age=34)
Use records when you need source metadata
Output objects are often plain dataclasses or domain models. They may not
carry Talika metadata. Use parse(...) when a step needs
table_source, source_for(...), or intermediate schema fields.
Validate Without Raising⚓︎
Use the fixture's validate(...) method when a step or helper needs structured
diagnostics instead of an exception:
Validation returns schema records only when the complete table is valid and always skips output conversion. It preserves the same automatic pytest-bdd source URI and cell coordinates as the fixture's parsing methods.
Use the Functional API When Preferred⚓︎
Some codebases prefer functions over schema classmethod calls. Talika provides functional helpers for that style.
from talika import parse_table, parse_table_as
records = parse_table(UserTable, datatable)
values = parse_table_as(UserTable, datatable, dict)
assert records[0].username == "alice"
assert values[0]["username"] == "alice"
assert records[0].as_dict() == {
"username": "alice",
"age": 34,
"role": "viewer",
}
These helpers delegate to the same schema lifecycle:
parse_table(UserTable, datatable)is equivalent toUserTable.parse(datatable)parse_table_as(UserTable, datatable, User)is equivalent toUserTable.parse_as(datatable, User)validate_table(UserTable, datatable)is equivalent toUserTable.validate(datatable)
They forward context=... and error_mode=... just like the schema methods.
One lifecycle
Direct schema parsing, the talika fixture, and the functional API all
call the same schema parser. Choose the calling style that makes the test
easiest to read.
Collect Multiple Table Errors⚓︎
In normal mode, parsing stops at the first error. In error_mode="collect",
Talika can report multiple independent table errors from the same parsing
phase.
class StrictUserTable(RowTable):
username = field("username", required=True)
age = field("age", parser=integer())
@given("the strict users exist:", target_fixture="users")
def strict_users(datatable, talika):
return talika.parse(
datatable,
schema=StrictUserTable,
error_mode="collect",
)
The same behavior is available through direct schema parsing:
StrictUserTable.parse(
[
["username", "age"],
["", "old"],
],
error_mode="collect",
)
Table contains 2 errors:
1. Required field has an empty value
(code=empty_required, schema=StrictUserTable, field='username',
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=StrictUserTable, field='age',
row=2, column=2, value='old').
Hint: Check the cell value or adjust the field parser for this syntax.
Collect mode is useful in tooling-like tests or CI checks where you want to see several table problems at once. For ordinary test setup, fail-fast parsing is often simpler.
Collect mode still respects lifecycle boundaries
Talika collects compatible errors from the same phase. It does not keep running dependent validation layers after structural parsing has already made the records unreliable.
Keep Step Functions Focused⚓︎
A good pytest-bdd step should usually do one of these things:
- receive a datatable and parse it into a fixture
- pass scenario context into parsing
- assert behavior using parsed records or output objects
- call application setup using parsed data
Avoid repeating table parsing logic across many steps. Once a schema exists, let it own labels, parsers, defaults, validation, output conversion, and source diagnostics.
Keep feature text and setup code close
The feature file should stay readable to the test author. The step should make the boundary clear: authored table in, dependable test objects out.