Skip to content

Column Tables⚓︎

Use ColumnTable when the first column contains labels and each later column describes one record.

This shape is useful when each item has several fields, when a record reads more naturally as a vertical card, or when row tables become too wide to scan. CMS content examples often fit this shape: an article, poll, or page section may have many properties, and each item is easier to read from top to bottom.

A column-oriented feature table
Given the content exists
  | IDs      | A-1          | P-1             |
  | Type     | Article      | Poll            |
  | Headline | Market brief | Reader question |
  | Status   | draft        | published       |

In this table, A-1 and P-1 are the records. Type, Headline, and Status are fields on each record.

When to choose a column table

Choose a column table when each item has enough fields that a row table would become wide or full of blanks. The first column becomes the list of labels, and each item gets its own vertical column.

Define the Column Contract⚓︎

A column table must declare exactly one id_field(...). That field identifies each item column and must be the first row in the table.

content_table.py
from talika import ColumnTable, field, id_field


class ContentTable(ColumnTable):
    id = id_field("IDs")
    content_type = field("Type", required=True)
    headline = field("Headline", required=True)
    status = field("Status")

The contract says:

  • id maps the ID row IDs to the Python attribute id. Every item column must have a unique ID in that row.
  • content_type maps the required table row Type to the attribute content_type.
  • headline maps the required table row Headline to the attribute headline.
  • status maps the optional table row Status to the attribute status, which returns None when the whole row is absent.

ColumnTable requires an ID row

A row table may choose whether to declare an ID field. A column table must declare one, because the parser needs a stable identity for each item column.

Exactly-one cardinality is checked when the schema class is created. Put incomplete reusable declarations on TableFields, then combine that component with a concrete ColumnTable that declares its single ID.

Parse Item Columns⚓︎

The datatable shape mirrors the feature table. The first cell of each row is a label. Each later cell belongs to the item column above it.

The datatable shape
datatable = [
    ["IDs", "A-1", "P-1"],
    ["Type", "Article", "Poll"],
    ["Headline", "Market brief", "Reader question"],
    ["Status", "draft", "published"],
]

Call parse() with the datatable:

Parse the content records
items = ContentTable.parse(datatable)

assert [item.id for item in items] == ["A-1", "P-1"]
assert items[0].content_type == "Article"
assert items[0].headline == "Market brief"
assert items[1].content_type == "Poll"
assert items[1].status == "published"

The parsed result is ordered by item column. The first output record belongs to A-1, and the second belongs to P-1.

Parsed column record
>> items[0]
ContentTable(id='A-1', content_type='Article', headline='Market brief', status='draft')

>> items[0].as_dict()
{'id': 'A-1', 'content_type': 'Article', 'headline': 'Market brief', 'status': 'draft'}

>> items[0].table_source.item_id
'A-1'

The item ID is both a normal parsed field (item.id) and source metadata (item.table_source.item_id). The normal field is useful in test setup. The source metadata is useful when validation or diagnostics need to identify the authored item.

Item Lookup and Column Metadata⚓︎

Column tables still return a sequence of parsed records, so index-based access works. The difference is what the index means. In a row table, record position usually points to a source row. In a column table, record position points to an item column.

Most tests become clearer if they do not rely on positional access for long. Parse the table, then build a small lookup by item ID for assertions and setup code.

  • table_source.column stores the one-based source column for the parsed item.
  • table_source.row is not populated for the record itself because the item runs vertically through several rows.
  • source_for("field_name") still points to the exact row and column for one field value.
Item ID lookup and column coordinates
# Create a fast lookup dict mapping ID to the parsed item
items_by_id = {item.id: item for item in items}
assert items_by_id["A-1"].content_type == "Article"

# Access column metadata
# Unlike row tables (which store .row), column records store .column (1-based index)
col_number = items[0].table_source.column
assert col_number == 2

Use IDs after parsing

Column tables are often authored by ID. Building by_id = {item.id: item} makes later assertions read like the feature file instead of like a column index calculation.

Missing Optional Rows⚓︎

When an optional field row is absent, Talika gives that field None unless the field declares a default.

The Status row is absent
items = ContentTable.parse(
    [
        ["IDs", "A-1", "P-1"],
        ["Type", "Article", "Poll"],
        ["Headline", "Market brief", "Reader question"],
    ]
)

assert [item.status for item in items] == [None, None]

This is different from an explicit empty cell. In the next table, the Status row exists, and A-1 intentionally has a blank status cell:

The Status cell is empty
items = ContentTable.parse(
    [
        ["IDs", "A-1", "P-1"],
        ["Type", "Article", "Poll"],
        ["Headline", "Market brief", "Reader question"],
        ["Status", "", "published"],
    ]
)

assert [item.status for item in items] == ["", "published"]

Absent row and empty cell are different

An absent optional row means the field was not provided for any item. An empty cell means the field was provided, but one item has a blank value. Those two cases are often handled differently in real feature files.

Missing and Empty Required Values⚓︎

A required field row must be present, and every item column must have a non-empty value for that field.

If the table omits a required row, the error includes the item ID because the missing value affects a specific item column:

Missing required row
ContentTable.parse(
    [
        ["IDs", "A-1"],
        ["Type", "Article"],
    ]
)
Missing required row error
Required field is missing from the table 
(code=missing_required, schema=ContentTable, field='Headline', 
item_id='A-1'). 
Hint: Add this field to the table, or make the schema field optional if the project should supply it.

If the row exists but an item leaves the required cell empty, the error points to the exact row and column:

Empty required cell
ContentTable.parse(
    [
        ["IDs", "A-1", "P-1"],
        ["Type", "Article", "Poll"],
        ["Headline", "Market brief", ""],
    ]
)
Empty required cell error
Required field has an empty value 
(code=empty_required, schema=ContentTable, field='Headline', 
row=3, column=3, item_id='P-1', value=''). 
Hint: Fill the cell, or remove required=True if an explicit empty value should be valid.

This is one of the main benefits of preserving table source information. The author can go directly to the blank cell instead of searching through parsing code.

The First Row Must Be the ID Row⚓︎

For column tables, the first row controls item identity. The first cell of that row must match the declared id_field(...) label or one of its aliases.

Wrong first row
ContentTable.parse(
    [
        ["Type", "Article"],
        ["IDs", "A-1"],
        ["Headline", "Market brief"],
    ]
)
Wrong first row error
The first row must be the declared id field 
(code=table_error, schema=ContentTable, field='IDs', 
row=1, column=1, value='Type'). 
Hint: Move the declared id_field label into the first cell.

Talika rejects this table before parsing records because it cannot safely know which values are item IDs.

The ID row is structural

The ID row is not just another optional field. It defines the item columns. Move it carefully when editing column-oriented feature files.

Duplicate Item IDs⚓︎

Every item column needs a unique ID. Duplicate IDs make references, defaults, diagnostics, and later validation ambiguous.

Uniqueness is checked after ID parsing, and IDs must be hashable. Consequently 1 and 01 collide when an integer parser converts both to 1; a parser that returns a list fails with invalid_id at the authored ID cell.

Duplicate item ID
ContentTable.parse(
    [
        ["IDs", "A-1", "A-1"],
        ["Type", "Article", "Poll"],
        ["Headline", "Market brief", "Duplicate poll"],
    ]
)
Duplicate item ID error
Duplicate item ID 
(code=duplicate_id, schema=ContentTable, field='IDs', 
row=1, column=3, item_id='A-1', value='A-1'). 
Hint: Use one unique item ID per parsed column.

The diagnostic points to the second occurrence. That is the cell the author needs to change.

Rectangular Tables⚓︎

Every row in a column table must have the same number of cells as the ID row. If the ID row defines two item columns, every other row must also provide two item cells.

A ragged column table
ContentTable.parse(
    [
        ["IDs", "A-1", "P-1"],
        ["Type", "Article"],
        ["Headline", "Market brief", "Reader question"],
    ]
)
Ragged row error
Ragged row: expected 3 cells, got 2 
(code=ragged_row, schema=ContentTable, row=2). 
Hint: Make every table row contain the same number of cells as the ID row.

Talika reports this as a table-shape error. Until the row is rectangular, the parser cannot reliably know which item a missing cell belongs to.

Keep column tables readable

Column tables are best when each item is worth reading vertically. If the table only has two or three simple fields, a row table may be shorter and easier to compare.