Skip to content

Constraints

Talea uses typing.Annotated as the carrier for reusable built-in constraints. The base annotation remains the static type, while Talea canonicalizes its validation metadata once during class declaration.

from typing import Annotated

from talea import Ge, Le, MaxLength, MinLength, Spec


class Registration(Spec):
    age: Annotated[int, Ge(18), Le(130)]
    roles: Annotated[list[str], MinLength(1), MaxLength(10)]

Constraint objects are immutable declaration values. They do not expose a runtime validate() method, and generated constructors do not loop over constraint metadata.

Public vocabulary

Constraint Supported base contracts Meaning
Gt(value) int, float, Decimal Strictly greater than
Ge(value) int, float, Decimal Greater than or equal
Lt(value) int, float, Decimal Strictly less than
Le(value) int, float, Decimal Less than or equal
MultipleOf(value) int, float, Decimal Integral multiple of a non-zero divisor
MinLength(value) str, bytes, list/set/frozenset/dict/tuple schemas Inclusive minimum size
MaxLength(value) The same sized contracts Inclusive maximum size
Pattern(value) str A regular-expression search must match

Numeric boundary and divisor values must have the exact numeric family of the base annotation. For example, a Decimal field uses Ge(Decimal("0")), not Ge(0). Bounds and divisors must be finite; divisors cannot be zero. Lengths must be non-negative integers and do not accept booleans.

Applying Pattern to an integer or MinLength to a UUID is a declaration error. Talea never waits until instance construction to discover that a built-in constraint is nonsensical for its base type.

Normalization and contradictions

Equivalent nested Annotated declarations become one canonical constrained schema. Redundant lower, upper, and length bounds reduce to the strongest check. Duplicate MultipleOf and Pattern declarations are removed.

from typing import Annotated

from talea import Ge, Le


Percentage = Annotated[int, Ge(0), Ge(10), Le(100), Le(90)]

Percentage canonicalizes to the effective range Ge(10), Le(90). Impossible ranges such as Ge(10), Lt(10), discrete integer ranges such as Gt(10), Lt(11), and MinLength(10), MaxLength(2) fail during schema resolution. Inclusive single-point ranges remain legal.

Floating-point and Decimal behavior

An unconstrained strict float accepts NaN and positive or negative infinity because they are float values. Ordered constraints reject NaN through their predicate. Infinity follows ordinary ordering: positive infinity satisfies a finite lower bound, while negative infinity satisfies a finite upper bound. MultipleOf rejects non-finite float values and uses math.remainder with a small divisor-relative tolerance to account for binary representation, so 0.3 is a multiple of 0.1.

Unconstrained Decimal values likewise retain Decimal's complete value domain. Numeric constraints reject non-finite Decimal values before ordering or divisibility operations. Decimal MultipleOf uses exact integer ratios and is independent of the active decimal context precision.

Strings and containers

Length checks execute after the exact base-type check and before container item validation. A constrained list therefore compiles into an exact list check, direct length comparisons, and the existing specialized item loop.

Pattern accepts a string or compiled string re.Pattern and uses search semantics. It compiles string declarations once. Generated source binds the compiled expression as a compiler-owned global; quotes, backslashes, and newlines in pattern text are never interpolated as Python source.

import re
from typing import Annotated

from talea import Pattern, Spec


class Product(Spec):
    code: Annotated[str, Pattern(re.compile(r"^[A-Z]{3}-\d{4}$"))]


product = Product(code="ABC-0042")

Metadata interoperability

Talea-owned constraints affect validation. Other Annotated metadata is ignored, not retained in the compact canonical validation schema, and never executed. This policy avoids turning arbitrary metadata objects into accidental validators or adding unused introspection cost. Talea-owned metadata has a separate canonical record; see Metadata and Sensitive.

Inheritance and trust

A constrained override must be provably no wider than its inherited contract. Stronger numeric and length bounds are accepted. Integer and Decimal MultipleOf constraints may narrow to a divisor whose multiples are a subset of the parent divisor. Identical float divisors and patterns are provable; otherwise Talea rejects the override rather than assume implication.

Constraints on immutable scalar values remain permanently trustworthy. Constrained mutable containers remain non-permanently trustworthy. If a nested mutable Spec crosses another Talea boundary after mutation, current-state revalidation executes its length, item, pattern, and numeric constraints.

Failures and performance

Constraint failures retain the rejected value, exact nested location, expected contract, stable category such as greater_than, max_length, or pattern, and structured context containing the failed limit or pattern. Talea remains fail-fast across Spec fields; Validation errors documents human rendering, JSON projection, and union branch diagnostics.

Unconstrained Specs contain no constraint metadata loops, regex machinery, type adapters, or registry lookups. Each used constraint adds only its direct Python operation to the specialized validator or constructor.

Boundary walkthrough

The lower and upper limits are inclusive or exclusive exactly as named:

from typing import Annotated

from talea import Contract, Ge, Lt


type RiskScore = Annotated[int, Ge(0), Lt(100)]
risk_scores = Contract[RiskScore](RiskScore)

assert risk_scores.validate(0) == 0
assert risk_scores.validate(99) == 99
# -1 fails with greater_than_or_equal; 100 fails with less_than.

For sized values, boundaries count the declared Python container or string:

from talea import MaxLength, MinLength


type Tags = Annotated[list[str], MinLength(1), MaxLength(3)]
tags = Contract[Tags](Tags)

assert tags.validate(["verified"])
# [] fails min_length; four tags fail max_length.

Constraints compose with aliases, nested Specs, unions, TypedDict fields, and container items. Place Annotated at the level being constrained:

type NonEmptyText = Annotated[str, MinLength(1)]
type NonEmptyList = Annotated[list[str], MinLength(1)]

# list[NonEmptyText] constrains each string; NonEmptyList constrains the list.

Inheritance narrowing example

from talea import Spec


class PublicLabel(Spec):
    value: Annotated[str, MinLength(1), MaxLength(120)]


class ShortLabel(PublicLabel):
    value: Annotated[str, MinLength(3), MaxLength(40)]

The child accepts a subset of parent values, so substituting it does not weaken the inherited contract. Reversing either bound is rejected at declaration. Pattern implication is not generally decidable; Talea accepts identical patterns but does not guess that one different regular expression narrows another.

Schema and debugging

Integer/float bounds and sized-container limits project to the corresponding Draft 2020-12 keyword. The property holding RiskScore, for example, carries minimum: 0 and exclusiveMaximum: 100. Decimal bounds stay runtime-only because Decimal is represented as JSON text, and float MultipleOf stays runtime-only because Talea's tolerance is not JSON Schema's exact mathematical rule.

When a declaration fails, inspect the base type and constraint value before debugging runtime input: Ge(0) on Decimal should be Ge(Decimal("0")), and MinLength cannot apply to UUID. When a value fails, consume the stable code, location, and context from ValidationError.errors(); the context retains the effective normalized bound.

Do not use a structural constraint for business policy that changes with database state, permissions, clocks, or external services. Those checks belong in the application operation, not a supposedly stable data contract.