Air 0.48.0: The AirModel ORM, AirForm, and AirField
We wanted to release this before our keynote at PythonAsia 2026 tomorrow morning. Air 0.48.0 has major features to the point we almost called it 1.0.0. It's got rough edges, so be ready to file issues on whatever you notice. We'll polish it together during the PythonAsia sprints and in the days to come.
Air's form system, field metadata, and database ORM are new and each live in their own package now. AirForm validates and renders forms with CSRF protection. AirField carries presentation metadata across every context. AirModel talks to PostgreSQL with async CRUD. All three are published on PyPI and usable independently, but from air import AirForm, AirModel, AirField still works.
uv add air --upgradeWhat's new
-
AirForm package. Form validation, rendering, and CSRF protection live in AirForm.
form.render()returns SafeHTML that embeds directly in Air Tags withoutair.Raw()wrapping. CSRF tokens are automatic: render pushes a hidden field, validate pops and checks it. -
AirField package. Django has
models.CharFieldandforms.CharField, two parallel field systems you keep in sync. AirField unifies them: one field carries database metadata (primary_key), form rendering hints (type,label,widget,choices), and Pydantic validation (min_length,ge) in a single declaration. BothAirField(type="email")andAnnotated[str, Widget("email")]work. -
AirModel package. An async ORM for Pydantic models and PostgreSQL, with a Django-flavored API:
create,get,filter,save,delete, plus bulk operations and transactions. AirModel derives table names from class names, maps Python types to PostgreSQL columns, and supports Django-style lookups (sparkle_rating__gte=8,location__icontains="falls"). SetDATABASE_URLand Air auto-connects on startup. Add a field to your model andcreate_tables()auto-migrates the table withALTER TABLE ADD COLUMN. -
Excludes with scoped tuples. Control which fields appear in the form and which reach the database:
class OrderForm(AirForm[Order]): excludes = ( "internal_notes", # hidden from display and save ("slug", "display"), # not rendered, still in save_data() ("tracking_id", "save"), # rendered, excluded from save_data() )
PrimaryKey fields are default display excludes.
form.save_data()returns a dict ready forModel.create(**form.save_data()). -
__html__protocol. Air Tags trust any object with__html__(the Jinja2/MarkupSafe convention). AirForm's render output embeds without escaping.
What's changed
-
to_form()is gone. Useclass MyForm(AirForm[MyModel]): passinstead. (#1100) -
includesis gone, replaced byexcludes. Forms that usedincludes = ("name", "email")should switch toexcludeslisting the fields to hide. -
default_form_widgetsignature changed. Theincludesparameter is replaced byexcludes. Import fromairformdirectly (from airform import default_form_widget), not fromair.forms. -
Helper functions moved.
errors_to_dict,get_user_error_message,pydantic_type_to_html_type, andlabel_for_fieldnow import fromairform, notair.forms. Air'sforms.pyre-exports onlyAirForm. -
render()returnsSafeHTML, notSafeStr. The new type follows the__html__protocol. Code that checkedisinstance(result, SafeStr)needs updating. Code that just used the string doesn't. -
Rendered HTML has
<div class="air-field">wrappers. Each field is wrapped in a div with label, input, and error elements. Code that matched exact HTML output from the old renderer needs updating.
What's better
-
app.jinjais the documented pattern. Docs and quickstart useapp.jinja(request, "template.html")instead of creating a manualJinjaRenderer. The auto-created renderer has been available since 0.47, now the docs match. -
validate()accepts any Mapping. Pass Starlette'sFormDatadirectly, nodict()wrapping needed. -
Production CSRF. Set
AIRFORM_SECRETenv var for multi-worker deployments so all workers share the same signing key.
Contributors
@audreyfeldroy (Audrey M. Roy Greenfeld) designed and built this release: the AirForm, AirField, and AirModel package extractions, the excludes system, CSRF push/pop, SafeHTML protocol, and the complete documentation rewrite.
@pydanny (Daniel Roy Greenfeld) designed the original AirForm class, the swappable widget pattern, and the render/validate lifecycle that the extraction preserved.