Skip to content

Fields⚓︎

A field is the smallest promise in a Talika table contract.

It says: when the authored table uses this label, store the value on this Python attribute, require it or allow it to be absent, optionally convert the cell text, and decide how strict the table should be when the value is blank.

A user table with field labels
Given the users exist
  | username | email             | role  | active |
  | alice    | alice@example.com | admin | true   |
  | bob      | bob@example.com   |       | false  |

The table author sees username, email, role, and active. Your test code should not have to keep re-reading those header strings by hand. A field declaration is the bridge between the table vocabulary and the Python value you want to use in setup code.

Think label first, then value

Start by asking what the label means to a feature-file author. After that, decide whether the value is required, whether it needs a parser, and what should happen when the field is absent or empty.

Define a Field Contract⚓︎

Field declarations live on a RowTable or ColumnTable schema class. The class attribute name is the Python name. When field() has no label, that attribute name also becomes the table label.

users_table.py
from talika import RowTable, boolean, field, integer


class UserFields(RowTable):
    username = field(required=True)
    email = field(required=True)
    role = field(default="viewer")
    active = field(parser=boolean(), default=True)

This contract makes four separate decisions:

  • username maps the table label username to user.username.
  • email maps the table label email to user.email.
  • role is optional and receives "viewer" when the whole role field is absent.
  • active is optional, but when present it is parsed with boolean().

The table label and the Python attribute can be the same, but they do not have to be. In real projects, feature tables often use labels such as Full name, Account status, or Can publish?, while Python code uses name, status, and can_publish.

Implicit and explicit labels
class UserTable(RowTable):
    name = field(required=True)             # label: "name"
    full_name = field("Full name")          # label: "Full name"

Specialized helpers such as id_field(), reference(), and discriminator_field() still require explicit labels because those labels are part of a broader table relationship.

Fields are declared once

The point of a field declaration is to stop every test step from rediscovering the same table rules. Once the schema says what active means, every test that parses through that schema gets the same conversion and validation.

Compiled fields are read-only⚓︎

Talika compiles a schema when its class is created. After that, field labels, aliases, parsers, defaults, and policies cannot be reassigned. This prevents a schema from changing meaning between two parses.

To specialize a contract, declare a subclass and replace the field there:

class BaseUsers(RowTable):
    name = field("name")

class ImportedUsers(BaseUsers):
    name = field("Full name")

The parent remains unchanged and each class receives its own compiled schema. Assigning a new label or parser directly to BaseUsers.name after class creation raises AttributeError.

Inheritance conflicts fail early⚓︎

Inherited fields keep their order, and an explicit field on a child replaces the inherited field in that position. Diamond inheritance deduplicates the same original declaration. Independent components that use the same Python attribute name are ambiguous and must be resolved by redeclaring that field on the concrete schema.

A normal attribute cannot silently replace an inherited field. Field names also cannot replace parser lifecycle methods such as parse, parse_as, validate, build_output, or validate_record, or record APIs such as table_source and as_dict. These mistakes raise SchemaDefinitionError while the schema is being built.

Parse Field Values⚓︎

When Talika parses a table, each field becomes an attribute on the parsed record.

Parse users through the field contract
users = UserFields.parse(
    [
        ["username", "email", "role", "active"],
        ["alice", "alice@example.com", "admin", "true"],
        ["bob", "bob@example.com", "", "false"],
    ]
)

assert users[0].username == "alice"
assert users[0].role == "admin"
assert users[0].active is True

assert users[1].role == ""
assert users[1].active is False
Parsed field values
>> users[0]
UserFields(username='alice', email='alice@example.com', role='admin', active=True)

>> users[1].as_dict()
{'username': 'bob', 'email': 'bob@example.com', 'role': '', 'active': False}

Notice the second record: role is an empty string, not "viewer". That is intentional. The role label exists in the table, and the author left that specific cell blank. Defaults are for absent fields, not for visible blank cells.

Do not use defaults as blank-cell cleanup

A default answers the question "what should happen if this field is not in the table?" It does not silently replace a cell the author wrote as empty. If blank cells need special behavior, choose an explicit empty-cell policy.

Required Fields⚓︎

Use required=True when the table is not meaningful without the value.

For row tables, a required field must appear in the header row, and each record must provide a non-empty cell. For column tables, the same idea applies to the field row and each item column.

If the table omits the required label, Talika reports a missing field:

Missing required label
UserFields.parse(
    [
        ["email", "active"],
        ["alice@example.com", "true"],
    ]
)
Missing required label error
Required field is missing from the table 
(code=missing_required, schema=UserFields, field='username'). 
Hint: Add this field to the table, or make the schema field optional if the project should supply it.

If the label is present but the value is blank, Talika reports the exact cell:

Empty required cell
UserFields.parse(
    [
        ["username", "email", "active"],
        ["", "alice@example.com", "true"],
    ]
)
Empty required cell error
Required field has an empty value 
(code=empty_required, schema=UserFields, field='username', 
row=2, column=1, value=''). 
Hint: Fill the cell, or remove required=True if an explicit empty value should be valid.

These two errors are intentionally different. A missing label means the table shape is wrong. An empty required cell means one record has incomplete data.

Make the important fields required

Required fields are not only about type safety. They also document which pieces of the scenario are essential. If a test would become vague or misleading without the value, make the field required.

Optional Fields and Defaults⚓︎

By default, a field is optional. If an optional field is absent from the table, Talika returns None.

You can provide a static default when the fallback is always the same, or a default_factory when the fallback depends on context.

Defaults for absent fields
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)

Here the table only provides username. The missing role and team fields are filled by the schema:

Parse with defaults
users = UserDefaults.parse(
    [
        ["username"],
        ["alice"],
    ],
    context={"team": "payments"},
)

assert users[0].role == "viewer"
assert users[0].team == "payments"

The factory receives a context object. That makes it useful for project data passed to parse(..., context={...}), or for item-aware defaults in tables that declare an id_field(...).

Now compare that with an explicit blank:

An empty cell is still a present field
users = UserDefaults.parse(
    [
        ["username", "role"],
        ["alice", ""],
    ],
    context={"team": "payments"},
)

assert users[0].role == ""

The role field is present, so Talika does not apply the default. It preserves the blank as a real authored value.

Required fields cannot declare defaults

A field cannot be both required and defaulted. If Talika supplied a value for a missing required field, the table would appear valid even though the scenario author did not provide the required data.

Static defaults must be safe to share

A static default must be hashable and must not be a recognized mutable container. Use default_factory for lists, dictionaries, sets, or other per-record values. Defaults and factory results are already final Python values; Talika does not pass them through the field parser.

Parsers on Fields⚓︎

Every cell starts as text. A parser gives a field permission to turn that text into something else.

Field-level parsers
class AccountFields(RowTable):
    username = field("username", required=True)
    age = field("age", parser=integer())
    active = field("active", parser=boolean(), default=True)
Parsing converted values
accounts = AccountFields.parse(
    [
        ["username", "age", "active"],
        ["alice", "34", "true"],
    ]
)

assert accounts[0].age == 34
assert accounts[0].active is True

The parser belongs to the field, not to the individual test step. That keeps the meaning of age and active stable wherever the schema is used.

If a parser fails, the diagnostic points back to the authored cell:

A value the parser cannot convert
AccountFields.parse(
    [
        ["username", "age"],
        ["alice", "thirty four"],
    ]
)
Parser failure
Field parser failed: invalid literal for int() with base 10: 'thirty four' 
(code=parser_failed, schema=AccountFields, field='age', 
row=2, column=2, value='thirty four'). 
Hint: Check the cell value or adjust the field parser for this syntax.

Python-looking values are still text

34, yes, false, and draft are table text until a field parser gives them meaning. Avoid relying on what a value looks like to a human reader.

Aliases⚓︎

Aliases let old and new table vocabulary coexist while your feature files evolve.

A field with accepted old labels
class LegacyUserFields(RowTable):
    name = field(
        "name",
        aliases=("full name", "display name"),
        required=True,
    )

The canonical label is name, but the schema also accepts full name and display name:

Parsing through an alias
users = LegacyUserFields.parse(
    [
        ["full name"],
        ["Alice Doe"],
    ]
)

assert users[0].name == "Alice Doe"

Aliases are useful when a team renames a label across many feature files. You can accept the older wording while gradually moving tables toward the preferred label.

Talika still rejects a table that uses both the canonical label and an alias for the same field:

Conflicting labels
LegacyUserFields.parse(
    [
        ["name", "full name"],
        ["Alice", "Alice Doe"],
    ]
)
Duplicate label diagnostic
Table contains both a field label and one of its aliases 
(code=duplicate_label, schema=LegacyUserFields, field='name', 
row=1, column=2, value='full name'). 
Hint: Use either the canonical label or one alias, not both in the same table.

That rejection prevents a confusing question: if both cells are present, which one should become user.name?

Unknown Field Labels⚓︎

Talika also checks the other side of the contract: the table should not invent labels the schema does not know. If a feature file adds a new column before the schema is updated, Talika treats that as a table-shape error instead of quietly dropping the value.

An undeclared label
UserFields.parse(
    [
        ["username", "team"],
        ["alice", "payments"],
    ]
)
Unknown field diagnostic
Unknown field label 
(code=unknown_field, schema=UserFields, field='team', 
row=1, column=2, value='team'). 
Hint: Use one of the schema field labels or aliases.

This strictness matters because unknown labels are often spelling mistakes, half-finished schema changes, or values that a test author expected the setup code to use. Silent ignore would make the scenario look complete while the Python test is missing part of the authored intent.

The current schema policy for unknown labels is deliberately narrow:

Unsupported unknown-field policy
class InvalidTable(RowTable):
    unknown_fields = "ignore"
    value = field("value")
Unsupported policy error
unknown_fields must be 'forbid' (schema=InvalidTable)

unknown_fields only accepts "forbid" in this version. If a label should be accepted, declare it as a field or as an alias for an existing field.

Unknown is different from inapplicable

An unknown label is not part of the schema vocabulary at all. A variant field can be known to the table family but inapplicable to one selected record. Those are different situations, and Talika handles them with different policies.

Label Matching and Case Sensitivity⚓︎

By default, Talika's label matching is strict, exact, and case-sensitive. The string passed to field(...) or defined as an alias must match the table cell exactly, including casing and spacing:

  • field("username") will not match a table header cell containing Username (capital U) or username (with trailing whitespace).
  • Talika uses the cell text it receives. If a Gherkin parser or test framework trims visual table padding before passing the datatable to Talika, that happens outside Talika. Once the value reaches Talika, label matching is exact.
Case sensitive label matching
class CaseSensitiveTable(RowTable):
    username = field("username", required=True)

Keep one preferred label

Treat aliases as compatibility vocabulary, not as equal alternatives forever. The first argument to field(...) should be the label you want new tables to use.

Empty-Cell Policies⚓︎

Optional fields can choose how to handle an explicit blank cell.

Different policies for blank optional cells
def parse_blank(value, context):
    return "<blank>" if value == "" else value


class EmptyPolicyFields(RowTable):
    raw_value = field("raw value", 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 available policies are:

  • empty="raw" preserves the blank as "".
  • empty="parse" sends the blank string to the parser.
  • empty="none" returns None.
  • empty="error" rejects the blank cell.

empty="parse" requires a callable parser. Talika rejects that declaration immediately when no parser is supplied, because there would be no defined conversion for the blank cell.

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

assert record.raw_value == ""
assert record.parsed_value == "<blank>"
assert record.none_value is None
Blank-cell results
>> record.as_dict()
{'raw_value': '', 'parsed_value': '<blank>', 'none_value': None, 'strict_value': None}

Use empty="error" when a field may be omitted, but must not be written as a blank value when the label is present.

A strict optional field rejects blanks
EmptyPolicyFields.parse(
    [
        ["strict value"],
        [""],
    ]
)
Strict blank-cell diagnostic
Optional field has an empty value 
(code=empty_optional, schema=EmptyPolicyFields, 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.

Optional does not mean careless

optional means the whole field can be absent. It does not automatically mean a blank cell is acceptable. If blank cells have a project meaning, write that meaning into the field declaration.

ID Fields⚓︎

id_field(...) is a specialized required field for item identity.

A row table with an ID field
from talika import id_field


class IdentifiedUserFields(RowTable):
    user_id = id_field("user id")
    name = field("name", required=True)
    role = field("role", default="viewer")

A row table may declare an ID field when diagnostics, parsers, defaults, or later validation should know which authored item is being parsed. A column table must declare one because each item column needs a stable ID.

A row schema may declare at most one ID field; a column schema must declare exactly one. These cardinality rules are checked while the class is created. Parsed IDs must also be hashable and unique, including row-table IDs and IDs that become equal only after conversion (for example 1 and 01 parsed as integers).

When an ID is present, diagnostics can include item_id:

An error on an identified row
IdentifiedUserFields.parse(
    [
        ["user id", "name"],
        ["U-1", ""],
    ]
)
Diagnostic with item_id
Required field has an empty value 
(code=empty_required, schema=IdentifiedUserFields, field='name', 
row=2, column=2, item_id='U-1', value=''). 
Hint: Fill the cell, or remove required=True if an explicit empty value should be valid.

That extra identity is helpful when a table is long, sorted by a helper, or displayed in CI output where row numbers alone are not enough.

Literal Labels⚓︎

Talika does not infer meaning from punctuation in labels.

A literal label
class ArticleFields(RowTable):
    headline = field("Headline*", required=True)

In this example, Headline* is the exact table label. The * does not make the field required. The field is required because the declaration says required=True.

This small rule keeps table vocabulary predictable. Labels belong to your project language; field options define parser behavior.

Invalid declarations fail before parsing

If a schema declaration itself is invalid, Talika raises SchemaDefinitionError while the schema class is being created or imported. That is different from TableError, which belongs to a specific authored table value.