Skip to main content

The Full-Stack Web Framework for Pythonistas.

PyPI - Version PyPi - Python Version PyPi - Downloads (Monthly) Github - Commits

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:

  1. 🎨 Styling Layer: Generate CSS rules programmatically with a fluent, pythonic API. Includes an Atomic CSS engine that generates utility classes on the fly.
  2. 🧱 UI Layer: Build reusable HTML components with a fluent builder pattern. Type-safe, refactor-friendly, and composable.
  3. ⚡ Logic Layer: Write server-side and client-side code in the same file. violetear handles 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 a Python→JS compiler (no Pyodide, no WASM, no 14MB download).

📦 Installation

To use the core library (HTML/CSS generation only), install the base package:

pip install violetear

To build full-stack applications (with the App Engine and Server), install the server extras:

pip install "violetear[server]"

🚀 Quickstart: The Isomorphic Counter

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.

1. Initialize the App

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")

2. Define Styles (CSS-in-Python)

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")

3. Server Logic (RPC)

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"}

4. Client Logic (In-Browser Python)

Define the interactivity. We use @app.client.on("ready") to restore state when the page loads and everything is setup.

v2.0: Client-side Python is now compiled to JavaScript at server startup. Import browser APIs from violetear.js — these are type-correct stubs for IDE/mypy support that become JS globals in the browser.

@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.js import DOM, localStorage

    # We can access storage like an object!
    saved_count = localStorage.count
    if saved_count is not None:
        DOM.find("display").text = str(saved_count)

And we use @app.client.callback to handle user interactions. The code is compiled to JavaScript at server startup — no Pyodide, no 14MB WASM download. We can also call server-side functions seamlessly, via automagic RPC (Remote Procedure Call).

from violetear.js import Event

@app.client.callback
async def handle_change(event: Event):
    """
    Compiled to JavaScript and runs in the browser on click.
    """
    from violetear.js import DOM, localStorage

    # A. Get current state from DOM
    display = DOM.find("display")
    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)
    localStorage.count = new_value

    # E. Sync with Server (Background)
    await report_count(current_count=new_value, action=action)

5. The UI (Server-Side Rendered)

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!

✨ Features

🎨 Powerful Styling Engine

  • 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 & SemanticDesign included.

🧱 Component System

  • Declarative Builder: Create HTML structures without writing HTML strings.
  • Reusability: Subclass Component to create reusable widgets (Navbars, Cards, Modals) that encapsulate their own structure and logic.

⚡ Full-Stack Application Engine

  • Hybrid Architecture: Supports both Server-Side Rendering (SSR) for SEO and speed, and Client-Side Rendering (CSR) for interactivity.
  • Python→JS Compiler: Client-side Python functions (@app.client.*) are compiled to JavaScript at server startup via an AST compiler. No Pyodide, no WASM, no 14MB download.
  • Browser API stubs: violetear.js provides type-correct Python stubs (DOM, localStorage, sessionStorage, sleep, fetch) for IDE and mypy support. Every export is a JS global in the runtime.
  • Asset Management: Stylesheets created in Python are served directly from memory.
  • Seamless RPC: Call server functions from the browser as if they were local.

📱 Progressive Web App (PWA) Support

Violetear allows you to turn any route into an installable PWA. This enables your app to:

  1. Be Installed: Users can add it to their home screen (mobile/desktop).
  2. Work Offline: The app shell and assets are cached automatically.
  3. Auto-Update: Changes to your Python code are detected, ensuring users always see the latest version.

How to Enable PWA

Simply pass pwa=True (or a Manifest object) to the @app.view 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.

Caching Strategy

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.js?v=2.0.0). This ensures instant loading while guaranteeing updates when the version changes.

Current Limitations

  • 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.

📡 Real-Time: Server Broadcasts

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.

1. Define the 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 is compiled to JS and runs in the User's Browser
@app.client.realtime
async def update_alert(message: str, color: str):
    from violetear.js import DOM

    # Update the DOM immediately
    el = DOM.find("status-message")
    el.text = message
    el.style(color=color)

2. Call it from the Server

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, to greet every client as it joins (the connect event fires per-client after startup, when there's a live socket to deliver to):

# Fires once per client, right after their websocket comes up
@app.server.on("connect")
async def greet(client_id: str):
    await update_alert.invoke(
        client_id,
        message="Welcome!",
        color="green",
    )

Note: Don't broadcast from @app.server.on("startup"). At startup the server has zero active websocket connections — clients only reconnect after startup completes — so any broadcast there silently no-ops.

3. Handle Connections

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.

🌐 Shared State: Multiplayer in One Decorator

@app.shared makes collaborative features trivial. Decorate a @dataclass with @app.shared and every field assignment — from any client or from server code — automatically broadcasts to all connected clients and updates their DOM.

No manual broadcast calls. No request_history. No receive_history. No @app.client.on("connect") to request a state dump. Everything is automatic.

from dataclasses import dataclass, field
from violetear import App
from violetear.js import Event

app = App(title="Shared Counter")

@app.shared
@dataclass
class Room:
    count: int = 0
    users: dict = field(default_factory=dict)
    # server_only=True → clients can read but cannot write
    version: str = field(default="1.0", metadata={"server_only": True})

@app.server.on("connect")
async def on_join(client_id: str):
    Room.users = {**Room.users, client_id: f"anon-{client_id[:6]}"}

@app.server.on("disconnect")
async def on_leave(client_id: str):
    users = dict(Room.users)
    users.pop(client_id, None)
    Room.users = users

@app.client.callback
async def on_click(event: Event):
    # Sends shared_set to server → server validates → broadcasts to ALL tabs
    Room.count = Room.count + 1

Open two browser tabs. Click the button in one. The counter updates in both — instantly, with zero extra wiring.

How it works: Room is a SharedProxy singleton on the server. Any field write intercepts __setattr__, which broadcasts a shared_sync WebSocket frame to every connection. On connect, the server pushes the full current state before the application's on("connect") handler fires. Client setters send shared_set to the server; the server is the source of truth and re-broadcasts to all clients (including the sender). The server_only metadata prevents clients from writing privileged fields.

🛣️ Roadmap

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 generate manifest.json and 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.

🤝 Contribution

violetear is open-source and we love contributions!

  1. Fork the repo.
  2. Install dependencies with uv sync --extra server.
  3. Run tests with make (or make test-unit).
  4. Submit a PR!

📄 License

MIT License. Copyright (c) Alejandro Piad.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

violetear-1.4.0.tar.gz (1.6 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

violetear-1.4.0-py3-none-any.whl (952.0 kB view details)

Uploaded Python 3

File details

Details for the file violetear-1.4.0.tar.gz.

File metadata

  • Download URL: violetear-1.4.0.tar.gz
  • Upload date:
  • Size: 1.6 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for violetear-1.4.0.tar.gz
Algorithm Hash digest
SHA256 0e2aea6e4e2b960104499648747c54f08ef7842fbc757ae270450b4a5ce7b7d7
MD5 c9fcb2193fa4c6675b1383d4168c7aa9
BLAKE2b-256 d48660a00e2c74e48fbf8b3ffb61c6039fc1660e196afb3ba5553c936116d23c

See more details on using hashes here.

File details

Details for the file violetear-1.4.0-py3-none-any.whl.

File metadata

  • Download URL: violetear-1.4.0-py3-none-any.whl
  • Upload date:
  • Size: 952.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.29 {"installer":{"name":"uv","version":"0.11.29","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

File hashes

Hashes for violetear-1.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 0c67eeb9aa95583235679bf785dc173830aba7ee48c92a539d173e386faa3e52
MD5 3bafc23ccd12265b1e3dc9c5f01538de
BLAKE2b-256 33416c123836dca920d6dca4334229a247b0c36e87172dc430368890d1a42fba

See more details on using hashes here.

Release history Release notifications | RSS feed

1.4.1

2 files

This release

1.4.0 This release

2 files

1.3.0

2 files

1.2.4

2 files

1.2.3

2 files

1.2.2

2 files

1.2.1

2 files

1.2.0

2 files

1.1.0

2 files

1.0.1

2 files

1.0.0

2 files

0.15.0

2 files

0.12.3

2 files

0.12.2

2 files

0.12.1

2 files

0.12.0

2 files

0.11.1

2 files

0.11.0

2 files

0.10.4

2 files

0.10.3

2 files

0.10.2

2 files

0.10.1

2 files

0.10.0

2 files

0.9.0

2 files

0.8.1

2 files

0.8.0

2 files

0.7.0

2 files

0.6.0

2 files

0.5.1

2 files

0.5.0

2 files

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

0.1.1

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page