Skip to main content

Pyrite

Code websites in Python 3, for JavaScript and CSS haters.

Want the short version? Start here: button colors, styling, first page.

Pyrite is a small, dependency-free starting point: compose Python components, return a page, and serve HTML with built-in responsive styling. Requires Python 3.10+.

Install from PyPI

Create a folder for your website, then use a virtual environment. This also avoids Arch Linux's externally-managed-environment error.

python3 -m venv .venv
.venv/bin/python -m pip install pyrite-web

The package name on PyPI is pyrite-web; the Python import is pyrite. On Windows, replace .venv/bin/python with .venv\Scripts\python.exe.

Your first website

Save this as app.py in your project folder:

from pyrite import App, Button, Column, Heading, Page, Text

app = App()

@app.page("/")
def home():
    return Page(Column(
        Heading("My first Pyrite website."),
        Text("Written in Python. No JavaScript or CSS files."),
        Button("About this site", "/about"),
    ), title="My website")

@app.page("/about")
def about():
    return Page(Column(
        Heading("About"),
        Text("Another URL. Another Python function."),
        Button("Back home", "/"),
    ), title="About")

if __name__ == "__main__":
    app.run()

Run it, then open http://127.0.0.1:8000:

.venv/bin/python app.py

Stop with Ctrl+C. After editing Python files, restart the server and refresh your browser. Use app.run(port=8001) if port 8000 is already occupied.

For your own shared style, create styles.py next to app.py:

from pyrite import Gradient, Theme

theme = Theme(
    background="#111820",
    text="#eef3f8",
    heading_font="sans",
    radius=16,
    button_radius=999,
    gradient=Gradient(
        colors=("#111820", "#214a42"),
        moving=True,
        duration=14,
    ),
)

Import theme with from styles import theme, then pass theme=theme to each Page(...). The root styles.py in this repository is an example, not a file installed into your project by pip.

The sections below cover components, forms, HTTP calls, animation, and chat. The demo URLs such as /try and /chat belong to this repository's example.py; installing the library does not automatically add these routes to your app.

Run the example

python3 example.py

Open http://127.0.0.1:8000. The example is Pyrite's first website: a homepage with a code/output comparison, quickstart, expandable FAQ, and a component guide at /guide. Try /playground for a Python-powered calculator. Its application source uses only Python components. No installation is required when running from this repository.

Build a page

from pyrite import App, Column, Heading, Page, Text

app = App()

@app.page("/")
def home():
    return Page(
        Column(Heading("Hello, Pyrite."), Text("Just Python.")),
        title="My website",
    )

if __name__ == "__main__":
    app.run()

Available components: Heading, Text, Column, Row, Card, Link, and Button, Section, Nav, Code, and Details. Nest components directly. Section(..., id="start") creates an anchor target. Code(source, label="Python") displays escaped source. Details(question, ...) provides a native expandable answer. Heading accepts level=1 through 6. Link(label, href) and Button(label, href) navigate to another URL. Text and HTML attributes are escaped automatically.

Register exact paths with @app.page("/about"). Handlers take no arguments and return a Page. Query strings do not affect route matching. Page accepts title and language; app.run accepts host and port.

Current scope

This version serves server-rendered pages with native GET forms and JSON POST callbacks through LiveForm and Chat. It does not include server sessions, token streaming, live reload, or a production server. The built-in HTTP server is for local development. Styling is supplied by Pyrite; applications do not need CSS files or JavaScript tooling.

Styling in Python

Edit the root styles.py to change the example website's shared theme. Restart the server after editing. Theme supports six-digit hex colors, sans, serif, or mono fonts, page width, corner radius, and spacing. Apply a theme to any page:

from pyrite import Theme

theme = Theme(background="#111820", text="#eef3f8", heading_font="sans")
page = Page(Heading("Your style."), theme=theme)

Forms without JavaScript

from pyrite import Form, Input, Submit, query

@app.page("/hello")
def hello():
    return Page(Column(
        Form(Input("name", "Your name", value=query("name")), Submit("Say hello")),
        Text(f"Hello, {query('name', 'world')}!"),
    ))

Slider(name, label, value=5, minimum=1, maximum=10) and Select(name, label, {"value": "Label"}, value="value") also work in forms. Submission reloads the page; query reads request-local URL parameters. GET forms are for read-only calculations and searches; their values appear in the URL.

Try Pyrite in Browser

Visit /try to edit Python and render a real Page without installing Python. Assign your result to page; do not call app.run(). The editor loads Pyodide from jsDelivr and runs code in a disposable Web Worker, with a 10-second execution limit and an isolated preview. First download can take up to two minutes. Code is not executed on the application server. Browser networking follows CORS rules; the standard-library HTTP client below is for server-side Python.

Images and motion

from pyrite import Image, Motion
from styles import hero_image

banner = Motion(Image(hero_image, "Mountain landscape"), effect="rise")

Choose rise, fade, or float. In styles.py, set motion=False to disable component animations, or change motion_duration (100–5000 milliseconds) for entrances. Reduced-motion browser preferences disable animations automatically. Theme(background_image="https://example.com/background.jpg") attaches a page background. Image accepts relative or HTTP(S) URLs and requires alt text. External images load directly from their host; local file serving is not included.

Requests and changing API data

from pyrite import requests

data = requests.get(
    "https://api.mymemory.translated.net/get",
    params={"q": "Good morning", "langpair": "en|uk"},
).json()

# For an API you control, with the authentication it requires:
# requests.post("https://your-api.example/items", json={"name": "New"})
# requests.put("https://your-api.example/items/1", json={"name": "Changed"})
# requests.delete("https://your-api.example/items/1")

The dependency-free client supports get, post, put, and delete, optional headers, JSON bodies, query parameters, and a timeout (8 seconds by default). Responses expose status_code, text, and json(). Catch requests.RequestError for HTTP/network/decoding failures. Responses are limited to 1 MiB. Supply trusted URLs in application code; do not forward visitor-provided URLs to this client. GET page handlers should only read data. POST actions can make remote writes; add application authentication and authorization before exposing private data or operations. Pyrite's local development server does not provide authentication.

/translate demonstrates input, a real free MyMemory request, and updated page content. No API key required; availability, translation quality, and quotas depend on MyMemory. Submitted text is sent to that service.

Corners and gradients

from pyrite import Gradient, Theme

theme = Theme(
    radius=20,              # Panels, code blocks, images, details
    button_radius=999,      # Pill buttons; 0 gives square corners
    input_radius=12,        # Inputs and dropdowns
    gradient=Gradient(
        colors=("#101923", "#214a42", "#17283d"),
        angle=120,
        moving=True,
        duration=14,       # Seconds per cycle
    ),
)

button_radius and input_radius inherit radius when omitted. Gradients accept 2–8 hex colors; set moving=False for a static gradient or gradient=None for the usual background. A fixed background layer animates independently of page length. motion=False or the browser's reduced-motion preference stops its movement. The gradient layer sits over a background image when both are configured.

In-page requests and chat

from pyrite import App, Chat, Page

app = App()

@app.action("/api/reply")
def reply(data):
    message = data["message"]
    history = data.get("history", [])
    # Call a model here with pyrite.requests; keep API keys on the server.
    return {"reply": "You said: " + message}

@app.page("/")
def home():
    return Page(Chat(action="/api/reply"))

Visit /chat for a working local rule-based chatbot and a live translation form. Chat sends {message, history} and displays reply without a page reload. History stays in this page's browser memory (last 20 messages), is sent with each request, and clears on reload. It is untrusted input; validate it in callbacks. This is completed-response chat, not token streaming. The demo uses no AI service.

For other interfaces, use LiveForm(Input(...), Submit(...), action="/api/action") and return {"text": "Updated result"} from the action. All field values arrive as strings. Pyrite supplies the JavaScript, pending state, errors, 30-second client timeout, and safe text rendering. The callback can use requests to reach external APIs without exposing credentials to the browser. Aborting a request does not cancel Python work already running.

Actions accept JSON objects up to 16 KiB and reject cross-origin browser requests. These checks do not authenticate users. Use application validation and authorization for sensitive actions. Live components require JavaScript; ordinary pages and GET forms continue working without it. Browser playground previews remain isolated and cannot call server actions.

Per-component color labels

Define reusable labels in your project's styles.py:

from pyrite import Color, Theme

theme = Theme(colors={
    "danger": Color("#b91c1c", text="#ffffff", hover="#991b1b"),
    "success": Color("#166534", text="#ffffff", hover="#14532d"),
})

Use Button("Continue", "/next", theme=theme["success"]) or Submit("Save", theme=theme["success"]). This changes only that component. Text, Heading, Column, Row, and Card also accept a color through theme=. Other built-in elements support .themed(theme["danger"]). Page(..., theme=theme) still sets the whole page's theme. theme=theme:label is not valid Python; use theme=theme["label"]. Labels contain validated Color objects, not arbitrary CSS. Missing labels raise an error explaining where to define them.

Pyrite doctor

From your project folder:

.venv/bin/pyrite doctor
# Or specify a directory:
.venv/bin/python -m pyrite doctor /path/to/your/project

Doctor reports known issues with file, line, explanation, and suggested fix:

Severity Meaning Exit code
Severe Syntax, invalid settings/colors, or known component misuse 2
Medium Missing page theme or low text contrast 1
Mild Usability advice, such as overly fast gradients 0

The highest severity determines the exit code. No issues also returns 0. Doctor reads Python source without executing apps or sending API requests. It skips virtual environments and build folders. Dynamic values and runtime behavior still need testing; passing doctor is not proof that every issue is fixed.

Tests

python3 -m unittest discover -s tests -v

Publishing a release to PyPI

These steps are for maintainers publishing Pyrite itself. Run from the repository root. Users building websites only need the installation instructions above.

Set up publishing tools

python3 -m venv .venv
.venv/bin/python -m pip install --upgrade build twine

Keep publishing tools inside this environment. Do not install them into Arch's managed system Python or use --break-system-packages.

Create an upload token

  1. Register or sign in at https://pypi.org and enable two-factor authentication.
  2. Open https://pypi.org/manage/account/token/ and create an API token.
  3. For an existing project, restrict its scope to pyrite-web. An account-wide token is needed only when creating a project for the first time.
  4. Copy the token immediately; PyPI shows it once. Keep it out of source files, Git, logs, screenshots, and chat.

Build and check

Choose an unused version in pyproject.toml. PyPI cannot overwrite an uploaded release, even when only the README changes. The README becomes the package's PyPI description when the new artifacts are uploaded.

For version 0.1.1:

.venv/bin/python -m unittest discover -s tests -v
.venv/bin/python -m build
.venv/bin/python -m twine check dist/pyrite_web-0.1.1.tar.gz dist/pyrite_web-0.1.1-py3-none-any.whl

Use the matching version in every filename for later releases. Upload explicit filenames so old artifacts in dist/ are not accidentally uploaded again.

Upload and verify

.venv/bin/python -m twine upload dist/pyrite_web-0.1.1.tar.gz dist/pyrite_web-0.1.1-py3-none-any.whl

When prompted, use __token__ as the username and the full token, including its pypi- prefix, as the password. The password prompt hides your input.

Check https://pypi.org/project/pyrite-web/0.1.1/ and install in a fresh environment:

python3 -m venv /tmp/pyrite-release-check
/tmp/pyrite-release-check/bin/python -m pip install pyrite-web==0.1.1
/tmp/pyrite-release-check/bin/python -c 'from pyrite import App, Chat, Gradient, Theme; print("Pyrite installed")'

If Twine reports No module named twine, use .venv/bin/python instead of system python3. A 403 usually means missing project permissions or an invalid token; an already-existing-file error requires a new version, not another upload of the same filename. Never paste your token into an error report.

Doctor also checks imported Pyrite component arguments, literal heading levels, motion effects, input kinds, slider bounds, link/image URLs, live-action paths, empty labels and page metadata, select options, and duplicate or invalid App endpoint paths. Import aliases and import pyrite as p are supported for these checks. Dynamic values are left for runtime validation; application code is never executed. Route checks are local to each file and App variable.

# Machine-readable issues and severity counts
python -m pyrite doctor app.py --json
# Report severe issues only (also controls exit status)
python -m pyrite doctor . --severity severe
# Skip additional directory names; repeat as needed
python -m pyrite doctor . --exclude generated --exclude vendor

Exit status: 2 for severe issues, 1 for medium issues, 0 otherwise. --severity defaults to mild, reporting every severity. JSON output contains issues (severity, path, line, message, fix) and counts.

Guide, rating, and helpful errors

Run pyrite guide app.py for a cyan terminal workspace with Guide, Doctor, and Logs tabs. Doctor findings refresh as you edit your source. Use Tab to switch views, arrows or j/k to select, Page Up/Down to scroll explanations, / to search, Escape to clear search, r to refresh, and q to quit. Click the underlined topic links to open explanations inside the guide; Enter opens the selected finding's explanation. Mouse support depends on your terminal. Use g to return to the guide topics. The TUI needs a terminal of at least 65 columns by 14 rows and Python's standard-library curses module.

pyrite guide app.py
pyrite guide app.py --topic theme
pyrite guide app.py --log pyrite-errors.jsonl
pyrite guide app.py --plain  # also automatic when output is redirected
pyrite rating app.py
pyrite rating app.py --json
pyrite run app.py --log startup-errors.jsonl

guide and rating only inspect application source; they never run it. Both accept repeatable --exclude DIRECTORY arguments. The guide includes runnable getting-started code and explanations of components, forms, routing, themes, URLs, diagnostics, and Python errors. Logs refresh every second, showing the latest 200 lines from at most 128 KiB, newest first. Both JSONL runtime logs and existing plain-text logs are supported; select an entry and open its explanation. A missing log file can be created while the guide is running. --plain prints the selected topic and a diagnostic/log snapshot.

Rating measures static diagnostic health: start at 100 and subtract 20 for each severe finding, 5 for each medium finding, and 1 for each mild finding, stopping at zero. It shows the five highest-priority fixes; JSON includes all tips, severity counts, and penalties. No source files or a missing path produces an unavailable score. A clean score does not prove correct runtime behavior, security, performance, or visual quality. Rating uses Doctor's exit codes: 2 for severe findings, 1 for medium, otherwise 0.

Request-handler failures now print the Python exception, application source location, a suggested fix, a guide topic, and the full traceback to the server terminal. Missing page returns point to the handler definition. Page configuration and execution failures return HTTP 500; malformed query parameters return 400. Actions may raise ValueError to reject user input with 400; other failures return 500. HTTP error responses stay generic.

Enable structured runtime error logs explicitly:

from pyrite import App

app = App(log_path="pyrite-errors.jsonl")

Use pyrite run app.py to execute an application with the same explanations for startup errors, including syntax mistakes. Its --log writes startup failures; use App(log_path=...) for failures during HTTP requests. Running python app.py also gets helpful request-handler errors, while Python handles startup exceptions normally. run returns 1 on a caught startup error and preserves explicit application exit codes.

New error records do not include request bodies, headers, query strings, or local variables. Exception messages may contain private data; keep logs outside source control. New log files use owner-only permissions on POSIX; existing files keep their current permissions. Standard HTTP access logs remain separate and can include request URLs.

Metadata

Release files for pyrite-web 0.1.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for pyrite-web 0.1.2
File Size Uploaded
pyrite_web-0.1.2.tar.gz 56.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyrite-web 0.1.2
File Interpreter ABI Platform
pyrite_web-0.1.2-py3-none-any.whl Python 3 none any Details

Total release size: 94.2 kB

Release files / pyrite_web-0.1.2.tar.gz

Download URL pyrite_web-0.1.2.tar.gz
Size 56.1 kB
Tags Source
SHA-256 checksum
How to use checksums
f12a0f8fdd5835e54057a299d1f2ef57ecd2c3a157ddfd7a1cacdf815478265d
BLAKE2b-256 checksum
How to use checksums
a101802cf0545434440972efb7dc355c07758d77b2a0e093f68dd1b02c7907ca
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.8

Release files / pyrite_web-0.1.2-py3-none-any.whl

Download URL pyrite_web-0.1.2-py3-none-any.whl
Size 38.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
49db3a4e91cd5c51145d36d0a8336c676fbaf2ae407eadda72db21fbe6517e8f
BLAKE2b-256 checksum
How to use checksums
84403c64f80b2147837b21cf321f071a67508cf448d2e3016d27262139be4acd
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.14.8

Release history Release notifications | RSS feed

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

This release

0.1.2 This release

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page