Skip to content

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.

A row-oriented feature table
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.

users_table.py
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:

  • username must appear in the header row and each cell must be non-empty.
  • email must also appear and be non-empty.
  • active must appear, be non-empty, and parse through boolean().

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:

The datatable shape
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:

Parse the users
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.

Parsed row record
>> 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 Python dict containing 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 immutable RecordSource object storing the record's source location (like the 1-based row index in the feature table).
  • source_for(field_name): Returns a TableCell object 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.
Accessing record and cell metadata
# 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"
Source metadata lookup
>> users[0].table_source.row
2

>> cell = users[0].source_for("email")
>> cell.source_column
2

Required Fields⚓︎

A required field has two rules:

  1. The label must be present in the header row.
  2. The cell for each record must not be empty.

If the table omits a required label, parsing fails before any row can be used:

Missing required label
UserTable.parse(
    [
        ["username", "email"],
        ["alice", "alice@example.com"],
    ]
)
Missing required label error
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:

Empty required cell
UserTable.parse(
    [
        ["username", "email", "active"],
        ["", "alice@example.com", "true"],
    ]
)
Empty required cell error
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.

Defaults for optional row fields
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:

Parsing a shorter table
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:

An empty cell is still present
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.

A row table with item IDs
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.

Using the row ID in 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.

A parser failure on an identified row
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"],
    ]
)
Diagnostic with item_id
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.

A ragged row
UserTable.parse(
    [
        ["username", "email", "active"],
        ["alice", "alice@example.com"],
    ]
)
Ragged row error
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.