Airpine 🏔️
[!WARNING] I this is in early dev. Could be rewrites, breaking changes, bugs, and more.
Alpine.js integration for the Air framework with excellent Python DX
Airpine provides a Pythonic, type-safe API for working with Alpine.js directives in Air applications. Get excellent IDE autocomplete, natural chained syntax, and type-safe modifiers.
Installation
pip install airpine
Or with uv:
uv pip install airpine
Quick Start
from air import Air, Div, Button, Input, Span
from airpine import Alpine
app = Air()
@app.page
def index():
return Div(
# Counter with reactive state
Button("-", **Alpine.at.click("count--")),
Span(**Alpine.x.text("count")),
Button("+", **Alpine.at.click("count++")),
**Alpine.x.data({"count": 0}),
)
Don't forget to include Alpine.js in your HTML:
from air import Html, Head, Body, Script
def layout(content):
return Html(
Head(
Script(
src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js",
defer=True
)
),
Body(content)
)
Why Airpine?
Before (painful)
# No autocomplete, easy typos, ugly syntax
Button(**{"@click.prevent.once": "save()"})
Form(**{"x-data": '{"email": "", "valid": false}', "@submit.prevent": "send()"})
With Airpine (delightful)
# Full IDE autocomplete, natural syntax, composable
Button(**Alpine.at.click.prevent.once("save()"))
Form(**(
Alpine.x.data({"email": "", "valid": False}) |
Alpine.at.submit.prevent("send()")
))
Features
- ✨ Excellent IDE autocomplete - All Alpine.js directives and modifiers typed
- 🔗 Natural chained syntax -
Alpine.at.click.prevent.once("handler()") - 🎯 Type-safe - Catch errors at dev time, not runtime
- 🐍 Pythonic - Use Python dicts for x-data, no manual JSON
- 🧩 Composable - Merge attributes with
|operator - 🚀 Production-ready - Comprehensive tests, proper escaping
Python → Alpine Cheat Sheet
| Python | Alpine HTML | Description |
|---|---|---|
Alpine.x.data({"count": 0}) |
x-data='{ "count": 0 }' |
Component state |
Alpine.x.text("message") |
x-text="message" |
Set text content |
Alpine.x.show("visible") |
x-show="visible" |
Toggle visibility (CSS) |
Alpine.x.if_("condition") |
x-if="condition" |
Conditional rendering (DOM) |
Alpine.x.for_("item in items") |
x-for="item in items" |
Loop rendering |
Alpine.x.model("email") |
x-model="email" |
Two-way binding |
Alpine.x.bind.class_("active") |
x-bind:class="active" |
Bind class |
Alpine.x.ref("myInput") |
x-ref="myInput" |
Element reference |
Alpine.at.click("handler()") |
@click="handler()" |
Click event |
Alpine.at.submit.prevent("send()") |
@submit.prevent="send()" |
Submit with preventDefault |
Alpine.at.keydown.enter("submit()") |
@keydown.enter="submit()" |
Enter key |
Alpine.at.click.outside("close()") |
@click.outside="close()" |
Click outside |
Alpine.at.input.debounce(300)("search()") |
@input.debounce.300ms="search()" |
Debounced input |
Common Patterns
Modal with ESC key
Div(
Button("Open", **Alpine.at.click("open = true")),
Div(
# Modal content
**Alpine.x.show("open"),
),
**(
Alpine.x.data({"open": False}) |
Alpine.at.keydown.escape.window("open = false") |
Alpine.at.click.outside("open = false")
)
)
Form Validation
Form(
Input(
type="email",
**(
Alpine.x.model("email") |
Alpine.at.input.debounce(300)("validate()")
)
),
Button(
"Submit",
**Alpine.x.bind.disabled("!valid")
),
**Alpine.x.data({
"email": "",
"valid": False,
"validate": RawJS("function() { this.valid = this.email.includes('@'); }")
})
)
Tabs
from airpine import Alpine
Div(
# Tab buttons
Div(
Button("Tab 1", **(
Alpine.at.click("tab = 0") |
Alpine.x.bind.class_("{ 'active': tab === 0 }")
)),
Button("Tab 2", **(
Alpine.at.click("tab = 1") |
Alpine.x.bind.class_("{ 'active': tab === 1 }")
)),
),
# Tab content
Div("Content 1", **Alpine.x.show("tab === 0")),
Div("Content 2", **Alpine.x.show("tab === 1")),
**Alpine.x.data({"tab": 0})
)
Search with Debounce
Div(
Input(
placeholder="Search...",
**(
Alpine.x.model("query") |
Alpine.at.input.debounce(300)("search()")
)
),
Div(**Alpine.x.html("results")),
**Alpine.x.data({
"query": "",
"results": "",
"search": RawJS("""function() {
fetch('/search?q=' + this.query)
.then(r => r.text())
.then(html => { this.results = html; });
}""")
})
)
API Reference
Events (Alpine.at.*)
Common Events
click,dblclick- Mouse clicksinput,change- Form inputsubmit- Form submissionkeydown,keyup,keypress- Keyboardfocus,blur- Focus eventsmouseenter,mouseleave- Mouse movementscroll,resize- Window events
Event Modifiers
.prevent- preventDefault().stop- stopPropagation().once- Run only once.self- Only if event.target is element itself.window- Listen on window.document- Listen on document.outside/.away- Click outside element.passive- Passive event listener.capture- Use capture phase.debounce(ms)- Debounce handler (default 250ms).throttle(ms)- Throttle handler (default 250ms)
Keyboard Modifiers
.enter,.space,.escape,.tab.up,.down,.left,.right.backspace,.delete,.home,.end.page_up,.page_down.shift,.ctrl,.alt,.meta,.cmd.key(name)- Custom key (e.g.,.key("f1"))
Chain modifiers: Alpine.at.keydown.ctrl.enter("submit()")
Directives (Alpine.x.*)
State & Rendering
data(dict | str)- Component statetext(expr)- Set text contenthtml(expr)- Set innerHTML (⚠️ XSS risk with user input)show(expr)- Toggle visibility (CSS)if_(expr)- Conditional rendering (DOM)for_(expr)- Loop rendering
Binding
model(expr)- Two-way data bindingbind.class_(expr)- Bind classbind.style(expr)- Bind stylebind.href(expr)- Bind hrefbind.{attribute}(expr)- Bind any attribute
Lifecycle & Utils
init(expr)- Run on initializationeffect(expr)- Re-run when dependencies changeref(name)- Element reference (access via$refs.name)cloak()- Hide until Alpine loadsignore()- Ignore element and childrenignore_self()- Ignore only element, not childrenkey(expr)- Unique key for x-for itemsid(list)- Generate scoped IDs for accessibilityteleport(selector)- Move content to selectormodelable(prop)- Make property bindable with x-model
Transitions
transition()- Simple transitiontransition.enter(classes)- Enter transitiontransition.enter_start(classes)- Enter start statetransition.enter_end(classes)- Enter end statetransition.leave(classes)- Leave transitiontransition.leave_start(classes)- Leave start statetransition.leave_end(classes)- Leave end state
Plugins (require Alpine.js plugins)
intersect(expr)- Intersection observermask(expr)- Input maskingtrap(expr)- Focus trappingcollapse()- Collapse animation
Model Modifiers
Alpine.x.model(expr)- Basic two-way bindingAlpine.x.model.lazy(expr)- Update on change instead of inputAlpine.x.model.number(expr)- Convert to numberAlpine.x.model.boolean(expr)- Convert to booleanAlpine.x.model.trim(expr)- Trim whitespaceAlpine.x.model.fill(expr)- Use input's value attribute to initializeAlpine.x.model.debounce(ms)(expr)- Debounce updatesAlpine.x.model.throttle(ms)(expr)- Throttle updates
Using RawJS
For JavaScript functions/expressions in x-data, use RawJS:
from airpine import Alpine, RawJS
Alpine.x.data({
"count": 0,
"increment": RawJS("function() { this.count++; }"),
"reset": RawJS("() => { this.count = 0; }")
})
⚠️ Security Warning: Never use RawJS with user input - it can lead to XSS vulnerabilities.
Escaping & Security
How Escaping Works
- Airpine converts Python values to valid JavaScript
- Air (the framework) handles HTML attribute escaping at render time
- You don't need to pre-escape values
Safe by Default
# Safe - strings are automatically escaped
Alpine.x.data({"message": "User's <script>alert('xss')</script> input"})
# Generates: x-data='{ "message": "User\'s <script>alert(\'xss\')</script> input" }'
# Air escapes this when rendering to HTML
Only Use Raw JS for Functions
# Safe - RawJS for JavaScript code only
Alpine.x.data({
"userInput": user_provided_data, # ✅ Safe - escaped
"handler": RawJS("function() { ... }") # ✅ Safe - your code
})
# NEVER do this:
Alpine.x.data({
"handler": RawJS(f"function() {{ alert('{user_input}'); }}") # ❌ XSS!
})
Merging Attributes
Use Python's | operator to merge attributes:
attrs = (
Alpine.x.data({"count": 0}) |
Alpine.at.click("count++") |
Alpine.x.bind.class_("'active'")
)
Button("Click me", **attrs)
Note: When merging, the last value wins for duplicate keys.
Supported Versions
- Python: ≥ 3.11
- Alpine.js: 3.x
- Air: ≥ 0.30.0
Examples
See examples/demo.py for a complete demo application with:
- Counter
- Toggle visibility
- Form validation
- Dropdowns
- Modals
- Search with debounce
- Tabs
- And more!
Run the demo:
python examples/demo.py
# Visit http://localhost:8001
Development
Setup
# Install with dev dependencies
uv pip install -e ".[dev]"
# Install playwright browsers (for integration tests)
playwright install chromium
Commands (using just)
# Run tests
just test
# Run specific tests
just test-serializer
just test-builders
# Lint and format
just lint
just format
just fix
# Type check
just typecheck
# Run all checks
just check
# Run demo
just demo
License
MIT License - see LICENSE for details.
Contributing
Contributions welcome! Please:
- Add tests for new features
- Run
just checkbefore submitting - Follow existing code style
Links
Metadata
Release files for airpine 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| airpine-0.2.1.tar.gz | 100.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| airpine-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 113.4 kB
Release files / airpine-0.2.1.tar.gz
| Download URL | airpine-0.2.1.tar.gz |
|---|---|
| Size | 100.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dce13674abd974a4ed9b8c215c9c6197ddce63ce027be360a2cb4e41c4d89e01
|
|
BLAKE2b-256 checksum How to use checksums |
cd59c3b6866ba84cf38bebcfaedb806b3551f07f0dd9ce85f0fab587a18be393
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|
Release files / airpine-0.2.1-py3-none-any.whl
| Download URL | airpine-0.2.1-py3-none-any.whl |
|---|---|
| Size | 13.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
ee2a6f63f7392a842a2c6cb6726a1b8b2d71ea0838b668a881c6bee96c512d41
|
|
BLAKE2b-256 checksum How to use checksums |
17ae5b0f02f179da6cff24065acef0081b230334133c1cdb349a4563f70346ef
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.9.2
|