Skip to content

Missing, Empty, Defaults⚓︎

Talika treats three cases differently:

  • a field is missing because its label is not in the table
  • a field is present, but one cell is explicitly empty
  • a field is absent and the schema supplies a default

Those cases look similar when a test fails, but they mean different things to the person editing the feature file. A missing label is a table-shape choice. A blank cell is authored data. A default is schema-owned data.

A table with omitted optional fields
Given the users exist
  | username |
  | alice    |

First ask whether the label exists

Before thinking about defaults or empty strings, ask whether the field label appears in the table at all. Talika's behavior starts from that boundary.

The Field Parsing Lifecycle⚓︎

Whenever Talika processes a field in a Gherkin data table, it evaluates the presence of the field label, default configurations, empty policies, parsers, and validators in a strict, predictable sequence:

  1. Label Presence Check: Talika checks if the field's canonical label or any of its declared aliases appear in the table header for row tables, or in the field-label column for column tables.
    • If absent: Talika looks for a default_factory, then a default. If neither is declared, the field becomes None.
    • If present: Talika proceeds to the cell evaluation step.
  2. Empty Value Check: If the field is present, Talika inspects the cell text.
    • If non-empty: Talika runs the field parser when one is declared. If there is no parser, the text is returned as-is.
    • If empty: Talika evaluates the field's empty policy (raw, none, parse, or error). If empty="parse", it passes the blank string to the parser. Otherwise, it sets the value based on the policy or raises a TableError if empty="error" or the field is required.
  3. Record-Level Validation: Once field values have been parsed, defaulted, or preserved as text, Talika runs validate_record(self, context) on each record.
  4. Table-Level Validation: Once all individual records have successfully passed record validation, Talika runs validate_records(cls, records, context) to validate relationships across the entire collection.

Missing Optional Fields⚓︎

A field is missing when none of its accepted labels appear in the table. In a row table, that means the header is absent. In a column table, that means the field row is absent.

Optional fields with defaults
from talika import RowTable, field


def default_team(context):
    return context.user_data["team"]


class UserDefaults(RowTable):
    username = field("username", required=True)
    role = field("role", default="viewer")
    team = field("team", default_factory=default_team)
    notes = field("notes")

This schema has four fields:

  • username is required.
  • role is optional and has a static default.
  • team is optional and has a context-aware default factory.
  • notes is optional and has no default.

When the table only provides username, Talika fills the missing optional fields:

Parsing a table with missing optional fields
user = UserDefaults.parse(
    [
        ["username"],
        ["alice"],
    ],
    context={"team": "platform"},
)[0]

assert user.username == "alice"
assert user.role == "viewer"
assert user.team == "platform"
assert user.notes is None
Missing optional field result
>> user
UserDefaults(username='alice', role='viewer', team='platform', notes=None)

>> user.as_dict()
{'username': 'alice', 'role': 'viewer', 'team': 'platform', 'notes': None}

The fallback order is direct:

  • if default_factory is declared, call it
  • otherwise, if default is declared, use it
  • otherwise, return None

Defaults are final values. A field parser only converts authored cells; it does not receive a static default or the result of a default factory. Static defaults must be hashable and must not be mutable containers. Use default_factory whenever each record needs a fresh list, dictionary, set, or other mutable value.

Defaults run only for absent fields

Defaults do not run because a cell is blank. They run because the field is not present in the table.

Empty Cells Are Present Values⚓︎

Now compare the same schema with a table that includes every label but leaves some cells empty.

Parsing explicit empty cells
user = UserDefaults.parse(
    [
        ["username", "role", "team", "notes"],
        ["alice", "", "", ""],
    ],
    context={"team": "platform"},
)[0]

assert user.role == ""
assert user.team == ""
assert user.notes == ""
Empty cells are present
>> user.as_dict()
{'username': 'alice', 'role': '', 'team': '', 'notes': ''}

The role default is not used. The team factory is not called. The notes field does not become None. Each field exists in the authored table, so the blank cell is treated as an explicit value.

This distinction is the main thing to remember. A blank cell can be deliberate: it may mean "clear this value", "leave this optional text empty", or "this field is intentionally blank for this record." Talika does not silently replace that author choice with a default.

Do not use defaults as blank-cell cleanup

If a blank cell should become None, be parsed, or be rejected, configure the field's empty-cell policy. A default is not a cleanup rule for visible blank cells.

Missing and Empty Required Fields⚓︎

Required fields add two checks:

  1. the field label must be present
  2. the value must not be empty by default

When the required label is absent, Talika reports a missing required field:

Missing required label
UserDefaults.parse(
    [
        ["role"],
        ["admin"],
    ],
    context={"team": "platform"},
)
Missing required label diagnostic
Required field is missing from the table 
(code=missing_required, schema=UserDefaults, field='username'). 
Hint: Add this field to the table, or make the schema field optional if the project should supply it.

When the label is present but the cell is blank, Talika reports the source cell:

Empty required cell
UserDefaults.parse(
    [
        ["username"],
        [""],
    ],
    context={"team": "platform"},
)
Empty required cell diagnostic
Required field has an empty value 
(code=empty_required, schema=UserDefaults, field='username', 
row=2, column=1, value=''). 
Hint: Fill the cell, or remove required=True if an explicit empty value should be valid.

These diagnostics intentionally point to different fixes. For a missing label, the author adds a column or row. For an empty cell, the author fills the cell or the schema changes what blank means.

Use required for scenario meaning

Make a field required when the scenario would be unclear without it. Do not use required=True just to get a Python type. Use parsers and validation for conversion and business rules.

Default Factories⚓︎

Use default_factory when the fallback value depends on the parse operation. The factory receives a DefaultContext, not a cell context, because there is no source cell for a missing field.

A row ID used by a default factory
from talika import id_field


def default_audit(context):
    return f"audit-{context.item_id}-{context.user_data['suffix']}"


class RowWithId(RowTable):
    user_id = id_field("user id")
    audit = field("audit", default_factory=default_audit)
Parsing a missing field with an item-aware default
record = RowWithId.parse(
    [
        ["user id"],
        ["U-7"],
    ],
    context={"suffix": "qa"},
)[0]

assert record.audit == "audit-U-7-qa"
assert record.table_source.item_id == "U-7"
Default factory result
>> record.as_dict()
{'user_id': 'U-7', 'audit': 'audit-U-7-qa'}

The factory can read:

  • context.schema
  • context.field_name
  • context.field_label
  • context.item_id
  • context.user_data

There is no row, column, or source_value, because the field was absent.

Default factories are not parsers

A parser converts a present cell. A default factory creates a value when the field is missing. Keep those responsibilities separate.

Defaults in Column Tables⚓︎

In column tables, a missing optional row applies to each item column. If the field has a default factory, the factory runs once per item and receives that item's ID.

A column table with one missing row and one blank cell
Given the content exists
  | IDs    | A-1   | P-1 |
  | Status | draft |     |
Column defaults
from talika import ColumnTable


class ContentDefaults(ColumnTable):
    id = id_field("IDs")
    headline = field("Headline", default_factory=default_audit)
    status = field("Status")

The Headline row is absent, so headline is generated for both A-1 and P-1. The Status row is present. A-1 has draft, and P-1 has an explicit empty cell.

Parsing column defaults and empty cells
items = ContentDefaults.parse(
    [
        ["IDs", "A-1", "P-1"],
        ["Status", "draft", ""],
    ],
    context={"suffix": "qa"},
)

assert items[0].headline == "audit-A-1-qa"
assert items[0].status == "draft"
assert items[1].headline == "audit-P-1-qa"
assert items[1].status == ""
Column default result
>> [item.as_dict() for item in items]
[
  {'id': 'A-1', 'headline': 'audit-A-1-qa', 'status': 'draft'},
  {'id': 'P-1', 'headline': 'audit-P-1-qa', 'status': ''},
]

This is the same rule as row tables, just turned sideways. Missing row means the field is absent. Empty item cell means the field is present for that item with a blank value.

A missing row and a blank item cell are not interchangeable

Removing the Status row would make every item's status None by default. Keeping the row and leaving one item blank makes only that item hold an explicit empty value.

Empty-Cell Policies⚓︎

Optional fields can choose what an explicit blank cell means.

Field policies for explicit empty cells
from talika import integer


def parse_blank(value, context):
    return "<blank>" if value == "" else value


class EmptyPolicies(RowTable):
    raw_value = field("raw value", parser=integer(), empty="raw")
    parsed_value = field("parsed value", parser=parse_blank, empty="parse")
    none_value = field("none value", empty="none")
    strict_value = field("strict value", empty="error")

The policies are:

  • empty="raw" preserves ""
  • empty="parse" sends "" to the parser
  • empty="none" returns None
  • empty="error" rejects ""

Declaring empty="parse" without an explicit or inferred parser is invalid and fails when the schema is created.

Parsing empty cells with policies
record = EmptyPolicies.parse(
    [
        ["raw value", "parsed value", "none value"],
        ["", "", ""],
    ]
)[0]

assert record.raw_value == ""
assert record.parsed_value == "<blank>"
assert record.none_value is None
assert record.strict_value is None
Empty policy result
>> record.as_dict()
{'raw_value': '', 'parsed_value': '<blank>', 'none_value': None, 'strict_value': None}

In this example, strict_value is absent from the table, so it is None. When the strict value label is present and its cell is blank, Talika rejects it:

A strict optional field
EmptyPolicies.parse(
    [
        ["strict value"],
        [""],
    ]
)
Strict optional diagnostic
Optional field has an empty value 
(code=empty_optional, schema=EmptyPolicies, field='strict value', 
row=2, column=1, value=''). 
Hint: Fill the cell, omit the field, or choose a different empty-cell policy for this schema field.

Choosing a policy

Use raw when blank text is acceptable, none when blank means no value, parse when your parser owns blank syntax, and error when the field may be omitted but must not be written blank.

Required Fields Always Reject Blanks⚓︎

required=True is authoritative. A required field rejects an explicit empty cell before any parser is called. Required fields cannot use empty="raw", empty="none", or empty="parse"; empty="error" is implied.

A required field with an empty-aware parser
class RequiredValue(RowTable):
    value = field(required=True, parser=parse_blank)


RequiredValue.parse(
    [
        ["value"],
        [""],
    ]
)
Required blank diagnostic
Required field has an empty value
(code=empty_required, schema=RequiredValue, field='value', row=2, column=1, value='')

If blank text has domain meaning, make the field optional and choose empty="parse" explicitly. Do not weaken a required field through a permissive parser.

Default Errors and Invalid Declarations⚓︎

If a default factory fails, Talika reports a default-factory diagnostic. The error is tied to the field, but there is no row or column because the field was missing.

An ordinary factory exception becomes default_factory_failed and remains available as the diagnostic cause. A deliberate TableError, TableErrors, or SchemaDefinitionError passes through unchanged, preserving a project code or hint.

A default factory that fails
def broken_default(context):
    raise RuntimeError("team service unavailable")


class BrokenDefault(RowTable):
    username = field("username")
    team = field("team", default_factory=broken_default)


BrokenDefault.parse(
    [
        ["username"],
        ["alice"],
    ]
)
Default factory diagnostic
Default factory failed: team service unavailable 
(code=default_factory_failed, schema=BrokenDefault, field='team')

Some field declarations are invalid before parsing starts. A field cannot declare both a static default and a default factory. A required field cannot declare a default, because that would make missing required data appear valid.

Invalid default declarations
field("value", default="x", default_factory=lambda context: "y")
field("value", required=True, default="x")
Invalid default declaration errors
ValueError: field cannot declare both default and default_factory
ValueError: required fields cannot declare defaults

Keep defaults boring

Defaults are easiest to maintain when they are predictable. Use static defaults for stable values, factories for context-aware values, and validation for rules that need to inspect the completed record.