Tech Review: AIR Python Web Framework
Fast API, Starlette and Pydantic? Sounds good to me!
Hey y’all, the following is a data technology review I put out in the middle of each week. I span the entire Data Stack and review cool Data and AI tech I like.
I also put out my a weekly newsletter on Sundays with my thoughts and links for the week.
Not Subscribed? Come join! ⊂(◉‿◉)つ
Why Web Frameworks Matter for Data Software
For the past two decades, we’ve relied on enterprise proprietary tools like Tableau and PowerBI. During this period, the boundaries between Data Teams and Software Teams have become increasingly blurred. While most large enterprise companies remain committed to Microsoft and Tableau solutions, there has been a growing trend of incorporating software best practices into data/analytics platforms. Several frameworks have emerged in this space, including Streamlit, Reflex, and Metabase.
What is the role of these frameworks in modern data and analytics? Modern data software requires robust interfaces for data scientists, analysts, and stakeholders to interact with data effectively. Web frameworks play a crucial role in this ecosystem for several reasons:
Data visualization and exploration: Web frameworks enable the creation of interactive dashboards and visualization tools that make complex data accessible and actionable.
Simplified deployment of machine learning models: Frameworks make it easier to deploy and serve ML models through APIs or interactive web interfaces.
Collaborative data workflows: Web applications facilitate collaboration among team members working with shared datasets and analytics pipelines.
Democratizing data access: Well designed web interfaces allow non technical stakeholders to access insights without requiring programming knowledge.
Realtime data processing: Async frameworks are particularly valuable for data applications that process streaming data or require realtime updates.
Introduction to Air Framework
Air is a modern Python web framework built on top of FastAPI, Starlette, and Pydantic. Developed by the authors of “Two Scoops of Django,” Air aims to simplify web development while maintaining high performance and developer productivity.
The framework is currently in alpha state (as of September 2025), with version 0.33.0 being the latest release. Despite its early stage, Air has already gained popularity with 355 stars and 33 forks on GitHub.
Key Features
Built on FastAPI: Air leverages FastAPI’s capabilities, allowing developers to serve both API and web pages from a single application.
Air Tags: Provides Python classes for generating HTML content, offering a typed approach to UI development.
Jinja Integration: Simplifies using Jinja templates compared to vanilla FastAPI.
HTMX Friendly: Includes utilities to work with HTMX for dynamic interfaces.
Pydantic Form Validation: Uses Pydantic for HTML form validation.
The Role of Starlette in Air
Starlette serves as the foundation for Air. It’s an ASGI framework that provides key web functionality including:
Asynchronous request handling: Enables handling multiple requests concurrently without blocking.
Websocket support: Facilitates realtime bidirectional communication.
Routing: Provides URL pattern matching and request routing.
Middleware: Allows processing requests before they reach route handlers.
Static files: Handles serving static files like CSS, JavaScript, and images.
The Importance of Async Web Applications
Async web applications provide several significant advantages:
Improved throughput: Async frameworks can handle more concurrent connections with fewer system resources, leading to better performance under load.
Reduced latency: Non blocking I/O operations allow the server to process other requests while waiting for database queries or external API calls to complete.
Efficient resource utilization: Async frameworks make better use of available CPU and memory resources.
Better handling of long running operations: Operations like file uploads, streaming responses, or long polling can be managed more efficiently.
Getting Started with Air
When exploring a new tool or idea, I immediately head to the quick start guide. Air offers an excellent one that quickly gets you to a “Hello World” example (see below). What I particularly appreciate about Air is how it combines frontend and backend, allowing the entire app to run with a single command. This streamlined launch simplifies deployment on platforms like GCP Cloud Run. Just use one Docker image with the FastAPI run command and your application deploys. While Streamlit also offers this single command convenience, a key difference is that Streamlit uses a single threaded webserver (Tornado) that doesn’t support concurrency like Air does. This gives Air tremendous potential as both a realtime dashboard and a batch data visualization tool. Unlike Streamlit, there’s no need to constantly refresh the page. That’s truly powerful.
import air
app = air.Air()
@app.get(”/”)
async def index():
return air.Html(air.H1(”Hello, world!”, style=”color: blue;”))
The example above demonstrates a simple Air application. The framework abstracts away much of the complexity of FastAPI while still allowing access to its powerful features.
Installation Options
Air can be installed using pip, conda, or uv:
# Using pip
pip install -U air
# Using conda
conda install air -c conda-forge
# Using uv
uv venv
source .venv/bin/activate
uv init
uv add air
Optional features can be installed as extras:
standard: FastAPI’s recommended extras
pretty: Beautiful HTML Rendering (lxml, rich)
sql: SQLModel / SQLAlchemy support
auth: OAuth clients via Authlib
How Air Binds Frontend and Backend
Air provides a unified approach to web development by combining backend API functionality with frontend rendering in a single codebase. This integration works through several key mechanisms:
Single application instance: Air creates a unified application instance that handles both API endpoints and web pages, eliminating the need for separate frontend and backend codebases.
Python based HTML generation: Air Tags allow developers to generate HTML directly from Python code, enabling type safe UI development without context switching between languages.
Streamlined deployment: The unified nature means you can develop, test, and deploy everything with a single command (fastapi dev), simplifying the development workflow.
Shared validation logic: Pydantic models are used for both API validation and form handling, ensuring consistency between frontend and backend data handling.
Integrated routing system: Air extends FastAPI’s routing to handle both API endpoints and page rendering through the same mechanism.
This tight integration is particularly valuable for data applications where the same Python code might be used for data processing and visualization. For example, a data scientist can create a model in Python, develop an API endpoint to serve predictions, and build a user interface to display results. All within the same file and application context.
Vibe Coding an Application
For this review, I wanted to go beyond the “Hello World” example. I used a synthetic dataset I maintain for prototyping data applications. You can access it through my public Github repository and run it locally to see Air in action yourself.
Disclaimer: It features questionable color choices and gratuitous emojis. My focus was on demonstrating functionality, not design aesthetics.
My goal for this coding session was to build an application featuring a filterable data table alongside various graphs that display calculated metrics from the dataset.
Since Air is such a new framework, I needed to provide reference material to help the LLM understand and generate appropriate Air code. To accomplish this, I created a reference markdown file:
# Air web framework
<https://github.com/feldroy/air>
# Boring Semantic Layer
<https://github.com/boringdata/boring-semantic-layer>
Notice I also added the github for Julien Hurault’s Boring Semantic layer. I now consider semantic models an essential part of a Data Platform’s architecture. The Boring Semantic layer’s Python implementation pairs nicely with Air. This Data Source → Semantic Model → UI architecture resembles Rill Data, a product I greatly admire.
I provided this reference markdown as context to the LLM and asked it to examine both repositories to understand Air and the Boring Semantic Layer.
Next, I introduced my dataset, which I stored in JSON format. Since the dataset is small and LLMs process JSON effectively, this format worked well for my needs.
Creating a data table
I had the LLM build a data table from the initial data, which it did very well (albeit with questionable color choices and added emojis).
I then asked the LLM to create filters above the table, letting it choose appropriate filters based on the dataset. Finally, I requested that the table be collapsible. This approach is my typical starting point for any data application prototype because tables are straightforward for an LLM to implement. They simply display source data without performing calculations. The filters demonstrate the framework’s state based architecture, updating the table’s contents without refreshing the entire page. This capability is impressive and powerful. Previously, I would have used a JavaScript framework like Svelte (which I enjoyed), but I prefer working with Python. While Svelte would produce excellent results, Air lets me stay in the Python ecosystem.
Looking at the code at this point, the LLM had chosen to write a lot of the table filter CSS right into the same python file. Air can allow you to pass javascript and CSS as a string and into a python function (Streamlit has similar options). When prettified, this makes the code more lines than I’d care so I collapsed the CSS in my IDE.
One notable issue was that the LLM generated extensive JavaScript code to handle the data table filtering and sorting. This seemed counterintuitive, given that we wanted to leverage Python through Air for this functionality. It’s unclear whether this approach is necessary when using Air or if the LLM simply took the path of least resistance instead of exploring Air specific solutions. The JavaScript implementation worked but was unwieldy, spanning approximately 160 lines (580-740) as a string literal in the Python code.
Adding batch data graphs
Tables with filters is a good start, but I wanted to explore visualization capabilities. Unlike Streamlit with its built in graphing functions, Air requires choosing a graphing library. I let the LLM make this decision, and it selected Charts.js (a good choice.)
Rather than simply visualizing raw data, I wanted to display calculated metrics over time. I asked the LLM to identify four suitable metrics from our dataset and then reference the Boring Semantic Layer GitHub repository to create a semantic layer for these metrics.
Creating a semantic layer is valuable because it allows the LLM to reference predefined calculations rather than reimplementing them for each query. Unfortunately, I had dependency issues I didn’t feel like working through, so I had the LLM create hardcoded calculations for each metric instead. These calculations were then used to generate dataframe outputs for the charts. The LLM still referenced the semantic layer when writing the calculation code, which likely improved the implementation.
The resulting graphs were interactive, properly positioned, and responsive. The code to generate these visualizations was concise and, importantly, written entirely in Python. While I would eventually create additional functions to standardize graph creation for this application, this implementation accomplished what I set out to achieve.
Trying out async capabilities
To showcase Air’s async capabilities, I collaborated with the new Claude 4.5 to develop a useful demo. My initial concept was to create a text input where users could enter a number, submit it, and watch a graph update in real time. Claude suggested the app generate that many sequential numbers and update the graph as each new number appeared. The result was an animated visualization that updated dynamically without refreshing the page. Meanwhile, all other page elements remained fully functional and responsive. Seeing this realtime interaction without page reloads was the “magic moment” that really got me excited about what Air offers.
Claude’s code:
# Async streaming endpoint for progressive data generation
async def generate_progressive_data(num_points: int):
“”“
Progressively generate data points with simulated computation delay.
This demonstrates Air’s async/streaming capabilities.
“”“
for i in range(num_points):
# Simulate some computation time
await asyncio.sleep(0.3) # 300ms delay between points
# Generate interesting data using sine wave with some randomness
x = i
y = 50 + 30 * math.sin(i * .2) + random.uniform(-5, 5)
# Stream as Server-Sent Events format
data = {
‘index’: i,
‘x’: x,
‘y’: round(y, 2),
‘total’: num_points,
‘progress’: round((i + 1) / num_points * 100, 1)
}
yield f”data: {json.dumps(data)}\\n\\n”
# Send completion signal
yield f”data: {json.dumps({’complete’: True})}\\n\\n”
@api.get(”/stream-data/{num_points}”)
async def stream_data_endpoint(num_points: int):
“”“
Endpoint that streams data progressively to showcase async capabilities.
User specifies how many data points to generate.
“”“
# Limit to reasonable range
num_points = max(5, min(num_points, 100))
return StreamingResponse(
generate_progressive_data(num_points),
media_type=”text/event-stream”,
headers={
“Cache-Control”: “no-cache”,
“Connection”: “keep-alive”,
}
)
After building this async section, I was really impressed with how Air handles state. The way you can leverage Python, as opposed to being fully Javascript, really makes the context switch from Data Engineering to Data Platform Engineering less painful.
Using Pydantic models with form submission
To explore Air’s form handling capabilities, I had the LLM create a feedback form that uses Pydantic models for validation. This provides a powerful way to ensure data integrity and type safety while maintaining a clean user experience. The new section showcases how Air seamlessly bridges backend validation with frontend form handling.
The LLM created a Pydantic model to define the structure and validation rules for user feedback:
from pydantic import BaseModel, Field, EmailStr
class FeedbackForm(BaseModel):
name: str = Field(..., min_length=2, max_length=50)
email: EmailStr
rating: int = Field(..., ge=1, le=5)
comments: str = Field(..., min_length=10, max_length=500)
Then, it added a form section to the dashboard that leverages Air’s form handling capabilities with the model:
Claude’s code:
@api.post(”/feedback”)
async def submit_feedback(request: Request):
“”“
Handle feedback form submission with Pydantic validation.
Returns validation errors or success message.
“”“
try:
# Parse form data
form_data = await request.form()
# Create feedback object with Pydantic validation
feedback = StreamingFeedback(
user_id=form_data.get(’user_id’, ‘’),
show_name=form_data.get(’show_name’, ‘’),
rating=int(form_data.get(’rating’, 0)),
comment=form_data.get(’comment’, ‘’)
)
# Add timestamp
feedback_dict = feedback.dict()
feedback_dict[’timestamp’] = datetime.now().isoformat()
feedback_dict[’id’] = len(feedback_storage) + 1
# Store feedback
feedback_storage.append(feedback_dict)
return JSONResponse(
status_code=200,
content={
“success”: True,
“message”: “Thank you for your feedback!”,
“feedback”: feedback_dict
}
)
except ValueError as e:
# Pydantic validation errors
return JSONResponse(
status_code=400,
content={
“success”: False,
“errors”: str(e)
}
)
What makes this approach particularly elegant is how Air connects the Pydantic model to the HTML form. When a user submits invalid data, Air automatically returns appropriate error messages without requiring manual validation code. The form retains the user’s input, highlighting fields with errors and displaying validation messages inline. This creates a seamless experience where backend validation directly informs the frontend interface. Claude also put it in a pretty nifty form design with a gradient background.
Conclusion
In practical terms, Air shines when you’re building applications that require a tight mix of server rendered HTML and dynamic interactions. Its support for HTMX is a strong point, giving you the ability to progressively enhance your pages with minimal JavaScript. The HTML form validation integration via Pydantic is useful, enabling declarative validation right in your view layer. On the flip side, as of now Air is still labelled “alpha,” which means breaking changes are possible and its ecosystem is fairly nascent. If you rely heavily on third party extensions or demand stability at scale, you’ll want to monitor its maturity trajectory.
Overall, Air is a promising framework for developers who like the declarative, type safe style of modern Python frameworks but want more seamless HTML handling. If your project involves a lot of server rendered pages with sprinklings of interactivity, Air provides a compelling, lightweight abstraction that reduces friction. That said, because it’s still in early stages, it’s best suited for new greenfield projects or experimental apps, rather than large production systems where long term stability and broad ecosystem support are nonnegotiable. I personally really enjoyed using it and will look for more opportunities to build with it here soon!






