Row Tables⚓︎
Use RowTable when the first row of the datatable contains labels and each
later row describes one record.
This is the most common shape for user lists, permissions, account states, small product records, and other examples where the reader naturally scans one record from left to right.
Given the users exist
| username | email | active |
| alice | alice@example.com | true |
| bob | bob@example.com | false |
In this table, the first row names the fields. Every later row is one user. The schema you write should describe the labels, the required values, and the cell parsers for that shape.
When to choose a row table
Choose a row table when each item has a small number of fields and the table is easiest to read horizontally. If each item has many fields or many optional values, a column-oriented table may be easier for authors to scan.
Define the Row Contract⚓︎
A row table contract is a normal Python class that inherits from RowTable.
Each field(...) declaration maps a table label to a Python attribute.
from talika import RowTable, boolean, field
class UserTable(RowTable):
username = field("username", required=True)
email = field("email", required=True)
active = field("active", parser=boolean(), required=True)
The class says:
usernamemust appear in the header row and each cell must be non-empty.emailmust also appear and be non-empty.activemust appear, be non-empty, and parse throughboolean().
Talika does not infer Boolean meaning merely because a word looks familiar to
humans. The default boolean() parser gives true and false meaning. Other
vocabularies, such as yes/no, must be configured explicitly on that parser.
Labels and attributes
The value passed to field("...") is the label in the authored table. The
attribute name on the class is the name your Python code reads after
parsing. Keeping those two names separate lets feature files use readable
wording while test code keeps normal Python attributes.
Parse Records from the Datatable⚓︎
pytest-bdd passes the table to the step as nested strings. You can also write
that shape directly while learning or testing a schema:
datatable = [
["username", "email", "active"],
["alice", "alice@example.com", "true"],
["bob", "bob@example.com", "false"],
]
Call parse() at the point where your test setup receives the table:
users = UserTable.parse(datatable)
assert users[0].username == "alice"
assert users[0].email == "alice@example.com"
assert users[0].active is True
assert users[1].active is False
The parsed result is a list. Each item is a record with the declared fields as attributes.
>> users[0]
UserTable(username='alice', email='alice@example.com', active=True)
>> users[0].as_dict()
{'username': 'alice', 'email': 'alice@example.com', 'active': True}
That record is intentionally small. It holds parsed values, supports
as_dict(), and carries source metadata for diagnostics and custom validation.
The Parsed Record Collection and Object⚓︎
When you call parse(), Talika returns a standard Python list containing parsed schema records. You can use standard Python collection methods on the result:
- Check the count:
len(users) - Retrieve by index:
users[0] - Iterate:
[u.username for u in users] - Slice:
users[1:]
Each record object provides attributes matching your declared schema fields, plus helper methods and metadata properties:
as_dict(): Returns a clean Pythondictcontaining only your declared schema attributes and their parsed values. It intentionally excludes metadata and extras, making it perfect for unpacking (e.g.User(**record.as_dict())) into domain models.table_source: An immutableRecordSourceobject storing the record's source location (like the 1-basedrowindex in the feature table).source_for(field_name): Returns aTableCellobject representing the source cell for a specific field. You can read its properties:value: The current parsed/transformed string value.source_row: The 1-based row number in the feature file.source_column: The 1-based column number in the feature file.source_value: The raw string value before any transformation.
# Access row metadata
row_number = users[0].table_source.row
assert row_number == 2
# Access a specific cell's source metadata
cell = users[0].source_for("email")
assert cell.value == "alice@example.com"
assert cell.source_row == 2
assert cell.source_column == 2
assert cell.source_value == "alice@example.com"
>> users[0].table_source.row
2
>> cell = users[0].source_for("email")
>> cell.source_column
2
Required Fields⚓︎
A required field has two rules:
- The label must be present in the header row.
- The cell for each record must not be empty.
If the table omits a required label, parsing fails before any row can be used:
Required field is missing from the table
(code=missing_required, schema=UserTable, field='active').
Hint: Add this field to the table, or make the schema field optional if the project should supply it.
If the label is present but a row leaves the cell blank, the error points to the specific authored cell:
UserTable.parse(
[
["username", "email", "active"],
["", "alice@example.com", "true"],
]
)
Required field has an empty value
(code=empty_required, schema=UserTable, field='username',
row=2, column=1, value='').
Hint: Fill the cell, or remove required=True if an explicit empty value should be valid.
Missing and empty are different
A missing label is a table-shape problem. An empty cell is a value problem in one record. Talika reports them differently because the author fixes them in different places.
Optional Fields and Defaults⚓︎
Not every field needs to be written in every feature table. Optional fields can
use a static default or a default_factory.
def default_team(context):
return context.user_data["team"]
class UserWithDefaults(RowTable):
username = field("username", required=True)
role = field("role", default="viewer")
team = field("team", default_factory=default_team)
When the entire field is absent from the header row, Talika supplies the default:
users = UserWithDefaults.parse(
[
["username"],
["alice"],
],
context={"team": "platform"},
)
assert users[0].role == "viewer"
assert users[0].team == "platform"
default_factory receives a context object. Use it when the missing value
depends on project data passed to parse(..., context={...}), or on an item ID
declared for the row.
An explicit empty cell is not the same as an absent field:
users = UserWithDefaults.parse(
[
["username", "role"],
["alice", ""],
],
context={"team": "platform"},
)
assert users[0].role == ""
The field exists in the table, so the static default is not used. This is important when feature authors intentionally write an empty value, or when a blank cell should fail under a stricter empty-cell policy.
Default factories are for missing fields
A default factory is a way to fill omitted optional data. It is not a general fallback for bad or blank cells. If an empty cell has meaning, make that policy explicit on the field.
Row IDs⚓︎
A row-oriented schema may declare an id_field(...). This is optional, but it
is useful when parsers, default factories, diagnostics, or later validation
need a stable item identifier.
Row schemas allow zero or one id_field. Multiple ID declarations fail while
the schema class is created. Parsed IDs must be hashable and unique across the
table; typed duplicates such as 1 and 01 are duplicates when the ID parser
converts both to integer 1.
Use TableFields for an incomplete reusable group of declarations. Do not
create an incomplete row or column schema solely to add an ID in a later
subclass.
from talika import id_field
seen_item_ids = []
def parse_display_name(value, context):
seen_item_ids.append(context.item_id)
return value.strip().title()
def default_audit_name(context):
return f"audit-{context.item_id}"
class UserWithId(RowTable):
display_name = field("display name", parser=parse_display_name)
user_id = id_field("user id")
audit_name = field("audit name", default_factory=default_audit_name)
The ID field is parsed before the other row fields, so its value is available through parser and default-factory context.
users = UserWithId.parse(
[
["display name", "user id"],
["alice rao", "U-100"],
]
)
assert seen_item_ids == ["U-100"]
assert users[0].display_name == "Alice Rao"
assert users[0].audit_name == "audit-U-100"
assert users[0].table_source.item_id == "U-100"
In this example, parse_display_name() sees context.item_id, and
default_audit_name() uses the same ID to create a missing audit value.
parse() always returns the Talika record object with its table_source
metadata. Use parse_as() when a caller explicitly needs an output model.
Row IDs in Diagnostics⚓︎
When a row has an ID field, errors can include item_id. That makes failures
easier to locate in larger tables, especially when rows are sorted or filtered
by a test helper.
def parse_status(value, context):
raise ValueError("unsupported status")
class StatusTable(RowTable):
status = field("status", parser=parse_status)
user_id = id_field("user id")
StatusTable.parse(
[
["status", "user id"],
["blocked", "U-500"],
]
)
Field parser failed: unsupported status
(code=parser_failed, schema=StatusTable, field='status',
row=2, column=1, item_id='U-500', value='blocked').
Hint: Check the cell value or adjust the field parser for this syntax.
The row and column still point to the authored cell. The item ID adds a stable record identity that can appear in logs, CI output, or editor diagnostics.
Rectangular Tables⚓︎
Every data row must have the same number of cells as the header row. This is easy to miss when editing feature files by hand.
Ragged row: expected 3 cells, got 2
(code=ragged_row, schema=UserTable, row=2).
Hint: Make every data row contain the same number of cells as the header row.
Talika reports ragged rows as table-shape errors because the parser cannot know which missing cell belongs to which label.
Keep row tables small
Row tables work best when each record has a compact set of fields. If the table becomes wide, hard to scan, or full of blank cells, the problem may be table shape rather than parsing.