Parser Factories⚓︎
Parser factories create the callables you pass to field(parser=...).
A table cell starts as text. A parser decides what that text means for one
field: maybe it becomes an integer, a boolean, a decimal, a normalized string,
a domain value, a list, or None.
The factories in Talika are deliberately small. Each one does one clear job, and the composition helpers let you build larger parsing rules without hiding the table vocabulary inside a long custom function.
Given the users exist
| username | age | mask | rating | balance |
| Alice | 34 | ff | 4.5 | 12.30 |
Read parsers as table vocabulary
A parser is not just a Python conversion. It is part of the contract for
what authors are allowed to write in the table. If a cell says ff, the
parser is the rule that explains why that means 255.
A parser factory returns a parser callable. Talika calls that parser with the cell value and source-aware context while parsing the field. Most built-in parsers do not need the context, but they accept it so they can be composed with custom parsers that do.
Start with Scalar Parsers⚓︎
Use scalar parsers when one cell should become one ordinary Python value.
from decimal import Decimal
from talika import RowTable, decimal, field, floating, integer, string
class ScalarParsers(RowTable):
username = field("username", parser=string(strip=True, lower=True))
age = field("age", parser=integer())
mask = field("mask", parser=integer(base=16))
rating = field("rating", parser=floating())
balance = field("balance", parser=decimal())
This schema uses:
string(strip=True, lower=True)to normalize authored text.integer()to parse base-10 whole numbers.integer(base=16)to parse hexadecimal text.floating()to parse Python floats.decimal()to parse exact decimal values.
user = ScalarParsers.parse(
[
["username", "age", "mask", "rating", "balance"],
[" Alice ", "34", "ff", "4.5", "12.30"],
]
)[0]
assert user.username == "alice"
assert user.age == 34
assert user.mask == 255
assert user.rating == 4.5
assert user.balance == Decimal("12.30")
>> user
ScalarParsers(username='alice', age=34, mask=255, rating=4.5, balance=Decimal('12.30'))
>> user.as_dict()
{'username': 'alice', 'age': 34, 'mask': 255, 'rating': 4.5, 'balance': Decimal('12.30')}
Use decimal() for values such as money, balances, and exact quantities where
binary floating-point behavior would make assertions harder to read.
String parsing is still parsing
string(...) is useful even though the input is already text. It gives
your schema a declared normalization rule, so every parsed record sees the
same stripped or cased value.
Parse Boolean Vocabulary⚓︎
Boolean values are common in feature tables, but the words are rarely universal.
One team may write yes and no; another may write enabled and disabled;
another may require exact uppercase tokens from an external system.
Given the account states exist
| default active | lifecycle active | strict active |
| true | enabled | YES |
| false | inactive | NO |
from talika import boolean, compose
class BooleanParsers(RowTable):
default_active = field("default active", parser=boolean())
lifecycle_active = field(
"lifecycle active",
parser=boolean(
true_values=("enabled", "active", "y"),
false_values=("disabled", "inactive", "n"),
),
)
strict_active = field(
"strict active",
parser=boolean(
true_values=("YES",),
false_values=("NO",),
case_sensitive=True,
),
)
The default boolean() parser accepts only true and false. Matching is
case-insensitive unless you set case_sensitive=True, so TRUE, False, and
other case variations still have the same meaning.
Values such as yes, no, 1, 0, on, and off are not universal
booleans. If they belong to your project language, declare them through
true_values and false_values, as the lifecycle_active field does above.
Pass those options as collections of strings: ("yes",) is a one-token tuple,
while the bare string "yes" is rejected as an invalid declaration.
Boolean vocabulary belongs to the schema
A reader should be able to discover every accepted spelling from the field
declaration. Talika does not hide a broad list of convenience tokens behind
boolean().
enabled, disabled = BooleanParsers.parse(
[
["default active", "lifecycle active", "strict active"],
["true", "enabled", "YES"],
["false", "inactive", "NO"],
]
)
assert enabled.default_active is True
assert enabled.lifecycle_active is True
assert enabled.strict_active is True
assert disabled.default_active is False
assert disabled.lifecycle_active is False
assert disabled.strict_active is False
>> enabled.as_dict()
{'default_active': True, 'lifecycle_active': True, 'strict_active': True}
>> disabled.as_dict()
{'default_active': False, 'lifecycle_active': False, 'strict_active': False}
Boolean parsing is strict. Unknown values fail instead of falling back to Python truthiness:
Field parser failed: Expected one of ['false', 'true']
(code=parser_failed, schema=BooleanParsers, field='default active',
row=2, column=1, value='maybe').
Hint: Check the cell value or adjust the field parser for this syntax.
Do not rely on Python truthiness
In plain Python, many non-empty strings are truthy. In a feature table,
"false" should not accidentally become true. Talika's boolean parser only
accepts the configured tokens.
The parser also does not remove whitespace. This keeps normalization visible instead of silently changing authored cells. If the table format permits padding, compose that rule explicitly:
class PaddedBoolean(RowTable):
active = field(
"active",
parser=compose(string(strip=True), boolean()),
)
record = PaddedBoolean.parse([["active"], [" true "]])[0]
assert record.active is True
Whitespace is part of the token
The default parser rejects " true ". Use
compose(string(strip=True), boolean()) only when the table contract says
surrounding whitespace is harmless.
describe() reports the effective Boolean vocabulary and case policy. This is
useful for CLI output, generated tooling, and reviews of a large schema:
default_contract = BooleanParsers.describe().fields[0].parser
lifecycle_contract = BooleanParsers.describe().fields[1].parser
assert default_contract == (
"boolean(true_values=('true',), false_values=('false',), "
"case_sensitive=False)"
)
assert lifecycle_contract == (
"boolean(true_values=('active', 'enabled', 'y'), "
"false_values=('disabled', 'inactive', 'n'), case_sensitive=False)"
)
Inspect the contract during a migration
When replacing an older broad Boolean vocabulary, use describe() to
confirm which fields intentionally retain domain tokens such as yes/no.
Choose Between Choice and Mapping⚓︎
Use choice(...) when the table should contain one of a known set of strings.
Use map_value(...) when the table text should become a different Python
value.
from talika import choice, map_value
class VocabularyParsers(RowTable):
role = field("role", parser=choice("admin", "editor", case_sensitive=False))
status = field("status", parser=choice("Draft", "Published", case_sensitive=False))
priority = field("priority", parser=map_value({"low": 1, "medium": 3, "high": 5}))
choice("Draft", "Published", case_sensitive=False) accepts case-insensitive
input but returns the canonical configured spelling. map_value(...) returns
whatever Python value is stored in the mapping.
user = VocabularyParsers.parse(
[
["role", "status", "priority"],
["Admin", "published", "high"],
]
)[0]
assert user.role == "admin"
assert user.status == "Published"
assert user.priority == 5
These two parsers solve different problems. choice() is about validation and
canonical spelling. map_value() is about translating table vocabulary into a
domain value, such as "high" becoming 5.
Field parser failed: Expected one of ['Draft', 'Published']
(code=parser_failed, schema=VocabularyParsers, field='status',
row=2, column=1, value='Archived').
Hint: Check the cell value or adjust the field parser for this syntax.
Field parser failed: No mapped value for 'urgent'
(code=parser_failed, schema=VocabularyParsers, field='priority',
row=2, column=1, value='urgent').
Hint: Check the cell value or adjust the field parser for this syntax.
Use mapping when the test needs another type
If the test should receive "Published", use choice(). If the test
should receive an enum member, integer weight, sentinel object, or domain
value, use map_value() or a custom parser.
Build Lists with Split, Compose, and Each⚓︎
Tables often store compact lists in one cell. Talika does not guess a list syntax from a type annotation, so the parser should describe how the cell is written.
Given the user metadata exists
| tags | scores | reviewer |
| qa, docs | 1;2;3 | none |
| smoke, regression | 4;5 | 42 |
from talika import compose, each, optional, split
class ListParsers(RowTable):
tags = field("tags", parser=split(","))
scores = field("scores", parser=compose(split(";"), each(integer())))
reviewer = field(
"reviewer",
parser=optional(integer(), none_values=("none", "n/a", "null")),
empty="parse",
)
This schema uses three parser helpers together:
split(",")turns one text cell into a list of strings.compose(a, b)runs parsera, then sends its result to parserb.each(integer())appliesinteger()to every item in a non-string iterable.
reviewed, assigned = ListParsers.parse(
[
["tags", "scores", "reviewer"],
["qa, docs", "1;2;3", "none"],
["smoke, regression", "4;5", "42"],
]
)
assert reviewed.tags == ["qa", "docs"]
assert reviewed.scores == [1, 2, 3]
assert reviewed.reviewer is None
assert assigned.tags == ["smoke", "regression"]
assert assigned.scores == [4, 5]
assert assigned.reviewer == 42
>> reviewed.as_dict()
{'tags': ['qa', 'docs'], 'scores': [1, 2, 3], 'reviewer': None}
>> assigned.as_dict()
{'tags': ['smoke', 'regression'], 'scores': [4, 5], 'reviewer': 42}
Parser order matters. each(integer()) expects an iterable that is already a
list-like value. It should usually come after split(...), not before it.
Field parser failed: invalid literal for int() with base 10: 'two'
(code=parser_failed, schema=ListParsers, field='scores',
row=2, column=1, value='1;two;3').
Hint: Check the cell value or adjust the field parser for this syntax.
In this diagnostic, the bad item is two, but the source value is still the
original cell 1;two;3. That is useful because the feature author fixes the
whole authored cell, not an intermediate parser value.
class WrongOrder(RowTable):
scores = field("scores", parser=each(integer()))
WrongOrder.parse(
[
["scores"],
["1;2;3"],
]
)
Field parser failed: each parser expects a non-string iterable
(code=parser_failed, schema=WrongOrder, field='scores',
row=2, column=1, value='1;2;3').
Hint: Check the cell value or adjust the field parser for this syntax.
Composition is left to right
Read compose(split(";"), each(integer())) as: first split the cell on
semicolons, then parse each split item as an integer. If you reverse that
order, each() receives the original string and rejects it.
Composing Custom Pipelines⚓︎
Because built-in parser factories are small, you can compose them using
compose(...) to create a parser pipeline for a project-specific cell shape.
This is useful when the authored cell has several steps of meaning. A category cell might first need to be split on semicolons, then each item needs whitespace handling, then each item must belong to a vocabulary. None of those steps is large enough to deserve a custom parser by itself, but together they describe a real table rule.
class PipelineSchema(RowTable):
categories = field(
"categories",
parser=compose(
split(";"),
each(choice("A", "B", "C", case_sensitive=False))
)
)
records = PipelineSchema.parse(
[
["categories"],
["a;B"],
]
)
assert records[0].categories == ["A", "B"]
This pipeline approach keeps the intent visible in the field declaration. The reader can see that the cell is split first, then every item is checked. A custom parser can still be the right choice for domain-heavy logic, but parser factories are easier to reuse when the behavior is mostly mechanical.
Use composition for boring rules
If the logic is split, trim, convert, choose, or validate each item, compose factories. If the logic needs domain decisions, service data, or several named branches, write a custom parser.
Handle Empty and Null-Like Values⚓︎
Use optional(parser) when an empty cell or a null-like token should become
None, while non-null values should still be parsed.
class OptionalParsers(RowTable):
reviewer = field(
"reviewer",
parser=optional(integer(), none_values=("none", "n/a", "null")),
)
records = OptionalParsers.parse(
[
["reviewer"],
[""],
["NULL"],
["none"],
["n/a"],
["7"],
]
)
assert [record.reviewer for record in records] == [None, None, None, None, 7]
optional(...) understands blank cells, none, and null. A blank reaches
the parser only when the field declares empty="parse"; without that policy,
the field handles the blank first. When you pass none_values=..., you are
replacing the configured null-like tokens. Include every token your table
should accept.
class ReplacedNullTokens(RowTable):
reviewer = field(
"reviewer",
parser=optional(integer(), none_values=("n/a",)),
empty="parse",
)
ReplacedNullTokens.parse(
[
["reviewer"],
["none"],
]
)
Field parser failed: invalid literal for int() with base 10: 'none'
(code=parser_failed, schema=ReplacedNullTokens, field='reviewer',
row=2, column=1, value='none').
Hint: Check the cell value or adjust the field parser for this syntax.
Here none_values=("n/a",) means none is no longer a null-like token. The
parser tries to parse none as an integer and fails.
Optional parser and optional field are different
An optional field may be absent from the table. optional(parser) handles
values that are present but intentionally blank or null-like. Both ideas
are useful, but they solve different cases.
Understand Parser Failures⚓︎
Parser failures are wrapped in TableError during table parsing. The wrapper
keeps the original source location, schema name, field label, and authored
value.
That wrapping is why failures from boolean(), choice(), integer(), or a
composed parser still point to the table cell instead of only showing a Python
stack trace. The underlying exception remains useful, but the table diagnostic
tells the feature author where to look.
Configuration errors are different. If a parser factory is called with an impossible configuration, it raises immediately while the schema is being defined. Each line below is a separate invalid parser declaration:
string(lower=True, upper=True)
boolean(true_values=("yes",), false_values=("YES",))
boolean(true_values="yes")
choice()
split("")
compose()
ValueError: string parser cannot enable both lower and upper
ValueError: Boolean true and false values overlap: ['yes']
TypeError: true_values must be a non-string iterable of strings
ValueError: choice parser requires at least one allowed value
ValueError: split separator cannot be empty
ValueError: compose requires at least one parser
These errors are not table data errors. They mean the parser declaration itself is contradictory, incomplete, or uses an ambiguous argument shape such as a bare string where a collection of Boolean tokens is required.
Keep parser declarations close to the field
When a parser is short, place it directly in field(parser=...). When the
parser starts to express project vocabulary, assign it to a named variable
or move it into a small helper so the table contract remains readable.