github feldroy/air v0.48.0
Air 0.48.0

one day ago

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 --upgrade

What's new

  • AirForm package. Form validation, rendering, and CSRF protection live in AirForm. form.render() returns SafeHTML that embeds directly in Air Tags without air.Raw() wrapping. CSRF tokens are automatic: render pushes a hidden field, validate pops and checks it.

  • AirField package. Django has models.CharField and forms.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. Both AirField(type="email") and Annotated[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"). Set DATABASE_URL and Air auto-connects on startup. Add a field to your model and create_tables() auto-migrates the table with ALTER 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 for Model.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. Use class MyForm(AirForm[MyModel]): pass instead. (#1100)

  • includes is gone, replaced by excludes. Forms that used includes = ("name", "email") should switch to excludes listing the fields to hide.

  • default_form_widget signature changed. The includes parameter is replaced by excludes. Import from airform directly (from airform import default_form_widget), not from air.forms.

  • Helper functions moved. errors_to_dict, get_user_error_message, pydantic_type_to_html_type, and label_for_field now import from airform, not air.forms. Air's forms.py re-exports only AirForm.

  • render() returns SafeHTML, not SafeStr. The new type follows the __html__ protocol. Code that checked isinstance(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.jinja is the documented pattern. Docs and quickstart use app.jinja(request, "template.html") instead of creating a manual JinjaRenderer. The auto-created renderer has been available since 0.47, now the docs match.

  • validate() accepts any Mapping. Pass Starlette's FormData directly, no dict() wrapping needed.

  • Production CSRF. Set AIRFORM_SECRET env 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.

Don't miss a new air release

NewReleases is sending notifications on new releases.