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.
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.
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:
usernamemaps the table labelusernametouser.username.emailmaps the table labelemailtouser.email.roleis optional and receives"viewer"when the wholerolefield is absent.activeis optional, but when present it is parsed withboolean().
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.
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.
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
>> 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:
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:
UserFields.parse(
[
["username", "email", "active"],
["", "alice@example.com", "true"],
]
)
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.
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:
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:
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.
class AccountFields(RowTable):
username = field("username", required=True)
age = field("age", parser=integer())
active = field("active", parser=boolean(), default=True)
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:
AccountFields.parse(
[
["username", "age"],
["alice", "thirty four"],
]
)
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.
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:
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:
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.
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:
class InvalidTable(RowTable):
unknown_fields = "ignore"
value = field("value")
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 containingUsername(capital U) orusername(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.
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.
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"returnsNone.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.
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
>> 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.
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.
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:
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.
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.