Skip to content

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.

A feature table with two table problems
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.

A schema that can be used by tests and static checks
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.

Finding matching data tables
from talika import discover_feature_tables


tables = discover_feature_tables(
    "features/users.feature",
    step="the following checked users:",
)

feature_table = tables[0]
Discovered table metadata
>> 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.

Checking a feature file
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.

Collected checker 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.

Returned checker object types
from talika import FeatureDiagnostic, FeatureTable


assert isinstance(feature_table, FeatureTable)
assert isinstance(diagnostics[0], FeatureDiagnostic)
Feature table and diagnostic objects
>> 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.

Readable table errors
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.

Validate a feature table
$ talika check features/users.feature \
>>   --schema app.schemas:CheckedUserTable \
>>   --step "the following checked users:"
Text diagnostics
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.

Validate with JSON output
$ talika check features/users.feature \
>>   --schema app.schemas:CheckedUserTable \
>>   --step "the following checked users:" \
>>   --format json
Structured checker output
{
  "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.

A context factory for checking
def checking_context():
    return {
        "allowed_roles": {"Admin", "Editor", "Viewer"},
        "strict": True,
    }
Check with context
$ 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:

  • 0 when all matched tables are valid
  • 1 when one or more matched tables have error-severity diagnostics
  • 1 when setup or discovery fails, including missing/unreadable files, invalid Gherkin, schema-import failures, or context-factory failures
  • 2 when 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:

No matching tables
No matching Gherkin data tables were found.

That 2 exit code is deliberate. In CI, "we checked nothing" is usually a different problem from "we checked tables and found validation failures."