Skip to content

Validators

Validators check field values and produce pass/fail results. All textual-wtf validators subclass textual_wtf.validators.Validator, which itself subclasses Textual's textual.validation.Validator.

Validator (base class)

Validator

Validator(failure_description=None, *, validate_on=None)

Bases: Validator

Base class for textual-wtf validators.

Fires on {"blur", "submit"} by default.

validate_on class-attribute instance-attribute

validate_on = frozenset({'blur', 'submit'})

validate

validate(value)

validate_on

validate_on: frozenset[str] = frozenset({"blur", "submit"})

The set of event names that trigger this validator during interactive use. Override at the class level or pass validate_on= to the constructor.

Valid event names:

  • "change" — fires on every widget value change (keystroke, toggle, selection)
  • "blur" — fires when focus leaves the widget
  • "submit" — fires during form.validate() / form.clean()

Submit always runs everything

validate_on only gates the interactive path. form.validate() runs every validator unconditionally.


FunctionValidator

FunctionValidator

FunctionValidator(fn, *, validate_on=None)

Bases: Validator

Adapt a plain callable into the Validator interface.

The callable receives the field value and should either return normally (indicating success) or raise ValidationError with a descriptive message (indicating failure).

Example::

def no_spaces(value):
    if " " in value:
        raise ValidationError("No spaces allowed.")

name = StringField("Name", validators=[no_spaces])

validate

validate(value)

Wraps a plain callable as a Validator. The callable receives the field value; it should return normally on success or raise ValidationError on failure.

from textual_wtf import ValidationError, FunctionValidator

def must_be_slug(value: str) -> None:
    if value and not value.replace("-", "").isalnum():
        raise ValidationError("Use only letters, numbers, and hyphens.")

slug_validator = FunctionValidator(must_be_slug)

Plain callables passed to validators= are wrapped in FunctionValidator automatically, so you rarely need to instantiate it directly.


Required

Required

Required(failure_description=None, *, validate_on=None)

Bases: Validator

Validates that a value is not None, empty string, or empty sequence.

Passes if the value is non-None, non-empty string (after stripping whitespace), and non-empty collection.

from textual_wtf import Form, StringField, Required

class MyForm(Form):
    # Equivalent forms:
    name = StringField("Name", required=True)
    name = StringField("Name", validators=[Required()])

Fires on: blur, submit.


MinLength

MinLength

MinLength(n, *, validate_on=None)

Bases: Validator

Validates that len(value) >= n.

validate

validate(value)

Passes if len(value) >= n. Skips the check if value is None.

from textual_wtf import Form, StringField, MinLength

class MyForm(Form):
    # Equivalent forms:
    bio = StringField("Bio", min_length=10)
    bio = StringField("Bio", validators=[MinLength(10)])

Fires on: blur, submit.


MaxLength

MaxLength

MaxLength(n, *, validate_on=None)

Bases: Validator

Validates that len(value) <= n.

Fires on {"change", "blur", "submit"} by default so the limit is enforced immediately as the user types.

validate

validate(value)

Passes if len(value) <= n. Skips the check if value is None.

Fires on: change, blur, submit — giving instant feedback as the user types.

from textual_wtf import Form, StringField, MaxLength

class MyForm(Form):
    # Equivalent forms:
    username = StringField("Username", max_length=30)
    username = StringField("Username", validators=[MaxLength(30)])

MinValue

MinValue

MinValue(n, *, validate_on=None)

Bases: Validator

Validates that value >= n.

validate

validate(value)

Passes if value >= n. Skips if value is None.

from textual_wtf import Form, IntegerField, MinValue

class ScoreForm(Form):
    # Equivalent forms:
    score = IntegerField("Score", minimum=0)
    score = IntegerField("Score", validators=[MinValue(0)])

Fires on: blur, submit.


MaxValue

MaxValue

MaxValue(n, *, validate_on=None)

Bases: Validator

Validates that value <= n.

Fires on {"change", "blur", "submit"} by default so the limit is enforced immediately as the user types.

validate

validate(value)

Passes if value <= n. Skips if value is None.

Fires on: change, blur, submit.

from textual_wtf import Form, IntegerField, MaxValue

class BidForm(Form):
    amount = IntegerField("Bid amount", validators=[MaxValue(10000)])

EmailValidator

EmailValidator

EmailValidator(
    failure_description=None, *, validate_on=None
)

Bases: Validator

Validates that a value matches a basic email pattern.

Passes if the value matches a standard email pattern (user@domain.tld). Skips validation when the value is None or empty — combine with Required for a mandatory email field.

from textual_wtf import Form, StringField, EmailValidator

class ContactForm(Form):
    email = StringField(
        "Email",
        required=True,
        validators=[EmailValidator()],
    )

Fires on: blur, submit.


Writing a custom validator

Subclass Validator and override validate(). Use self.success() and self.failure(message) to return results:

custom_validator.py
from typing import Any
from textual.validation import ValidationResult
from textual_wtf import Validator

class NoReservedWords(Validator):
    """Rejects usernames that are reserved system names."""

    RESERVED = frozenset({"admin", "root", "system", "superuser"})

    def validate(self, value: Any) -> ValidationResult:
        if isinstance(value, str) and value.lower() in self.RESERVED:
            return self.failure(
                f"{value!r} is a reserved name and cannot be used."
            )
        return self.success()

Use it on any field:

from textual_wtf import Form, StringField

class SignupForm(Form):
    username = StringField(
        "Username",
        required=True,
        validators=[NoReservedWords()],
    )