The Full-Stack Web Framework for Pythonistas.
violetear is a minimalistic yet fully capable framework for building modern web applications in pure Python. It eliminates the context switch between backend and frontend by allowing you to write your styles, your markup, and your client-side logic all in the language you love.
It features a unique 3-layer architecture:
- π¨ Styling Layer: Generate CSS rules programmatically with a fluent, pythonic API. Includes an Atomic CSS engine that generates utility classes on the fly.
- π§± UI Layer: Build reusable HTML components with a fluent builder pattern. Type-safe, refactor-friendly, and composable.
- β‘ Logic Layer: Write server-side and client-side code in the same file.
violetearhandles the compilation, bundling, RPC bridges, and state persistence seamlessly.
Use it for anything: from a simple script to generate a CSS file, to a static site generator, all the way up to a full-stack Isomorphic Web App powered by FastAPI and Pyodide.
To use the core library (HTML/CSS generation only), install the base package:
pip install violetearTo build full-stack applications (with the App Engine and Server), install the server extras:
pip install "violetear[server]"Let's build a fully interactive "Counter" app. The state persists across reloads using Local Storage, updates instantly in the browser via the DOM API, and syncs with the server via RPC.
Zero JavaScript required.
First, we create the application instance. This wraps FastAPI to provide a powerful client-server isomorphic engine.
from violetear import App
app = App(title="Violetear Counter")Instead of writing CSS strings, use the fluent API to define your theme.
from violetear import StyleSheet
from violetear.color import Colors
from violetear.style import Style
# Create a global stylesheet
style = StyleSheet()
style.select("body").background(Colors.AliceBlue).font(family="sans-serif") \
.flexbox(align="center", justify="center").height("320px").margin(top=20)
style.select(".counter-card").background(Colors.White).padding(40).rounded(15) \
.shadow(blur=20, color="rgba(0,0,0,0.1)").text(align="center")
style.select(".count-display").font(size=64, weight="bold").color(Colors.SlateBlue).margin(10)
style.select("button").padding("10px 20px").font(size=20, weight="bold") \
.margin(5).rounded(8).border(0).rule("cursor", "pointer").color(Colors.White)
style.select(".btn-plus").background(Colors.MediumSeaGreen)
style.select(".btn-minus").background(Colors.IndianRed)
style.select(".btn:hover").rule("opacity", "0.8")Define a function that runs on the server. The @app.server.rpc decorator exposes this function so your client code can call it directly.
@app.server.rpc
async def report_count(current_count: int, action: str):
"""
This runs on the SERVER.
FastAPI automatically validates that current_count is an int.
"""
print(f"[SERVER] Count is now {current_count} (Action: {action})")
return {"status": "received"}Define the interactivity. We use @app.client.on("ready") to restore state when the page loads and everything is setup.
@app.client.on("ready")
async def init_counter():
"""
Runs automatically when the page loads (Client-Side).
Restores the counter from Local Storage so F5 doesn't reset it.
"""
from violetear.dom import Document
from violetear.storage import store
# We can access storage like an object!
saved_count = store.count
if saved_count is not None:
Document.find("display").text = str(saved_count)
print(f"Restored count: {saved_count}")And we use @app.client.callback to handle user interactions and run Python code in the browser. Check out the Pythonic API for interacting with the DOM and the LocalStorage. We can also call server-side functions seamlessly, via automagic RPC (Remote Procedure Call).
from violetear.dom import Event
@app.client.callback
async def handle_change(event: Event):
"""
Runs in the browser on click.
"""
from violetear.dom import Document
from violetear.storage import store
# A. Get current state from DOM
display = Document.find("display")
# We can read/write text content directly
current_value = int(display.text)
# B. Determine action
action = event.target.id # "plus" or "minus"
new_value = current_value + (1 if action == "plus" else -1)
# C. Update DOM immediately (Responsive)
display.text = str(new_value)
# D. Save to Local Storage (Persistence)
# This automatically serializes the value to JSON
store.count = new_value
# E. Sync with Server (Background)
await report_count(current_count=new_value, action=action)Finally, create the route that renders the initial HTML. We attach the style and bind the Python function to the button's click event.
from violetear.markup import Document, HTML
@app.view("/")
def index():
doc = Document(title="Violetear Counter")
# Auto-serve our generated CSS at this URL
doc.style(style, href="/style.css")
doc.body.add(
HTML.div(classes="counter-card").extend(
HTML.h2(text="Isomorphic Counter"),
# The Count
HTML.div(id="display", classes="count-display", text="0"),
# Controls - Both call the same Python function
HTML.button(id="minus", text="-", classes="btn-minus btn").on(
"click", handle_change
),
HTML.button(id="plus", text="+", classes="btn-plus btn").on(
"click", handle_change
),
HTML.p(text="Refresh the page! The count persists.").style(
Style().color(Colors.Gray).margin(top=20)
),
)
)
return doc
if __name__ == "__main__":
app.run()Run it with python main.py and open http://localhost:8000. You have a full-stack, styled, interactive app with persistence in 70 lines of pure Python!
- Fluent API:
style.select("div").color(Colors.Red).margin(10) - Type-Safe Colors: Built-in support for RGB, HSL, Hex, and a massive library of standard web colors (
violetear.color.Colors). - Presets:
- Atomic CSS: A complete Tailwind-compatible utility preset. Generate thousands of utility classes (
p-4,text-xl,hover:bg-red-500) purely in Python. FlexGrid&SemanticDesignincluded.
- Atomic CSS: A complete Tailwind-compatible utility preset. Generate thousands of utility classes (
- Declarative Builder: Create HTML structures without writing HTML strings.
- Reusability: Subclass
Componentto create reusable widgets (Navbars, Cards, Modals) that encapsulate their own structure and logic.
- Hybrid Architecture: Supports both Server-Side Rendering (SSR) for SEO and speed, and Client-Side Rendering (CSR) for interactivity.
- Pythonic DOM: A wrapper (
violetear.dom) that provides a clean, type-safe Python API for DOM manipulation in the browser. - Smart Storage: A Pythonic wrapper (
violetear.storage) aroundlocalStoragethat handles JSON serialization automatically and allows attribute access (store.user.name). - Asset Management: Stylesheets created in Python are served directly from memory.
- Seamless RPC: Call server functions from the browser as if they were local.
Violetear allows you to turn any route into an installable PWA. This enables your app to:
- Be Installed: Users can add it to their home screen (mobile/desktop).
- Work Offline: The app shell and assets are cached automatically.
- Auto-Update: Changes to your Python code are detected, ensuring users always see the latest version.
Simply pass pwa=True (or a Manifest object) to the @app.route decorator.
Important: You must define an app version. If you don't, Violetear generates a random one on every restart, which will force users to re-download the app every time you deploy.
# 1. Set a version string (e.g., from git commit or semantic version)
app = App(title="My App", version="v1.0.2")
# ...
# 2. Enable PWA on your desired route
@app.view("/", pwa=Manifest(
name="My Super App",
short_name="SuperApp",
description="An amazing Python PWA",
theme_color="#6b21a8"
))
def home():
return Document(...)The "application" cache is defined per route (@app.view), so you can have multiple PWAs served from the same violetear application. You can setup some routes for delivering PWA-enables app while other routes serve server-side rendered documents or standard (non-PWA) dynamic documents. You can match and mix as you wish.
Violetear uses a hybrid strategy to ensure safety and speed:
- Navigation (HTML): Network-First. It tries to fetch the latest version from the server. If offline, it falls back to the cache.
- Assets (JS/CSS): Cache-First. Assets are versioned (e.g.,
bundle.py?v=1.0.2). This ensures instant loading while guaranteeing updates when the version changes.
- Push Notifications: Not yet supported, and unclear if we ever will.
- Background Sync: Offline actions (like submitting a form while disconnected) are not automatically retried when online. You must handle connection errors manually in your client logic. At some time we may provide a standard mechanism for queueing this type of actions.
Violetear supports Reverse RPC, allowing the server to call functions running in the user's browser. This is perfect for real-time notifications, live feeds, or multiplayer games.
The magic happens via the .broadcast() method available on any @app.client function.
Create a function decorated with @app.client.realtime. This code will be compiled and run in the browser, but the server "knows" about it and can invoke it.
# This function runs in the User's Browser
@app.client.realtime
async def update_alert(message: str, color: str):
from violetear.dom import Document
# Update the DOM immediately
el = Document.find("status-message")
el.text = message
el.style(color=color)Now the server code can call update_alert.invoke(...) for any specific client.
You can get the appropriate client ID via @app.server.on("connect") handlers.
You can also call it for all connected clients using .broadcast(). For example, if the server shutdowns and restarts later on, you can inform your clients like this:
import asyncio
# Register the background task using standard FastAPI lifespan
@app.server.on("startup")
async def start_monitor():
# A background task running on the server
await update_alert.broadcast(
message="Server is alive!",
color="green"
)You can hook into WebSocket lifecycle events to track users or trigger actions when they join or leave.
@app.server.on("connect")
async def on_join(client_id: str):
print(f"Client {client_id} connected.")
# You could broadcast a "User Joined" message here
await update_alert.broadcast(f"User {client_id} joined!", "blue")
@app.server.on("disconnect")
async def on_leave(client_id: str):
print(f"Client {client_id} left.")Similarly, you can hook @app.client.on("connect") and "disconnect" events to execute client-side code whenever the client websocket connects and disconnects.
The long-term vision for Violetear is to become a Python-native, full-stack, production-ready web framework. Here are some of the currently planned features:
- π± Progressive Web Apps (PWA): Simply pass
@app.route(..., pwa=True)to automatically generatemanifest.jsonand a Service Worker. - π‘ Reverse RPC (Broadcast and Invoke): Invoke client-side functions from the server via websockets.
- π₯ JIT CSS: An optimization engine that scans your Python code and serves only the CSS rules actually used by your components.
- π§ SPA Engine: An abstraction (
violetear.spa) for building Single Page Applications. - π Client-Side Routing: Define client-side routes that render specific components into a shell without reloading the page.
- π Partial Views: Define server views that render only partial documents, which can be injected into the client-side DOM dynamically.
- ποΈ
@app.local: Reactive state that lives in the browser (per user). Changes update the DOM automatically. - π
@app.shared: Real-time state that lives on the server (multiplayer). Changes are synced to all connected clients via WebSockets.
violetear is open-source and we love contributions!
- Fork the repo.
- Install dependencies with
uv sync. - Run tests with
make test. - Submit a PR!
MIT License. Copyright (c) Alejandro Piad.
