millwork
A modern UI framework for Python
A page is a Python function of the session's state. A handler changes the state, the page renders again, and the browser receives a patch. The React bundle ships inside the package, so a project is Python only.
import millwork as mw
from millwork import ui
from pydantic import BaseModel
class State(BaseModel):
clicks: int = 0
app = mw.App("hello", state=State)
@app.page("/")
def home(s: mw.Session[State]) -> ui.Component:
return ui.Page(
ui.Text(f"Clicked {s.state.clicks} times."),
ui.Button("Add one", on_click=lambda s, _: setattr(s.state, "clicks", s.state.clicks + 1)),
)
uv add millwork # or `pip install millwork`
uv run millwork run app.py
What is in the box
- A catalogue of components on shadcn/ui: layout, text, inputs, feedback, tables, charts, forms from Pydantic models, files, tasks with progress, and a chat surface that streams replies from Python.
- One model of state. Python owns the state; a control is owned by Python or by the browser, and the page says which.
- Long work as tasks, with progress, steps and cancellation drawn by the page, and chat replies that stream text, reasoning and tool steps.
- Files over the session's socket. Uploads, downloads and inline images travel on the same connection as everything else, so replicas need no sticky routing and no shared storage.
- Sign-in with password, OAuth and proxy-header providers behind one
on_loginhook, and a theme file in the shadcn token vocabulary for colors, fonts and logos. - A test harness that drives a page without a browser: open it, click, change, submit, and read what the tab would have received.
- One process to run.
millwork runserves an app on a port or a unix socket, behind a proxy and under a path prefix, with a health route for the host.
The manual
The manual is at millwork-ui.com. It is a millwork app: every example in it runs, with the Python code beside it. The chapters cover pages and routing, state and handlers, signing in, theming, testing and running, and every component has a live example. It also runs locally:
uv run millwork docs
Then open http://127.0.0.1:8008.
Status
millwork is still in beta.
Developing millwork
Python is managed with uv, the bundle with pnpm; only bundle/ needs
Node.
uv run pytest -q # the Python suite
uv run mypy src && uv run ruff check src tests
pnpm -C bundle exec tsc --noEmit # the bundle's types
CI=1 pnpm -C bundle exec vitest run # the bundle's unit tests
CI=1 pnpm -C bundle build # rebuilds src/millwork/_bundle/dist
A change to the bundle is committed together with the rebuilt dist, so
every checkout runs without Node. That includes a Dependabot bump of the
bundle's packages: apply it, rebuild, look at the manual, and commit the
dist with it, or CI refuses the branch. The manual's pages under
src/millwork/manual/pages are the gallery a bundle upgrade is judged
against.
Releasing
A release is a tag. Write the version's section in CHANGELOG.md, set the
version with uv version, commit, tag it v plus the version and push the
tag. The release workflow waits for approval, checks the tagged commit,
builds the wheel, publishes it to PyPI and makes the GitHub release from
the changelog section. The published release then deploys the hosted
manual from that version on PyPI; deploy/ holds its image and Fly
configuration, and flyctl deploy ./deploy --build-arg MILLWORK_VERSION=<version> deploys one by hand.
License
Apache-2.0.
Metadata
Release files for millwork 0.1.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| millwork-0.1.1-py3-none-any.whl | Python 3 | none | any | Details for millwork-0.1.1-py3-none-any.whl |
Release files / millwork-0.1.1-py3-none-any.whl
| Download URL | millwork-0.1.1-py3-none-any.whl |
|---|---|
| Size | 1.2 MB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3693e3ba9a7ba97913a8e773449d2c87bec897ee4e837599ba3d7f1ceee602f3
|
|
BLAKE2b-256 checksum How to use checksums |
b1feaf0f64a5b78f21b7aa2dcb73b82bbb50cb1922561833d1295fc959a56a80
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logSecurity for millwork 0.1.1
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 1 file in this release has been uploaded with provenance metadata.
Attestation for millwork-0.1.1-py3-none-any.whl
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Nov 2, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.