Releases: feldroy/air
Air 0.48.1
Air sites now respond correctly to HTTP HEAD requests. If you use Facebook link previews, Twitter cards, Slack unfurls, or uptime monitors that send HEAD, they work on Air out of the box.
uv tool upgrade air
What's fixed
-
HTTP HEAD requests return 200 instead of 405. Every GET route (
@app.page,@app.get,@router.page,@router.get) now responds to HEAD with the correct status, headers, and empty body. FastAPI's routing layer has never added HEAD to GET routes the way Starlette does. Air now restores that behavior in its own route class. (#1123) -
Cleaner linter config for dependency injection. Air's
ruff.tomlnow lists its immutable function calls (Depends,Query,Header, etc.) inextend-immutable-calls, removing the need fornoqa: B008comments throughout the codebase. (#1109)
Contributors
@audreyfeldroy (Audrey M. Roy Greenfeld) traced the HEAD bug to FastAPI's route initialization and wrote the fix.
@pydanny (Daniel Roy Greenfeld) cleaned up the doc build.
Thanks to @francisdbillones (Francis Billones) for adding extend-immutable-calls to the ruff config and cleaning up the noqa comments.
Air 0.48.0
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.
Air 0.47.0
Air 0.47.0 makes Jinja templates as effortless as static files: drop a templates/ directory into your project and app.jinja is ready to use. This release also ships AGENTS.md, a complete guide that teaches AI coding assistants how to build Air apps from scratch. Upgrade with:
uv add --upgrade airWhat's new
Zero-config Jinja templates. app.jinja is now always available on every air.Air() instance, no setup required. Drop a templates/ directory into your project and start rendering. The same pattern as static files: put files where Air expects them, and the wiring happens automatically.
import air
app = air.Air()
@app.page
def index(request: air.Request):
return app.jinja(request, "home.html", title="Home")AGENTS.md for AI coding tools. A comprehensive build guide lives in the repo root and is served from the docs site at docs.airwebframework.org/AGENTS.md. AI assistants that read it can scaffold a complete Air app: routes, Jinja templates, static files, forms, HTMX, SSE, and Railway deployment. The guide was tested end-to-end by an AI building its first Air app, and four gaps it found were fixed immediately.
Railway deployment in the AI guide. The AGENTS.md build instructions cover the full path from blank project to production on Railway: a railway.json, a hypercorn dependency, and four CLI commands.
What's better
Static files cookbook rewritten. The cookbook recipe now shows the actual zero-config workflow instead of a manual app.mount() call that hasn't been needed since 0.46.0.
Docs build works with griffe v2. Bumped mkdocstrings-python to 2.0.3, which depends on griffelib directly instead of the removed griffe meta-package. The docs build had been broken by the griffe 2.0 package split but hadn't been deployed since October 2025.
Contributors
@audreyfeldroy (Audrey M. Roy Greenfeld) and @pydanny (Daniel Roy Greenfeld) designed and built this release: zero-config Jinja template detection, the AGENTS.md build guide, the mockup-to-template workflow, Railway deployment instructions, and the docs build fix.
Air 0.46.0
Air 0.46.0 brings three features that change how you build with Air: sync route handlers that don't block the event loop, automatic cache busting for static files, and the ability to mix Jinja templates directly into Air tag trees. Upgrade with:
uv add --upgrade airWhat's new
Cache-busted static files. Drop a static/ directory into your project and Air handles the rest. Every file gets a content hash in its URL, served with immutable cache headers. HTML responses have their /static/ paths rewritten automatically, so templates, Air tags, and hardcoded HTML all benefit without opt-in. Powered by staticware.
Jinja templates inside Air tag trees. JinjaRenderer now accepts as_string=True, returning rendered HTML that can be embedded directly in Air tags. Mix and match both approaches in the same page. (#981)
jinja = air.JinjaRenderer('templates')
@app.page
def index(request: air.Request):
return air.layouts.mvpcss(
air.Title('Home Page'),
jinja(request, 'content.html', as_string=True)
)Railway deployment guide. New docs walk through deploying Air to Railway with Hypercorn, including configuration and environment setup.
What's better
Windows support. Air's CI now tests on Windows across Python 3.13 and 3.14. Windows users can rely on the test suite catching platform-specific issues.
air.__version__ available at runtime. Read the installed version programmatically without importlib boilerplate.
What's fixed
Sync handlers run in a threadpool. Route handlers written as plain def functions now run in Starlette's threadpool instead of blocking the event loop. Database queries, file I/O, and other blocking operations work naturally without async/await. Air preserves the sync/async nature of your endpoint, so Starlette dispatches it correctly.
Custom exception handlers take priority. User-defined exception handlers now override Air's defaults instead of being silently replaced. Air's built-in 404/500 handlers are also applied automatically when wrapping a custom FastAPI instance. Thanks @msaizar!
status_code honored in route decorators. @app.post("/created", status_code=201) now produces a 201 response as expected, both in the HTTP response and in OpenAPI docs.
Contributors
@audreyfeldroy (Audrey M. Roy Greenfeld) and @pydanny (Daniel Roy Greenfeld) designed and built this release: sync handler dispatch, staticware integration, cache-busted static files, the release automation pipeline, deployment docs, and the README rewrite.
Thanks to @msaizar (Martin Saizar) for fixing exception handler priority and ensuring custom handlers work correctly with Air's defaults, and for adding the missing-examples audit to the QA workflow.
Thanks to @kaapstorm (Norman Hooper) for a docs formatting fix, @garzuze (Lucas Garzuze Cordeiro) for correcting doc callout rendering, and @sankarebarri for ruff naming convention compliance.
v0.45.0
What's Changed
New Contributors
Build items
- BUILD: Added
djhtmlhook to both.pre-commit-config-check.yamland.pre-commit-config-format.yamlto automate formatting of HTML files and Jinja Templates! by @pygarap in #932 - BUILD: Improvements to the project's code quality tooling and configuration management. by @pygarap in #957
- BUILD: Add pre-commit-hooks: Some out-of-the-box hooks for pre-commit. by @pygarap in #935
- BUILD: Added the
validate-pyprojecthook by @pygarap in #959 - BUILD: Some hooks can run in parallel by priority(the new prek priority feature) & Added new hooks: check-jsonschema, yamlfmt & typos now run inside prek! by @pygarap in #960
- BUILD: Added several new pre-commit hooks: creosote, complexipy, flake8-class-attributes-order, flake8-pydantic and pyroma! by @pygarap in #962
- BUILD: Added new pre-commit hooks: rumdl - A modern, high-performance Markdown linter and formatter, built for speed in Rust & editorconfig-checker - A tool to verify that your files are in harmony with your .editorconfig by @pygarap in #966
- BUILD: hotfix: now renovate will upgrade the just-install action correctly! by @pygarap in #970
Chores
- CHORE: enable Ruff rule A (builtins shadowing) by @ohhaus in #919
- CHORE: Remove ruff lint per file ignores by @msaizar in #930
- CHORE: Remove air_tag_source_samples.py from ruff lint per-file-ignore by @msaizar in #954
- CHORE: chore(deps): update endbug/latest-tag digest to 52ce15b by @renovate[bot] in #968
- CHORE: chore(deps): update actions/checkout action to v6.0.1 by @renovate[bot] in #971
- CHORE: chore(deps): update astral-sh/setup-uv action to v7.2.0 - autoclosed by @renovate[bot] in #972
- CHORE: chore-ruff-rule: tc-sub-issue task of #642 completed by @sankarebarri in #973
- CHORE: chore(deps): update pre-commit hook tombi-toml/tombi-pre-commit to v0.7.16 by @renovate[bot] in #974
- CHORE: chore(deps): update pre-commit hook rvben/rumdl-pre-commit to v0.0.212 by @renovate[bot] in #975
Doc changes
- DOCS: Make location of docs consistent by @pydanny in #963
- DOCS: Instructions for serving air uses the air CLI now by @pydanny in #958
- DOCS: Add a few more tools to the Air site by @pydanny in #980
New features
- FEAT: Air CLI polish for clarity and joy by @audreyfeldroy in #929
- FEAT: Convert routing.AirRouter to composition by @pydanny in #964
- FEAT: Remove extraneous HTTP method args from AirRouter by @pydanny in #976
Bug fixes
- FIX:
id_inconsistently used to represent HTML attribute ofidby @msaizar in #926 - FIX: Replace type argument with type_ in tags by @msaizar in #931
- FIX: Rename max argument to max_ in tags to prevent builtin shadowing by @msaizar in #933
- FIX: Rename min to min_ to prevent builtin shadowing in tags by @msaizar in #936
- FIX: Rename open to open_ to prevent builtin shadowing in tags by @msaizar in #938
- FIX: Rename reversed to reversed_ to prevent builtin shadowing in tags by @msaizar in #947
- FIX: Rename list to list_ to prevent builtin shadowing in tags by @msaizar in #952
- FIX: Rename dir to dir_ to prevent builtin shadowing in tags by @msaizar in #953
Refactors
Full Changelog: v0.44.0...v0.45.0
v0.44.1
What's Changed
- CHORE: Air CLI polish for clarity and joy by @audreyfeldroy in #929
- CHORE: enable Ruff rule A (builtins shadowing) by @ohhaus in #919
- CHORE:
id_inconsistently used to represent HTML attribute ofidby @msaizar in #926 - CHORE: Replace type argument with type_ in tags by @msaizar in #931
- CHORE: Remove ruff lint per file ignores by @msaizar in #930
- CHORE: Added
djhtmlhook to both.pre-commit-config-check.yamland.pre-commit-config-format.yamlto automate formatting of HTML files and Jinja Templates! by @pygarap in #932 - CHORE: Rename max argument to max_ in tags to prevent builtin shadowing by @msaizar in #933
- CHORE: Rename min to min_ to prevent builtin shadowing in tags by @msaizar in #936
- CHORE: Rename open to open_ to prevent builtin shadowing in tags by @msaizar in #938
- CHORE: Add pre-commit-hooks: Some out-of-the-box hooks for pre-commit. by @pygarap in #935
- CHORE: Rename reversed to reversed_ to prevent builtin shadowing in tags by @msaizar in #947
- CHORE: Rename list to list_ to prevent builtin shadowing in tags by @msaizar in #952
- CHORE: Rename dir to dir_ to prevent builtin shadowing in tags by @msaizar in #953
- CHORE: Remove air_tag_source_samples.py from ruff lint per-file-ignore by @msaizar in #954
New Contributors
Full Changelog: v0.44.0...v0.44.1
v0.44.0
What's Changed
Features
- FEAT: Add "air run" and "air version" CLI commands, make uvicorn a main dep by @audreyfeldroy in #920
- FEAT: add
preka betterpre-commit, re-engineered in Rust & blacken-docs, Runblackon python code blocks in documentation files! by @pygarap in #918 - FEAT: Introduced two new class methods to
BaseTaginsrc/air/tags/models/base.pyby @pygarap in #917:from_html_filefor building an air-tag tree from a filefrom_html_file_to_sourcefor generating the instantiable source from a file
Refactoring
- REFACTOR: Type Annotations Adjustments in the
air.tags.models.base.BaseTagclass! by @pygarap in #914 - REFACTOR: rename
kwargstocustom_attributesfor improved clarity! by @pygarap in #915 - REFACTOR: reorganize test files into
tagssubdirectory! by @pygarap in #916 - REFACTOR: Convert air.Application from inheritance to composition by @pydanny in #906
Docs
- DOC: Added remaining docstrings to HTML air tags, finishing the awesome effort by @vanessapigwin! by @pydanny in #923
Bugfixes
Full Changelog: v0.43.0...v0.44.0
v0.43.0
What's Changed
New feature
- FEAT: Add support for query params in .url() function by @aybruhm in #899 (First time contribution!)
- FEAT: Add caching for inspect.signature and inspect.unwrap by @msaizar in #903
Bugfixes
- BUGFIX Handle more edge cases in from_html by @pygarap in #891
- BUGFIX: Resolve PEP 563 string annotations in AirRoute by @msaizar in #893
Documentation changes
- DOCS: Update html to source tag by @pydanny in #883
- DOCS: Remove airbook by @pydanny in #884
- DOCS: Improve docs in preparation for the beta of Air by @audreyfeldroy in #890
- DOCS: Improve page decorator docs by @audreyfeldroy in #894
- DOCS: improve RedirectResponse for AI and editors by @audreyfeldroy in #897
Chores
- CHORE: Update FastAPI to 0.125.0 and modernize other libraries by @pydanny in #901
- CHORE: Remove ANN201 and add return types by @msaizar in #880
- CHORE: Remove ANN202 rule by @msaizar in #887
- CHORE: Remove FBT rules by @msaizar in #888
- CHORE: Uncomment PT rule in ruff by @msaizar in #889
- CHORE: Exclude htmlcov directory from codespell checks by @audreyfeldroy in #898
- CHORE: Uncomment DOC ruff rule by @msaizar in #900
- CHORE: Renovate to update by version not hash by @pydanny in #905
New Contributors
Full Changelog: v0.42.0...v0.43.0
v0.42.0
What's Changed
- feat: migrate AirConvert functionality to air-tags by introducing
BaseTag.from_html_to_source! by @pygarap in #879 - [FEAT] Add new AirConvert functionality powered by selectolax by @pygarap in #879
- [FEAT] Improvements to AirTag typing and constants management by @pygarap in #879
- [FEAT] New utility methods for BaseTag and children by @pygarap in #879
BaseTag.is_attribute_free_void_elementBaseTag.has_childrenBaseTag.first_childBaseTag.last_childBaseTag.first_attributeBaseTag.last_attributeBaseTag.num_of_direct_childrenBaseTag.num_of_attributesBaseTag.tag_id
- [CHORE] Remove FURB189 ruff rule by @msaizar in #872
- [CHORE] Add 100% test coverage to missing_examples script by @msaizar in #873
- [CHORE] Add baseline and check modes for scripts/missing_examples.py by @msaizar in #874
- [CHORE] Remove ANN001 from pyproject.toml by @msaizar in #877
Full Changelog: v0.41.2...v0.42.0
v0.41.2
What's Changed
- DOCS: Add source example for AirModel.to_form by @msaizar in #864
- CHORE: Remove dependabot by @pydanny in #867
- chore(deps): update actions/checkout action to v6 by @renovate[bot] in #839
- CHORE: Ruff rule: E501 by @msaizar in #866
- CHORE: remove PGH004 ruff rule by @msaizar in #870
- CHORE: remove W505 ruff rule by @msaizar in #871
- CHORE: Upgrade
ty,and address its errors! by @pygarap in #868 - build(deps): lock file maintenance by @renovate[bot] in #859
Full Changelog: v0.41.1...v0.41.2