Static Checking⚓︎
Static checking validates feature-file tables without running the pytest scenario that owns them.
That is useful when table problems should be caught earlier: in CI, in a pre-commit hook, in an editor integration, or in a documentation example that should be checked like source code.
Feature: Static checking
Scenario: Invalid users
Given the following checked users:
| name | age |
| | old |
This table has an empty required name cell and an age cell that cannot be
parsed as an integer.
from talika import RowTable, field
class CheckedUserTable(RowTable):
name = field("name", required=True)
age: int = field("age", required=True)
Static does not mean shallow
Static checking does not merely inspect labels. It runs the schema parser,
field parsers, record validators, table validators, references, defaults,
and transformations. It deliberately uses validate(), so output models
and custom build_output() hooks do not run during checking, and every
returned item uses Diagnostic Model v1.
Discover Feature Tables⚓︎
The checker first finds Gherkin data tables that match your filters. Discovery preserves feature-file coordinates, not just table-relative positions.
from talika import discover_feature_tables
tables = discover_feature_tables(
"features/users.feature",
step="the following checked users:",
)
feature_table = tables[0]
>> feature_table.table.to_rows()
[['name', 'age'], ['', 'old']]
>> (feature_table.feature, feature_table.scenario, feature_table.step)
('Static checking', 'Invalid users', 'the following checked users:')
>> (
... feature_table.table.cell(2, 1).source_row,
... feature_table.table.cell(2, 1).source_column,
... )
(6, 15)
The second row of the datatable is still row 6 in the feature file. That is the coordinate an editor, CI log, or reviewer needs.
Scenario outlines are expanded with the official Gherkin compiler. Each
Examples row produces one logical FeatureTable and counts as one matched
table. Filters and returned metadata keep the original outline scenario and
step text. A cell that is exactly <parameter> points to the corresponding
Examples cell; a mixed template such as user-<id> keeps the template cell as
its source while exposing the compiled logical value.
Filter narrowly
Use step when one schema belongs to one step text. Use scenario when a
file has several similar tables and you only want to check one scenario.
Check with the Python API⚓︎
Use check_feature(...) when Python code should discover and validate matching
tables in one call.
from talika import check_feature
diagnostics = check_feature(
"features/users.feature",
schema=CheckedUserTable,
step="the following checked users:",
)
Static checking uses collect mode, so one table can return several diagnostics.
>> len(diagnostics)
2
>> [diagnostic.error.code for diagnostic in diagnostics]
['empty_required', 'parser_failed']
>> [(d.error.row, d.error.column, d.error.value) for d in diagnostics]
[(6, 15, ''), (6, 17, 'old')]
Each diagnostic keeps the feature file identity, scenario name, step text, and the structured table error.
from talika import FeatureDiagnostic, FeatureTable
assert isinstance(feature_table, FeatureTable)
assert isinstance(diagnostics[0], FeatureDiagnostic)
>> type(feature_table).__name__
'FeatureTable'
>> type(diagnostics[0]).__name__
'FeatureDiagnostic'
>> (diagnostics[0].scenario, diagnostics[0].step, diagnostics[0].error.code)
('Invalid users', 'the following checked users:', 'empty_required')
FeatureTable represents one discovered step datatable. FeatureDiagnostic
represents one diagnostic attached back to the feature, scenario, and step
that produced it. These objects are useful when you are building a custom
checker, editor integration, or report generator and do not want to parse CLI
text.
FeatureDiagnostic.error remains the compatibility TableError adapter.
FeatureDiagnostic.diagnostic exposes the shared immutable model directly.
Required field has an empty value (code=empty_required, schema=CheckedUserTable, field='name', row=6, column=15, value=''). Hint: Fill the cell, or remove required=True if an explicit empty value should be valid.
Field parser failed: invalid literal for int() with base 10: 'old' (code=parser_failed, schema=CheckedUserTable, field='age', row=6, column=17, value='old'). Hint: Check the cell value or adjust the field parser for this syntax.
Your lifecycle code still runs
If a parser, validator, reference rule, or transformer normally talks to a service, reads a clock, or depends on random data, make the checking path deterministic. Static checking should be boring and repeatable.
Check from the CLI⚓︎
The talika check command is the same idea from a terminal. It discovers
matching feature tables, parses them with the schema, and prints diagnostics.
The CLI uses the optional Gherkin parser dependency, so install the cli extra
in environments that run feature-file checks.
$ talika check features/users.feature \
>> --schema app.schemas:CheckedUserTable \
>> --step "the following checked users:"
features/users.feature:6:15: empty_required: Required field has an empty value (code=empty_required, schema=CheckedUserTable, field='name', row=6, column=15, value=''). Hint: Fill the cell, or remove required=True if an explicit empty value should be valid. [scenario='Invalid users']
features/users.feature:6:17: parser_failed: Field parser failed: invalid literal for int() with base 10: 'old' (code=parser_failed, schema=CheckedUserTable, field='age', row=6, column=17, value='old'). Hint: Check the cell value or adjust the field parser for this syntax. [scenario='Invalid users']
Found 2 table error(s) in 1 table(s).
The schema target is written as module:SchemaClass. Point it at a plain schema
module, not a pytest module that has to execute scenario decorators.
Keep schemas importable
A good CLI schema module only declares schemas, parsers, validators, and deterministic helpers. It should not need pytest fixtures or browser setup just to import.
Emit JSON for Tools⚓︎
Use JSON when another tool should consume the result.
$ talika check features/users.feature \
>> --schema app.schemas:CheckedUserTable \
>> --step "the following checked users:" \
>> --format json
{
"diagnostics": [
{
"code": "empty_required",
"column": 15,
"diagnostic_version": 1,
"feature": "Static checking",
"field": "name",
"field_label": "name",
"field_name": "name",
"has_item_id": false,
"has_logical_value": true,
"has_source_value": true,
"hint": "Fill the cell, or remove required=True if an explicit empty value should be valid.",
"item_id": null,
"logical_value": "",
"message": "Required field has an empty value",
"path": "features/users.feature",
"row": 6,
"scenario": "Invalid users",
"schema": "CheckedUserTable",
"schema_name": "CheckedUserTable",
"severity": "error",
"source_uri": "file:///project/features/users.feature",
"source_value": "",
"step": "the following checked users:",
"value": ""
},
{
"code": "parser_failed",
"column": 17,
"diagnostic_version": 1,
"feature": "Static checking",
"field": "age",
"field_label": "age",
"field_name": "age",
"has_item_id": false,
"has_logical_value": true,
"has_source_value": true,
"hint": "Check the cell value or adjust the field parser for this syntax.",
"item_id": null,
"logical_value": "old",
"message": "Field parser failed: invalid literal for int() with base 10: 'old'",
"path": "features/users.feature",
"row": 6,
"scenario": "Invalid users",
"schema": "CheckedUserTable",
"schema_name": "CheckedUserTable",
"severity": "error",
"source_uri": "file:///project/features/users.feature",
"source_value": "old",
"step": "the following checked users:",
"value": "old"
}
],
"error_count": 2,
"format_version": 1,
"matched_tables": 1,
"status": "failed",
"warning_count": 0
}
The JSON document has format_version: 1, status and matched-table counts,
error_count, warning_count, and an ordered diagnostics array. Each item
contains every Diagnostic Model v1 field plus feature, scenario, step,
and the legacy path, schema, field, and value aliases.
Presence flags distinguish absent values from explicit JSON null. Values are
encoded deterministically: mapping keys and unordered containers are sorted,
common standard-library values use stable strings, and unknown or cyclic
objects expose stable type information rather than process-specific repr text.
Pass Deterministic Context⚓︎
Schemas often use parse context for project rules. The CLI can call a zero-argument context factory and pass the returned mapping into parsing.
def checking_context():
return {
"allowed_roles": {"Admin", "Editor", "Viewer"},
"strict": True,
}
$ talika check features/users.feature \
>> --schema app.schemas:CheckedUserTable \
>> --context-factory app.schemas:checking_context
Use this for stable lists, configuration flags, fake clocks, or lookup tables that validators need while checking.
Avoid live dependencies in checking context
A checker that needs a database, external API, or non-deterministic service becomes slow and surprising. Prefer small in-memory data that represents the contract you want feature files to satisfy.
Read Exit Codes⚓︎
The CLI returns conventional exit codes:
0when all matched tables are valid1when one or more matched tables have error-severity diagnostics1when setup or discovery fails, including missing/unreadable files, invalid Gherkin, schema-import failures, or context-factory failures2when the filters match no tables
Operational failures are concise and traceback-free in text mode. JSON mode
keeps the normal top-level shape with status="failed", zero matched tables,
one error, and nullable table-specific diagnostic fields. Invalid command
syntax remains argparse exit code 2.
Warnings from validation hooks are included in checker output but do not
produce exit code 1. Error-severity diagnostics still control failure.
When no tables match, the command prints:
That 2 exit code is deliberate. In CI, "we checked nothing" is usually a
different problem from "we checked tables and found validation failures."