Skip to main content

VarnaPy — reactive Python web UI framework, build beautiful apps without JavaScript

Project description

VarnaPy

Reactive Python web UI framework — build beautiful apps without JavaScript.

PyPI Python CI License: MIT


Why VarnaPy?

Feature VarnaPy Streamlit¹ Gradio¹
Targeted re-renders (no full page rerun)
Multi-page SPA routing partial
Component override system
Third-party plugin ecosystem partial
Dark mode
File upload
Charts (Plotly) partial
DataGrid with sort + pagination partial
WebSocket protocol (sub-6KB JS)

¹ Streamlit and Gradio are trademarks of their respective owners. Feature assessments based on publicly available documentation as of June 2026 and may change with newer releases.


Installation

pip install varnapy

Optional extras:

pip install "varnapy[charts]"   # Plotly Chart component
pip install "varnapy[share]"    # Public tunnel (share=True)
pip install "varnapy[all]"      # Everything

Quickstart

import varnapy as ui

app = ui.App("My App")

@app.page("/")
def home(session):
    count = ui.state(0)

    with ui.Column(gap="md"):
        ui.Heading("Hello from VarnaPy", level=1)
        ui.Button("Click me", on_click=lambda: count.set(count.value + 1))
        ui.Text(lambda: f"Clicked {count.value} times")

app.run()
varnapy run app.py
# → http://127.0.0.1:8000

Core Concepts

Reactive State

# Simple value
count = ui.state(0)
count.set(10)
count.update(lambda v: v + 1)
count.value  # → 11

# Derived / computed
full_name = ui.computed(lambda: f"{first.value} {last.value}")

# Side effects
ui.effect(lambda: save_to_db(query.value))

# Mutable containers
items = ui.list_state(["a", "b"])
items.append("c")

config = ui.dict_state({"debug": False})
config["debug"] = True

Any ReactiveVar read inside a component's render function automatically subscribes that component. When the value changes, only that component re-renders — never the full page.

Component Library

Layout

ui.Row(gap="md")
ui.Column(gap="lg")
ui.Grid(cols=3)
ui.Card(title="Stats", description="Overview")
ui.Tabs(["Overview", "Details", "Settings"])
ui.Modal(open=show_modal, on_close=lambda: show_modal.set(False))
ui.Sidebar(open=sidebar_open)

Input

ui.Button("Save", on_click=save, variant="primary")
ui.TextInput(label="Name", value=name, on_change=lambda v: name.set(v))
ui.NumberInput(label="Amount", value=amt, min=0, max=1000)
ui.Select(label="Status", options=["Draft", "Published"], value=status)
ui.Checkbox(label="Agree", checked=agreed)
ui.Switch(label="Dark mode", checked=dark_mode, on_change=toggle_theme)
ui.Slider(label="Volume", value=vol, min=0, max=100)
ui.FileUpload("Drop files here", multiple=True, on_change=handle_files)

Display

ui.Text("Body text")
ui.Heading("Page Title", level=1)
ui.Metric(label="Revenue", value="$12,400", delta="+8.2%")
ui.Badge("Active", variant="success")
ui.Progress(75.0, label="Upload progress")
ui.Table(data, columns=["name", "email", "status"])
ui.Chart(plotly_figure, height="400px")
ui.Code("print('hello')", language="python")

Feedback & Navigation

ui.Alert("Saved!", variant="success")
ui.Spinner(size="md")
ui.NavMenu(brand="My App")
ui.NavItem("Home", href="/", active=True)
ui.Pagination(page=current_page, total_pages=10)

Advanced

ui.DataGrid(data, columns=[{"key": "name", "label": "Name", "sortable": True}], page_size=20)
ui.CustomComponent()  # base class for custom components with client-side JS

Multi-Page Routing

@app.page("/")
def home(session):
    ui.Heading("Home")
    ui.Button("Go to profile", on_click=lambda: session.navigate("/profile/42"))

@app.page("/profile/{user_id}")
def profile(session, user_id: str):
    ui.Heading(f"Profile: {user_id}")

Navigation is SPA-style — no full page reload.

Dark Mode

dark = ui.state(False)

async def toggle(v: bool) -> None:
    dark.set(v)
    await session.set_theme("dark" if v else "light")

ui.Switch(label="Dark mode", checked=dark, on_change=toggle)

Component Overrides

Replace any built-in component app-wide or per-page:

from varnapy.components._base import Component

class MyButton(Component):
    def render(self) -> str:
        return f'<button {self._attrs("my-btn")} data-flux-event="click">{self._label}</button>'

# App-wide
ui.override("Button", MyButton)

# Single page only
@app.page("/special", overrides={"Button": MyButton})
def special(session): ...

Third-Party Plugins

Publish component libraries to PyPI with a single entry-point:

# your plugin's pyproject.toml
[project.entry-points."varnapy.components"]
my_plugin = "my_package:register"
# my_package/__init__.py
def register(registry):
    registry.add("MyWidget", MyWidget)

VarnaPy discovers installed plugins automatically on startup.


CLI

varnapy run app.py                   # dev server, hot-reload, opens browser
varnapy run app.py --port 3000       # custom port
varnapy run app.py --no-open         # don't open browser
varnapy run app.py --no-reload       # disable hot-reload
varnapy run app.py --prod            # production mode (no dev overlay)
varnapy run app.py --share           # public URL via localtunnel

varnapy new my_app                   # scaffold new project
varnapy export-docker app.py         # generate Dockerfile + docker-compose.yml

Testing

from varnapy.testing import FluxTestClient

def test_counter():
    app = ui.App("test")

    @app.page("/")
    def home(session):
        count = ui.state(0)
        ui.Button("Inc", on_click=lambda: count.set(count.value + 1))
        ui.Text(lambda: str(count.value))

    with FluxTestClient(app) as client:
        client.connect("/")
        assert "0" in client.html()
        client.click("Inc")
        assert "1" in client.html()

Production Deployment

Docker

varnapy export-docker app.py --output ./deploy
docker compose -f deploy/docker-compose.yml up

Manual

pip install "varnapy[all]" gunicorn
gunicorn app:app._fastapi -w 1 -k uvicorn.workers.UvicornWorker -b 0.0.0.0:8000

Note: Use 1 worker per process (sessions are in-process). For horizontal scaling, add Redis-backed session sharing (roadmap).


Examples

Example Description
hello_world.py Counter in 15 lines
multi_page.py SPA routing, DataGrid, Chart, FileUpload

License

MIT — see LICENSE.

Built by Minato3000.

Project details


Download files

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

Source Distribution

varnapy-0.1.1.tar.gz (103.6 kB view details)

Uploaded Source

Built Distribution

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

varnapy-0.1.1-py3-none-any.whl (63.4 kB view details)

Uploaded Python 3

File details

Details for the file varnapy-0.1.1.tar.gz.

File metadata

  • Download URL: varnapy-0.1.1.tar.gz
  • Upload date:
  • Size: 103.6 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for varnapy-0.1.1.tar.gz
Algorithm Hash digest
SHA256 5a9a13d65bd2438bbbe5475422eb544c86688ebb2c31a60c75ca06dccd2de72d
MD5 795d2a59b7bba8b477a4e8deb99ec639
BLAKE2b-256 83cc2380d595f000fb3c8a60c49f8d06e2a9beed4edaf0a878d48f3ed54793a5

See more details on using hashes here.

File details

Details for the file varnapy-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: varnapy-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 63.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.10.12

File hashes

Hashes for varnapy-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2c8f3308e813feb8be2e7b078500a3f6c7acf3cb1b744ac582d67f5d0f3d62e1
MD5 567fd1d5d54e83dbef8fb60afa56a043
BLAKE2b-256 6750b42b408176fe2a0a4b9dcbb3fe65d3aa9073fdfd64982767e6fbc06f86df

See more details on using hashes here.

Supported by

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