Skip to content

Forms

The form classes provide the declarative base for field definitions, the validation pipeline, and the data management API.

BaseForm

BaseForm

BaseForm(
    data=None,
    *,
    label_style=None,
    help_style=None,
    required=None,
    title=None,
)

Base class for form definitions.

Handles field binding, attribute access, validation, and data management.

layout_class class-attribute instance-attribute

layout_class = None

label_style class-attribute instance-attribute

label_style = 'above'

help_style class-attribute instance-attribute

help_style = 'below'

required class-attribute instance-attribute

required = None

title class-attribute instance-attribute

title = ''

bound_fields property

bound_fields

fields property

fields

Submitted dataclass

Submitted(layout, form)

Bases: Message

Posted when the user submits the form.

Cancelled dataclass

Cancelled(layout, form)

Bases: Message

Posted when the user cancels the form.

get_field

get_field(name)

Get a bound field by name.

validate

validate()

Validate all fields. Returns True only if all fields are valid.

is_valid

is_valid()

Alias for validate().

clean

clean()

Full form-level cleaning pipeline.

Calls validate() to check each field. If all pass, calls clean_form() for cross-field checks. Any calls to add_error() inside clean_form() also cause clean() to return False, even if clean_form() returns True.

After clean_form() completes, all field widgets are notified so that any errors set via the backward-compatible direct-assignment style are reflected in the UI.

clean_form

clean_form()

Override for cross-field validation.

Called only after all individual fields pass validate(). Return True if valid, False otherwise.

Use self.add_error(field_name, message) to attach errors to specific fields — this is preferred over direct attribute assignment.

add_error

add_error(field_name, message)

Attach a cross-field error to a named field.

Intended for use inside clean_form(). The error is visible in the field's error label when the UI is next refreshed (which happens automatically at the end of clean()).

Raises FormError if field_name does not exist.

get_data

get_data()

set_data

set_data(data)

layout

layout(using=None, *, id=None)

Return the widget that renders this form.

With no argument, returns a :class:~textual_wtf.DefaultFormLayout widget (fields + Submit/Cancel buttons). The default may be customised by setting the :attr:layout_class class attribute on the form.

With a :class:~textual_wtf.FormLayout subclass, returns an instance of that class wrapping this form.

With a callable, calls using(form) and returns the result. The callable receives this form instance and must return a :class:~textual.widget.Widget.

Usage::

# default layout
yield self.form.layout()

# custom Widget subclass
yield self.form.layout(MyTwoColumnLayout)

# callable returning a Widget
def my_layout(form) -> Widget:
    return MyTwoColumnLayout(form=form)

yield self.form.layout(my_layout)

Form

Form

Form(
    data=None,
    *,
    label_style=None,
    help_style=None,
    required=None,
    title=None,
)

Bases: BaseForm

Public alias for BaseForm. Use this to define forms.


Class attribute reference

layout_class

layout_class: type[FormLayout] | None = None

The layout class used by layout(). When None, DefaultFormLayout is used. Set this on your form class to use a custom layout by default:

class MyForm(Form):
    layout_class = MyTwoColumnLayout

label_style

label_style: LabelStyle = "above"

Default label positioning for all fields in this form. Fields that explicitly set their own label_style are not affected. Valid values:

  • "above" — label on its own line above the input
  • "beside" — label to the left of the input on the same line
  • "placeholder" — label text used as the input placeholder; no visible label

help_style

help_style: HelpStyle = "below"

Default help-text rendering for all fields. Valid values:

  • "below" — help text appears as a muted line below the input
  • "tooltip" — help text appears in a tooltip on hover / focus

required

required: bool | None = None

Form-wide required default. None means "do not override field defaults". See the required cascade.

title

title: str = ""

Displayed as a bold heading at the top of DefaultFormLayout. Also used as the tab label in TabbedForm.


Messages

Form.Submitted

Posted when the user confirms the form and all validation passes.

@dataclass
class Submitted(Message):
    layout: FormLayout   # the FormLayout widget that received the submit action
    form: BaseForm       # the form instance with validated data

Handle it anywhere in the widget hierarchy above the layout:

@on(MyForm.Submitted)
def handle_submit(self, event: MyForm.Submitted) -> None:
    data = event.form.get_data()

Form.Cancelled

Posted when the user presses Cancel or the Escape key.

@dataclass
class Cancelled(Message):
    layout: FormLayout
    form: BaseForm

Method reference

init

def __init__(
    self,
    data: dict[str, Any] | None = None,
    *,
    label_style: LabelStyle | None = None,
    help_style: HelpStyle | None = None,
    required: bool | None = None,
    title: str | None = None,
) -> None

All keyword arguments override the corresponding class attribute for this instance only.

validate / is_valid

def validate(self) -> bool
def is_valid(self) -> bool  # alias

Run all validators on every field. Returns True only if every field passes. Updates error state on failing BoundField objects. Does not call clean_form().

clean

def clean(self) -> bool

Full pipeline: validate() then clean_form() (if all fields pass). Returns True only if both succeed. Synchronises error state to the UI after clean_form() completes.

clean_form

def clean_form(self) -> bool

Override in your form class for cross-field validation. Called only after validate() succeeds. Use self.add_error() to attach errors to specific fields. The base implementation returns True.

add_error

def add_error(self, field_name: str, message: str) -> None

Attach a validation error to the named field. Intended for use inside clean_form(). Raises FormError if field_name does not exist.

get_data / set_data

def get_data(self) -> dict[str, Any]
def set_data(self, data: dict[str, Any]) -> None

get_data() returns a snapshot of all current field values. set_data() updates the named fields without affecting others.

layout

def layout(
    self,
    using: type[FormLayout] | Callable[..., Widget] | None = None,
    *,
    id: str | None = None,
) -> Widget

Return the widget that renders this form. Use with yield like any other Textual widget:

yield self.form.layout()                    # default layout
yield self.form.layout(MyTwoColumnLayout)   # Widget subclass
yield self.form.layout(my_layout_fn)        # callable returning a Widget

With no argument, returns a DefaultFormLayout widget (fields + Submit/Cancel buttons). The default may be customised by setting layout_class on the form class. With a FormLayout subclass, returns an instance of that class. With a callable, calls using(form) and returns the result (must be a Widget).