Skip to content

Validation

Real-time validation for Widget[T] forms using the Observant library (PyPI).

Overview

When you use Widget[T], you get built-in validation capabilities through the underlying ObservableProxy. Add validation rules to fields and get real-time feedback as users edit.

from dataclasses import dataclass
from qtpy.QtWidgets import QWidget, QLineEdit, QLabel
from qtpie import widget, make, Widget, state

@dataclass
class User:
    name: str = ""
    email: str = ""

@widget
class UserEditor(QWidget, Widget[User]):
    name: QLineEdit = make(QLineEdit)
    email: QLineEdit = make(QLineEdit)

    errors: str = state("")
    error_label: QLabel = make(QLabel, bind="{errors}")

    def setup(self) -> None:
        # Add validation rules
        self.add_validator("name", lambda v: "Name required" if not v else None)
        self.add_validator("email", self._validate_email)

    def on_valid_changed(self, is_valid: bool) -> None:
        # Called automatically when validation state changes
        if is_valid:
            self.errors = ""
        else:
            all_errors = []
            for msgs in self.validation_errors().values():
                all_errors.extend(msgs)
            self.errors = ", ".join(all_errors)

    def _validate_email(self, value: str) -> str | None:
        if not value:
            return "Email required"
        if "@" not in value:
            return "Invalid email format"
        return None

Core Methods

add_validator(field, validator)

Add a validation rule to a specific field.

Parameters: - field: str - The model field name - validator: Callable[[Any], str | None] - Validation function that returns: - None if valid - Error message string if invalid

# Simple validator
self.add_validator("name", lambda v: "Required" if not v else None)

# Multiple validators for one field
self.add_validator("age", lambda v: "Required" if v == 0 else None)
self.add_validator("age", lambda v: "Must be 18+" if v < 18 else None)

# Complex validator
def validate_password(value: str) -> str | None:
    if len(value) < 8:
        return "Password must be at least 8 characters"
    if not any(c.isupper() for c in value):
        return "Password must contain uppercase letter"
    return None

self.add_validator("password", validate_password)

is_valid()

Get an observable that tracks overall validity.

Returns: IObservable[bool] - True when all fields pass validation

# Preferred: Override on_valid_changed hook (auto-wired by @widget)
def on_valid_changed(self, is_valid: bool) -> None:
    self.submit_btn.setEnabled(is_valid)

# Or use as a simple bool check
if self.is_valid():
    print("Form is valid!")

validation_for(field)

Get validation errors for a specific field.

Parameters: - field: str - The field name

Returns: IObservable[list[str]] - List of error messages (empty if valid)

# Access field errors in on_valid_changed
def on_valid_changed(self, is_valid: bool) -> None:
    errors = self.validation_for("name").get()
    self.name_error = errors[0] if errors else ""

validation_errors()

Get all validation errors across all fields.

Returns: IObservableDict[str, list[str]] - Dict mapping field names to error lists

# Display all errors
errors_dict = self.validation_errors().get()
for field, messages in errors_dict.items():
    print(f"{field}: {', '.join(messages)}")

# Check specific field from the dict
name_errors = self.validation_errors().get("name", [])
if name_errors:
    print(f"Name errors: {name_errors}")

Complete Example: Registration Form

from dataclasses import dataclass
from qtpy.QtWidgets import QWidget, QLineEdit, QSpinBox, QLabel, QPushButton
from qtpie import widget, make, Widget, state

@dataclass
class User:
    name: str = ""
    age: int = 0
    email: str = ""

@widget
class RegistrationForm(QWidget, Widget[User]):
    # Input fields (auto-bind to model)
    name: QLineEdit = make(QLineEdit)
    age: QSpinBox = make(QSpinBox)
    email: QLineEdit = make(QLineEdit)

    # Error state - reactive strings bound to labels
    name_error: str = state("")
    age_error: str = state("")
    email_error: str = state("")

    # Error displays with reactive binding
    name_error_label: QLabel = make(QLabel, bind="{name_error}", classes=["error"])
    age_error_label: QLabel = make(QLabel, bind="{age_error}", classes=["error"])
    email_error_label: QLabel = make(QLabel, bind="{email_error}", classes=["error"])

    # Submit button
    submit: QPushButton = make(QPushButton, "Submit", clicked="on_submit")

    def setup(self) -> None:
        # Add validation rules
        self.add_validator("name", lambda v: "Name required" if not v else None)
        self.add_validator("age", lambda v: "Must be 18+" if v < 18 else None)
        self.add_validator("email", self._validate_email)

        # Initially disable submit
        self.submit.setEnabled(False)

    def _validate_email(self, value: str) -> str | None:
        if not value:
            return "Email required"
        if "@" not in value:
            return "Invalid email format"
        return None

    def on_valid_changed(self, is_valid: bool) -> None:
        # Called automatically when validation state changes!
        self.submit.setEnabled(is_valid)

        # Update field-specific error messages
        name_errors = self.validation_for("name").get()
        age_errors = self.validation_for("age").get()
        email_errors = self.validation_for("email").get()

        self.name_error = name_errors[0] if name_errors else ""
        self.age_error = age_errors[0] if age_errors else ""
        self.email_error = email_errors[0] if email_errors else ""

    def on_submit(self) -> None:
        if self.is_valid():
            print(f"Submitting: {self.record.name}, {self.record.age}, {self.record.email}")
            self.save_to(self.record)

Validation Timing

Validators run automatically when: - Field value changes (via widget or direct proxy assignment) - You call is_valid().get(), validation_for(field).get(), or validation_errors().get()

The validation is reactive - observables update automatically when values change.

Multiple Validators

You can add multiple validators to the same field:

# All validators must pass for the field to be valid
self.add_validator("password", lambda v: "Required" if not v else None)
self.add_validator("password", lambda v: "Min 8 chars" if len(v) < 8 else None)
self.add_validator("password", lambda v: "Need uppercase" if not any(c.isupper() for c in v) else None)

# If multiple fail, all error messages are returned
errors = self.validation_for("password").get()
# errors might be: ["Min 8 chars", "Need uppercase"]

See Also