Skip to content

Collect Errors⚓︎

By default, Talika fails fast. The first table problem raises one TableError, and parsing stops there.

That default is useful for ordinary test setup because one clear failure is usually enough. But when a table is authored by several people, generated by a tool, or checked in CI, stopping at the first problem can create a slow edit-run loop. The author fixes one cell, runs the test again, sees the next cell, and repeats the same cycle.

Collect mode changes that feedback loop. It still fails the parse, but it raises TableErrors, an aggregate that contains every independent diagnostic Talika could safely collect from the current parsing phase.

A table with several independent mistakes
Feature: Imported users

  Scenario: Review an authored users table
    Given the imported users:
      | username | age | role  |
      |          | old | admin |
      | sam      | 41  |       |
    Then the table diagnostics should be clear

Use collect mode for review-style feedback

Collect mode is most useful when the reader wants a list of table fixes: CI checks, editor diagnostics, static table validation, generated feature reviews, and larger setup tables. For normal test execution, fail-fast mode is often easier to read.

Start with a Normal Schema⚓︎

Collect mode does not require a special schema. Use the same RowTable or ColumnTable declaration you would use for ordinary parsing.

A user import schema
from talika import RowTable, TableErrors, field, integer


class ImportedUserTable(RowTable):
    username = field("username", required=True)
    age = field("age", parser=integer())
    role = field("role", required=True)

This schema expects three fields:

  • username must be present and non-empty
  • age must parse as an integer
  • role must be present and non-empty

The authored table below violates all three rules, but in different cells.

A datatable with several problems
bad_users = [
    ["username", "age", "role"],
    ["", "old", "admin"],
    ["sam", "41", ""],
]

The important point is that these mistakes are independent. The empty username does not prevent Talika from also checking the age cell in the same row, and the blank role in the next row can be checked separately.

Collection is not a different contract

The schema still owns the same labels, parsers, defaults, empty-cell policy, validators, references, and optional parse_as() conversion. error_mode changes how failures are reported, not what the table means.

Compare Fail-Fast and Collect Mode⚓︎

With the default mode, Talika raises the first error it finds:

Default fail-fast parsing
ImportedUserTable.parse(bad_users)
First diagnostic
Required field has an empty value (code=empty_required, schema=ImportedUserTable, field='username', row=2, column=1, value=''). Hint: Fill the cell, or remove required=True if an explicit empty value should be valid.

This is precise, but it only tells the author about row 2, column 1. The invalid age and blank role remain hidden until the first problem is fixed.

To ask Talika for all safe diagnostics from the same parsing phase, pass error_mode="collect":

Collect mode parsing
ImportedUserTable.parse(bad_users, error_mode="collect")
Collected diagnostics
Table contains 3 errors:
  1. Required field has an empty value (code=empty_required, schema=ImportedUserTable, 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=ImportedUserTable, field='age', row=2, column=2, value='old'). Hint: Check the cell value or adjust the field parser for this syntax.
  3. Required field has an empty value (code=empty_required, schema=ImportedUserTable, field='role', row=3, column=3, value=''). Hint: Fill the cell, or remove required=True if an explicit empty value should be valid.

The parse still fails. Talika does not return partially valid records when errors were collected. The difference is that the exception now contains a numbered list of structured TableError objects.

Collect mode is not best-effort parsing

Do not use collect mode when the caller expects usable output despite bad input. If any error-severity diagnostic is collected, parsing raises TableErrors and no result is returned. Warnings alone retain the records.

Inspect the Aggregate⚓︎

TableErrors is intentionally small. It contains an immutable errors tuple, supports len(...), and can be iterated directly.

Reading structured diagnostics
collected = None

try:
    ImportedUserTable.parse(bad_users, error_mode="collect")
except TableErrors as exc:
    collected = exc

if collected is not None:
    diagnostics = [
        {
            "code": error.code,
            "field": error.field,
            "row": error.row,
            "column": error.column,
            "value": error.value,
        }
        for error in collected
    ]
Inspecting collected errors
>> len(collected)
3

>> diagnostics[0]
{'code': 'empty_required', 'field': 'username', 'row': 2, 'column': 1, 'value': ''}

>> diagnostics[1]
{'code': 'parser_failed', 'field': 'age', 'row': 2, 'column': 2, 'value': 'old'}

For tools, prefer the structured attributes over parsing the exception string. Each contained TableError can carry:

  • code, such as empty_required, parser_failed, or record_validation_failed
  • schema, the schema display name
  • field, the schema field involved in the failure
  • row and column, using one-based source coordinates when known
  • item_id, for column-shaped records or ID-aware rows
  • value, the authored value that caused the failure
  • hint, when Talika can suggest a likely fix

The aggregate message is meant for humans. The attributes are meant for test runners, command-line tools, editor integrations, and custom reporting.

TableErrorCode exposes the same supported diagnostic codes as constants. It is useful when a tool wants to compare against named values instead of spelling string literals throughout the codebase.

Every TableError also exposes its immutable diagnostic. The aggregate's diagnostics property returns those values in the same order as errors.

Render diagnostics from attributes

If you are building a checker, group or color errors by code, point to source using row and column, and show the human message as supporting text. Avoid scraping the formatted string.

Return Diagnostics Instead Of Raising⚓︎

Use Schema.validate(...) when the caller wants the same safe collection behavior as a result value instead of a TableErrors exception.

result = UserImport.validate(bad_table)

assert not result.valid
assert result.records == ()
for diagnostic in result.errors:
    print(diagnostic.code, diagnostic.row, diagnostic.column)

validate() has no error_mode; it always collects what is safe within each lifecycle phase. It runs references and validators but skips output conversion. Records are returned only when the complete table is valid. See Diagnostics And Validation Results for the Model v1 fields, presence flags, and code catalog.

Use the Same Entry Points⚓︎

Collect mode is available through the same public parsing entry points as fail-fast mode.

Collect mode entry points
from talika import parse_table, parse_table_as


ImportedUserTable.parse(bad_users, error_mode="collect")
ImportedUserTable.parse_as(bad_users, dict, error_mode="collect")

parse_table(ImportedUserTable, bad_users, error_mode="collect")
parse_table_as(ImportedUserTable, bad_users, dict, error_mode="collect")


def imported_users(datatable, talika):
    return talika.parse(
        datatable,
        schema=ImportedUserTable,
        error_mode="collect",
    )

Use the calling style that already fits the surrounding code. The behavior is the same: errors are collected while it is safe to continue, then TableErrors is raised if at least one diagnostic was collected.

The error_mode value is validated before parsing starts. The only supported values are "first" and "collect". Passing another value is API misuse and raises ValueError, not a table diagnostic.

The default remains first

You only get aggregate diagnostics when you explicitly request error_mode="collect". Existing parsers and tests keep their fail-fast behavior.

Understand the Collection Boundary⚓︎

Collect mode does not blindly keep running every later stage after an earlier stage has failed.

That boundary is important. Field parsers, validators, references, table validators, and output builders depend on earlier data being reliable. If a cell did not parse, later validation may see missing or invalid values and produce secondary errors that are technically true but not useful.

A schema with parsing and validation rules
class AccountTable(RowTable):
    email = field("email", required=True)
    age = field("age", parser=integer())

    def validate_record(self, context):
        if "@" not in self.email:
            raise ValueError("email must contain @")

This table has two kinds of problems:

  • row 2 has an invalid integer in age
  • both rows have emails that would fail record validation
Parsing with mixed failure phases
AccountTable.parse(
    [
        ["email", "age"],
        ["bad-email", "old"],
        ["still-bad", "31"],
    ],
    error_mode="collect",
)
Only the parsing phase is reported
Table contains 1 errors:
  1. Field parser failed: invalid literal for int() with base 10: 'old' (code=parser_failed, schema=AccountTable, field='age', row=2, column=2, value='old'). Hint: Check the cell value or adjust the field parser for this syntax.

Talika reports the cell parser failure and stops before record validation. That keeps the first failure list focused on the thing the author must fix first: the table cannot reliably become records until its cells parse.

After the author fixes the parsing error and runs again, record validation can report the email problems against a structurally valid table.

Collection respects lifecycle order

Talika collects independent errors inside a safe phase. It does not mix later dependent failures into a result that was already made unreliable by earlier structural or parsing errors.

Reference resolution is one safe collection phase of its own. Missing targets and key-conversion failures are reported in record, field, then key order, including several failures from one many-reference cell. No reference field is partially assigned, and any reference failure stops record validation, whole-table validation, and output conversion.

Collect Validation Failures⚓︎

When the table parses successfully, collect mode can also aggregate independent record validation failures.

Column-shaped tables make this easy to see because each item column is one record. In this score table, two records parse correctly as integers but fail the same record-level rule.

A column schema with record validation
from talika import ColumnTable, id_field


class ScoreTable(ColumnTable):
    id = id_field("IDs")
    score = field("Score", parser=integer())

    def validate_record(self, context):
        if self.score < 0:
            raise ValueError("score cannot be negative")
Two invalid records in one table
ScoreTable.parse(
    [
        ["IDs", "S-1", "S-2", "S-3"],
        ["Score", "-1", "10", "-5"],
    ],
    error_mode="collect",
)
Collected record validation failures
Table contains 2 errors:
  1. Record validation failed: score cannot be negative (code=record_validation_failed, schema=ScoreTable, column=2, item_id='S-1')
  2. Record validation failed: score cannot be negative (code=record_validation_failed, schema=ScoreTable, column=4, item_id='S-3')

These diagnostics have code=record_validation_failed. They point to item columns and include item_id because the table is column-shaped and has an id_field.

Later phases use the same aggregate error channel after the earlier phases have succeeded. Record validation and parse_as() output conversion can report more than one record-level failure. Whole-table validation usually reports one table-level failure, but in collect mode it is still wrapped into the same TableErrors shape.

One phase may be enough

A collected report does not need to include every possible future problem in the table. It should include the useful set of problems that can be trusted from the current parse state.

Choose When to Collect⚓︎

Use fail-fast mode when the table is part of normal test setup and the next developer only needs the first clear failure.

Use collect mode when the table itself is being reviewed:

  • a CI job checks many feature files before merge
  • a command-line tool validates authored tables
  • an editor extension wants to underline several cells
  • a generated table needs human cleanup
  • a large Gherkin data table should give authors a compact repair list

In project tests, it is common to leave normal scenario execution in fail-fast mode and reserve collect mode for tooling-like checks. That keeps ordinary test failures short while still giving table authors a way to see several actionable fixes at once.

Fail the parse, improve the feedback

Collect mode should not make bad data acceptable. Its job is to make the failure report more useful when the table author is ready to fix more than one problem.