Full-stack web apps in one Python file.
Server-rendered pages, a reactive UI compiled from Python, typed calls to your server, Models with migrations, forms, accounts, live updates, background jobs and one-command deploys, with no JavaScript toolchain.
Documentation · Try it in your browser · Examples · Changelog
from pyweb import App, server
app = App(title="Guestbook")
ENTRIES = []
@server
def sign(name: str) -> list:
ENTRIES.append(name.strip() or "anonymous")
return ENTRIES
@app.page("/")
def Home():
entries = list(ENTRIES) # computed on the server per request
name = "" # bound to the input: browser state
def submit(): # compiled to JavaScript
entries = sign(name) # typed RPC to the server
name = ""
<main>
<h1>Guestbook ({len(entries)})</h1>
<form onsubmit={submit}>
<input bind={name} placeholder="Your name" />
<button>Sign</button>
</form>
<ul>
for entry in entries:
<li>{entry}</li>
</ul>
</main>
pip install pyweb-stack # imported as `pyweb`
pyweb dev app.pyweb # http://localhost:8000, with live reloadThat's the whole app. The page is rendered on the server with real data,
the handler becomes a few hundred bytes of JavaScript, and sign() becomes
a validated JSON endpoint the browser calls for you.
from pyweb import App, Field, Form, Input, Model, Submit, server
from pyweb.authkit import User
app = App(title="Feedback", database="sqlite:///app.db")
auth = app.use_auth() # sign-up, passkeys, OAuth, /admin
class Idea(Model):
text: str = Field(min=3, max=280)
votes: int = 0
author: User = Field(readonly=True)
def own(user, idea):
return user is not None and idea.author_id == user.id
Idea.policy(write=own) # only its author can change an idea
@server(login=True)
def share(idea: Idea): # the form's fields come from Idea
idea.author = auth.user()
idea.save()
thank.enqueue(idea.id) # runs after the request commits
@app.job(retries=3)
def thank(idea_id: int):
print(f"thanks for idea {idea_id}")
@app.page("/", login=True)
def Ideas():
ideas = Idea.query().order("-id").live() # every open tab
<Form action={share}>
<Input name="text" placeholder="Your idea" />
<Submit>Share</Submit>
</Form>
<ul>
for idea in ideas:
<li>{idea["text"]} ({idea["votes"]})</li>
</ul>
That's accounts (sign-up with email confirmation, passkeys, OAuth, an
admin), a table with rules and a row policy, a form that checks what
people type in the browser and again on the server (and works without
JavaScript), a background job queued in the same transaction, and a list
that updates in every open tab when anyone shares an idea. Start a bigger
one with pyweb new myapp --template saas, or follow
a SaaS in an hour.
You write one file. The compiler decides, line by line, what runs in the
browser and what stays on the server, and tells you why
(pyweb inspect). Variables your handlers change become reactive state;
everything else is computed on the server. Database handles, imports and
secrets can't leak into browser code: that's a compile error with a line
number, not a production incident.
from pyweb import App, Field, Model
app = App(database="sqlite:///shop.db") # DATABASE_URL takes over in production
class Order(Model):
item: str = Field(max=80)
status: str = Field(default="new", choices=["new", "paid", "shipped"])
@app.page("/orders")
def Orders():
orders = Order.query().order("-id").limit(20).live()
<ul>
for o in orders:
<li>{o["item"]}: {o["status"]}</li>
</ul>
Models work on SQLite, Postgres and MySQL, with relations loaded by
include() (an N+1 loop is an error while you develop), cursor
pagination, transactions per request and read replicas.
pyweb db diff writes the migration when a Model changes and
pyweb db upgrade applies it, under a lock so many servers can start at
once. .live() keeps the list in step with the database in every open
tab: when one row changes, one row is sent. Raw SQL works too
(live(db, "select ...")). Data → ·
Live data →
from pyweb import App, Markdown, server
app = App()
@server
def answer(question: str):
for piece in ("**Streaming**", " from", " the", " server."): # your model's tokens
yield piece
@app.page("/")
def Ask():
question = ""
reply = ""
stream = None
async def ask():
reply = ""
stream = answer(question)
async for piece in stream:
reply += piece
def stop():
if stream:
stream.cancel()
<input bind={question} />
<button onclick={ask}>Ask</button>
<button onclick={stop}>Stop</button>
<Markdown text={reply} />
yield on the server, async for in the browser. Stop really stops:
the generator on the server is closed, so the model call ends too.
<Markdown> is safe on model output. Start from
pyweb new mychat --template ai-chat (Anthropic, any OpenAI-compatible
server, or a built-in demo model). Building AI apps →
from pyweb import App
app = App(title="Plant shop")
@app.layout
def Shell(children):
cart = 0
def add():
cart += 1
<nav><a href="/">Home</a> <a href="/plants">Plants</a> <button onclick={add}>Cart {cart}</button></nav>
{children}
@app.page("/plants", description="Every plant we sell")
def Plants(light: str = "", max_price: int = 0): # typed query parameters
<h1>Plants</h1>
Layouts keep their state while links load without a full page reload
(with prefetching and scroll restore). Links to the current page are
marked automatically, every page gets its own title, description and
social tags, and 404/500 pages are written in .pyweb too.
Layouts & navigation →
pyweb add chart.js/autoChart = npm("chart.js/auto")PyWeb downloads the package from the npm registry, checks its checksum,
keeps only the files the browser loads and pins them in pyweb.lock.
There's no node_modules and no bundler, and pages that don't use a
package don't load it. npm packages →
pyweb deploy fly # or docker, compose, k8s, render, railwaypyweb deploy reads your app and explains its plan: Postgres instead of
SQLite, a worker for jobs, Redis when there's more than one server,
migrations before the new version takes traffic. Then it writes the
files. In production every log line is JSON with the request id and
trace id, /metrics answers Prometheus, OpenTelemetry traces follow a
click into its server function, SQL and background job, and while you
develop a toolbar on every page shows each request's queries and flags
N+1 loops. Scaling & deploying → ·
Logs, metrics & tracing →
- Every page is server-rendered: complete HTML on first paint, then hydrated in place. Pages without interactivity ship no JavaScript.
- Small: a typical interactive page is under 1 KB of code plus a ~14 KB (gzip) runtime that's cached across pages. Forms and live updates add a few KB, only on pages that use them.
- Its own server:
pyweb servehandles HTTP/1.1 keep-alive and WebSockets on one asyncio loop per process, with--workers Nprocesses. Sessions are signed cookies, so servers stay stateless behind any load balancer. uvicorn and other ASGI servers work too. - Secure defaults: escaped output, a hashed Content Security Policy,
CSRF and origin checks, row policies, rate limits, request size limits
and
pyweb check --productionbefore you deploy.
| Live dashboard | Streaming AI chat | Multi-page site |
|---|---|---|
![]() |
![]() |
![]() |
live() queries + Chart.js from npm |
yield + Stop + <Markdown> |
layout, query parameters, 404 page |
Or open the playground: it runs the real PyWeb, server functions included, in your browser.
pip install pyweb-stack
pyweb new myapp --template saas # saas | blank | counter | todo | blog | auth | chat | ai-chat
cd myapp
pyweb dev app.pyweb # edit app.pyweb; the page reloads on saveThen:
pyweb check app.pyweb # compile + security checks (CI-friendly exit codes)
pytest # new apps come with test_app.py
pyweb db diff # a migration for what changed in your Models
pyweb deploy compose # or fly, render, railway, k8s, dockerThe quickstart and tutorial take it from there.
app.pyweb
│
┌───────┴────────┐ compiler: parses Python + markup, decides
▼ ▼ where each name runs, checks the boundary
┌─────────────────┐ ┌─────────────────────────┐
│ browser module │ │ server │
│ signals, DOM │ │ page bodies, @server │
│ updates, your │ │ functions, database, │
│ handlers in JS │ │ sessions, secrets │
└────────┬────────┘ └────────────┬────────────┘
└──── typed JSON RPC ────┘
(streams over HTTP; live data over one WebSocket per tab)
The architecture overview goes through each stage.
PyWeb ships an MCP server so AI assistants can scaffold, check, inspect, render, screenshot and test your app, add npm packages, map its routes, and look at its data: the schema, read-only queries (row-limited, secrets redacted), pending migrations, jobs and recent requests. Errors come back as line numbers with fix hints:
claude mcp add pyweb -- pyweb mcp # Claude Code{ "mcpServers": { "pyweb": { "command": "pyweb", "args": ["mcp"] } } }(the JSON is for Cursor, Claude Desktop, VS Code, Windsurf and other MCP
clients). New projects include AGENTS.md and CLAUDE.md, and the docs
site publishes llms-full.txt.
AI assistants & MCP →
The VS Code extension adds highlighting, errors as
you type, hover that shows where code runs (and the signatures of npm
packages), completion that knows your Models' fields and query methods,
warnings for N+1 loops and form fields the action doesn't take, a code
lens that writes the migration when a Model changes, and go to
definition. Any other LSP editor can run pyweb lsp
(setup).
Each runs with pyweb dev examples/<name>/app.pyweb and is tested in a
real browser by the test suite.
| Example | Shows |
|---|---|
saas |
accounts, Models with migrations, row policies, forms, live lists, a job, a daily email, an admin |
counter |
signals, computed values, binding a number input |
todo |
components, list mutation, filters, keyed lists |
blog |
a Model, server functions, route parameters, 404s, validation errors |
auth |
your own registration, password hashing and sessions (or use app.use_auth()) |
chat |
route parameters, shared server state, live updates with publish/subscribe |
showcase |
everything on one page, with a stylesheet |
site |
a layout, client-side navigation, query parameters, page titles, a 404 page |
dashboard |
live queries and a Chart.js chart from npm, in every open window |
ai-chat |
streaming AI replies with Stop and Markdown (Anthropic, OpenAI-compatible or a demo model) |
| Start | Introduction · Quickstart · Tutorial · A SaaS in an hour · AI assistants & MCP |
| Language | .pyweb files · State & reactivity · Python in the browser · npm packages |
| Server | Server functions & RPC · Pages & routing · Layouts & navigation · Data · Forms · Auth · Live data · Background jobs · Building AI apps |
| Ship | Testing · Deployment · Scaling · Logs, metrics & tracing · Security · CLI · Upgrading to 0.5 |
| Reference | Recipe: wallets & web3 · Toolkit & stability · Limitations & roadmap · Architecture · Changelog |
The same docs are published at maanavkrishna.github.io/PyWeb, with compiler output shown next to each example.
Good fit: internal tools, admin panels, dashboards, CRUD apps, AI features, small SaaS products and content sites with interactive parts, built by people who'd rather stay in Python.
Not a fit: large client-heavy single-page apps built around a JavaScript component framework (use React, Svelte or Vue), or running scientific Python in the browser (use Pyodide or PyScript). See the introduction and the current limitations.
PyWeb is in beta (0.x). The language, server API, RPC protocol and CLI
are documented and tested, and changes to them are announced in the
changelog (see stability).
Upgrade with pip install -U pyweb-stack.
| Version | Highlights |
|---|---|
| 0.5 | Models with migrations, forms, auth kit with passkeys, OAuth and an admin, row-level live updates over WebSockets on PyWeb's own server, durable jobs and schedules, pyweb deploy for docker, compose, k8s, fly, render and railway, logs, metrics, tracing and a dev toolbar, data-aware editor and MCP tools, the saas template, pyweb upgrade |
| 0.4 | npm packages without Node, layouts and client-side navigation, streaming server functions and <Markdown> for AI apps, live queries, page head tags, .pyweb error pages; 0.4.1: richer MCP tools; 0.4.2: VS Code run, new-app and MCP commands; 0.4.3: fixes, security hardening, gzip and caching; 0.4.4: key rotation, session revocation, typed arguments, Redis-shared limits, check --production |
| 0.3 | Hydration, live updates (SSE), multi-file apps, language server + VS Code extension, browser playground, faster rendering, screenshot/test MCP tools |
| 0.2 | MCP server for AI assistants, AI guide, project templates, AGENTS.md/CLAUDE.md, llms.txt |
| 0.1 | First public release: compiler, reactive runtime, server rendering, typed RPC, sessions, databases, CLI |
The test suite (about 1,200 tests) covers the parser, the
Python→JavaScript translation (differentially, against CPython), the
reactive runtime, server rendering, RPC, forms, Models and migrations
(with property tests), the HTTP and WebSocket parsers (fuzzed), live
patches, jobs on every backend, every example app in Chromium, the
playground on Pyodide, and the whole suite against real Postgres, MySQL
and Redis servers, on Python 3.10–3.13. CI also boots a
pyweb deploy compose stack and runs the VS Code extension in a real
VS Code.
Issues and pull requests are welcome: see CONTRIBUTING.md. Security reports: SECURITY.md.
Built by MaanavKrishna. MIT licensed.


