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.
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.
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:
usernamemust be present and non-emptyagemust parse as an integerrolemust be present and non-empty
The authored table below violates all three rules, but in different cells.
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:
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":
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.
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
]
>> 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 asempty_required,parser_failed, orrecord_validation_failedschema, the schema display namefield, the schema field involved in the failurerowandcolumn, using one-based source coordinates when knownitem_id, for column-shaped records or ID-aware rowsvalue, the authored value that caused the failurehint, 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.
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.
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
AccountTable.parse(
[
["email", "age"],
["bad-email", "old"],
["still-bad", "31"],
],
error_mode="collect",
)
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.
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")
ScoreTable.parse(
[
["IDs", "S-1", "S-2", "S-3"],
["Score", "-1", "10", "-5"],
],
error_mode="collect",
)
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.