How AirModel.to_form() Automatically Generates AirForm Classes in Air
AirModel.to_form() delegates to the air.forms.to_form helper, which dynamically creates a new subclass of AirForm using Python's type() constructor, binding the Pydantic model and optional rendering parameters before returning an instantiated form object.
The feldroy/air repository streamlines web development by bridging Pydantic models and HTML forms through automatic class generation. Understanding how AirModel.to_form() constructs matching AirForm classes reveals the framework's zero-configuration approach to form handling. This mechanism eliminates repetitive boilerplate while maintaining full validation and rendering capabilities.
The Two-Step Generation Mechanism
The conversion process operates through a thin delegation layer followed by runtime subclass construction.
Step 1: Invocation from the Model Class
In src/air/models.py, the AirModel base class defines the to_form() classmethod (lines 22-30). This method acts as a forwarder, passing the model class (cls) along with optional arguments—name, includes, and widget—directly to the air.forms.to_form helper function.
Step 2: Dynamic Subclass Creation with type()
The core implementation resides in src/air/forms.py within the to_form function. This utility builds a new subclass of AirForm on the fly using Python's built-in type() constructor. It configures the class by setting model to the supplied Pydantic model, storing optional includes for field filtering, and attaching a custom widget callable if provided. The function generates a class name—either the user-supplied name or the default "{ModelName}Form"—attaches a docstring, and instantiates the class before returning the object.
How Generated AirForm Classes Function
Generated classes inherit all standard behavior from air.forms.AirForm. The model attribute maintains a reference to the original AirModel (a Pydantic BaseModel), ensuring type definitions remain synchronized. When you invoke validate() on the form, the underlying Pydantic model executes the actual data validation against field constraints. Rendering logic delegates to default_form_widget (or your custom widget), which inspects the model's fields, constraints, and any validation errors to produce HTML output.
Practical Implementation Example
import air
class ContactModel(air.AirModel):
name: str
email: str
message: str | None = None
# Generate a form class with selective fields and custom widget
ContactForm = ContactModel.to_form(
includes=["name", "email"], # render only these fields
widget=lambda **kw: air.Div(...), # optional custom renderer
)
# Use the generated form in a route handler
@app.page
def contact_page() -> air.Html:
form = ContactForm() # instance of the dynamic class
return air.layouts.mvpcss(
air.H1("Contact us"),
air.Form(form.render(), air.Button("Send"))
)
In this implementation, ContactModel.to_form() triggers the dynamic creation of ContactModelForm (or the supplied name) that inherits from AirForm. The returned ContactForm object functions as a fully instantiated form with bound validation and rendering capabilities.
Key Source Files and Their Roles
src/air/models.py: Defines theAirModelbase class and theto_formclassmethod that initiates the generation request.src/air/forms.py: Contains theto_formhelper function that constructs dynamic subclasses usingtype(), and defines theAirFormbase class handling validation and rendering.src/air/forms.py(also): Housesdefault_form_widget, the default HTML renderer used by generated forms when no custom widget is specified.
Summary
- AirModel.to_form() provides a zero-boilerplate convenience wrapper for form generation.
- The
air.forms.to_formhelper uses Python'stype()to construct new AirForm subclasses at runtime. - Generated forms bind the original Pydantic model via the
modelattribute and delegate validation to Pydantic's logic. - Optional parameters enable field filtering (
includes) and custom HTML rendering (widget). - The architecture maintains synchronization between model definitions and form behavior without manual class maintenance.
Frequently Asked Questions
What parameters can I pass to AirModel.to_form()?
The method accepts three optional parameters: name (specifies the generated class name), includes (list of field names to render), and widget (a callable for custom HTML generation). These pass through to air.forms.to_form to configure the dynamic subclass.
How does the generated form validate submitted data?
The generated AirForm subclass delegates validation to its bound Pydantic model. When you call validate() on the form instance, it uses the underlying AirModel (a Pydantic BaseModel) to enforce the original field constraints, types, and validation rules defined in the model class.
Can I customize HTML rendering for dynamically generated forms?
Yes. Pass a callable to the widget parameter when calling to_form(). This replaces the default default_form_widget function, allowing you to control HTML output while the framework automatically handles field structure and error display based on the Pydantic model schema.
Where is the dynamic class creation logic implemented?
According to the feldroy/air source code, the dynamic subclass creation occurs in src/air/forms.py within the to_form function, while the entry point that model classes use resides in src/air/models.py via the AirModel.to_form() classmethod.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →