textual-wtf¶
Declarative, validated forms for Textual TUI applications.
textual-wtf brings the ergonomics of Django forms to the terminal. Define your form fields as class attributes, compose them into a layout in one line, and get real-time validation, embedded sub-forms, and tabbed multi-form support — all without writing boilerplate widget code.
-
Declarative API
Define fields as class attributes on a
Formsubclass. Field order is preserved. No metaclass magic to wrestle with — it just works. -
Three-level required cascade
Control required state at the field, class, instance, or render level. Each level overrides the previous, giving you fine-grained control when embedding sub-forms.
-
Embedded sub-forms
Assign a
Forminstance as a class attribute. Its fields are flattened into the parent with a name prefix (billing_street,billing_city, …). Required state cascades independently per embedding. -
Tabbed multi-form layout
TabbedFormrenders multiple forms as tabs. Tab labels turn red whenever any field in that tab has a validation error, giving the user instant visual feedback. -
Real-time validation
Validators fire on the right events automatically.
MaxLengthfires on every keystroke;Requiredfires on blur and submit. Custom validators are one function or one class away. -
Flexible rendering
Use
layout()for a zero-config layout,simple_layout()for full chrome on each field, orbf()for the raw widget when you need complete layout freedom.
At a glance¶
from textual.app import App, ComposeResult, on
from textual_wtf import Form, StringField, IntegerField, BooleanField
from textual_wtf import EmailValidator
class ContactForm(Form):
title = "Contact Us"
name = StringField("Name", required=True, min_length=2)
email = StringField("Email", required=True, validators=[EmailValidator()])
age = IntegerField("Age", minimum=0, maximum=120)
updates = BooleanField("Subscribe to updates")
class ContactApp(App):
def compose(self) -> ComposeResult:
self.form = ContactForm()
yield self.form.layout()
@on(ContactForm.Submitted)
def on_submitted(self, event: ContactForm.Submitted) -> None:
data = event.form.get_data()
self.notify(f"Submitted: {data}")
@on(ContactForm.Cancelled)
def on_cancelled(self, event: ContactForm.Cancelled) -> None:
self.app.exit()
if __name__ == "__main__":
ContactApp().run()
Installation¶
Python version
textual-wtf requires Python 3.11 or later and Textual 1.0.0 or later.
Where to go next¶
| Resource | What you'll find |
|---|---|
| Getting Started | Step-by-step first form, submit/cancel handling |
| Guide: Forms | Class attributes, required cascade, data access |
| Guide: Fields | All field types and validators |
| Guide: Layout | Rendering modes and custom layouts |
| Guide: Embedding | Sub-form embedding and field prefixes |
| Guide: Validation | When validation fires, cross-field logic |
| Guide: Tabbed Forms | Multi-form tab layouts |
| API Reference | Complete API documentation |