Skip to main content
Pyplet logo

Python everywhere

Pyplet is an application server that lets you create interactive web applications using Python on both the client and server side. Powered by PyScript (Python compiled to WebAssembly), Pyplet brings the full power of Python to the browser.

Resources

Why Pyplet?

  • Pure Python: Write your entire application in Python - no JavaScript required
  • Real-time Communication: Built-in WebSocket support for seamless client-server interaction
  • Modern Async: Leverages Python's async/await for responsive applications
  • Browser-Native: Client code runs directly in the browser via WebAssembly
  • Shared Code: Reuse Python modules between client and server

Quick Start

Prerequisites

  • Python ≥3.12
  • uv (recommended) or pip

Installation

The recommended way to install Pyplet is via uv.

  1. Install venv

    uv venv
    
  2. Activate the venv

    source ./venv/bin/activate
    
  3. Install Pyplet

    uv pip install pyplet
    

Create Your First App

pyplet init my_app

This creates a new project in apps/my_app/ with two files:

  • my_app_client.py - Python code that runs in the browser
  • my_app_server.py - Server-side Python logic

Run the Server

pyplet start

Then open your browser to http://localhost:8080 to see your apps!

Example Application

Here's a minimal Pyplet app showing real-time communication:

hello_client.py, runs in the browser:

import pyplet
from js import document

container = document.getElementById("container")

class MyClientApp(pyplet.client.ClientApplication):
    async def websocket_client_loop(self, ws: pyplet.WebSocket):
        # Receive message from server
        message = await ws.receive()
        container.innerText = message.decode()

        # Send message back to server
        await ws.send(b"Hello from the browser!")

hello_server.py, runs on the server:

import pyplet

class _(pyplet.server.ServerApplication):
    async def websocket_server_loop(self, ws: pyplet.WebSocket):
        # Send message to client
        await ws.send(b"Hello from the server!")

        # Receive client's response
        response = await ws.receive()
        print(f"Client says: {response.decode()}")

Custom GUI components

You can create reusable components that work on both client and server.

Download Component

# Server side download component
download("./static/static_file.txt", "Download from server"),

# Client side download component (from virtual file system, i.e., from_vfs=True)
download(
    "./public/vfs_file.txt", "Download from client", from_vfs=True
),

You must put you files in the right project folder in the static directory (the name of the directory must be static) for the server, and in the public directory for the client (the name of the directory can be changed, but should not be static). The from_vfs flag tells Pyplet to look for the file in the virtual file system (client-side) instead of the server's filesystem.

How It Works

Pyplet uses a unique dual-runtime architecture:

  1. Server-side: Standard CPython running Tornado web server
  2. Client-side: Python code compiled to WebAssembly (e.g., via PyScript using Pyodide), running in the browser
  3. Communication: WebSocket connection bridges the two environments
┌──────────────────────┐         WebSocket         ┌───────────────────────┐
│  Browser (PyScript)  │ <───────────────────────> │   Server (CPython)    │
│  your_app_client.py  │                           │   your_app_server.py  │
└──────────────────────┘                           └───────────────────────┘

Project Structure

apps/                     # Your apps live here
├── auth_rules.json       # The authentification rules are defined here
└── app_1/
    ├── app_1_client.py   # The client code
    └── app_1_server.py   # The server code

Authentication

Pyplet supports platform-level OAuth2 / OIDC authentication via Google and Microsoft. When enabled, all pages and WebSocket connections are gated behind a login screen. Auth is opt-in: if no provider is configured the platform runs with no login, exactly as before.

Setup

1. Register an OAuth app with your provider and obtain a client ID and secret. Set the callback URL to:

http://<your-host>/oauth/callback

2. Set environment variables:

# Required & persistent in production — generate once and keep it stable.
# Under PYPLET_REQUIRE_AUTH=1 the server refuses to boot when this is unset
# (a per-process random secret logs out every user on each restart):
export PYPLET_COOKIE_SECRET=$(python -c "import secrets; print(secrets.token_hex(32))")

# Google
export OAUTH_GOOGLE_CLIENT_ID=your-client-id
export OAUTH_GOOGLE_CLIENT_SECRET=your-client-secret

# Microsoft / Entra ID (can be set alongside Google)
export OAUTH_MICROSOFT_CLIENT_ID=your-client-id
export OAUTH_MICROSOFT_CLIENT_SECRET=your-client-secret
export OAUTH_MICROSOFT_TENANT=common  # or your tenant ID

3. Start the server — a login page with provider buttons appears automatically.

Access control (ACL)

To restrict which apps each user can see, create apps/auth_rules.json — a JSON array of ["project/app regex", "email regex"] pairs:

[
    [".*",           "@mycompany\\.com$"],
    ["public/demo",  ".*"]
]

Rules are evaluated in order; the first matching rule grants access. The first regex is matched against the combined "project/app" string; the second against the user's email address. If no rule matches, access is denied.

Override the rules file path with PYPLET_AUTH_RULES_FILE.

Deny-by-default (fail closed): when authentication is enabled but the rules file is missing, access is denied to every app — ship auth_rules.json in your deploy artifact. (When auth is fully disabled — local dev with no provider — a missing file still allows all apps, so an un-authenticated local run works.)

Magic-link e-mail authentication

As an alternative (or complement) to OAuth, users can sign in by entering their e-mail address and clicking a single-use link delivered to their inbox — no password required.

Configure an SMTP server to enable it:

export MAGICLINK_SMTP_HOST=smtp.example.com
export MAGICLINK_SMTP_PORT=587          # default
export MAGICLINK_SMTP_USER=noreply@example.com
export MAGICLINK_SMTP_PASSWORD=secret
export MAGICLINK_FROM=noreply@example.com   # optional, defaults to SMTP_USER
export MAGICLINK_TOKEN_TTL=900              # seconds (default: 15 min)
# Set to "0" to disable STARTTLS (not recommended):
# export MAGICLINK_SMTP_TLS=0

Magic-link and OAuth providers can be active simultaneously — the login page shows all available methods.

The ACL rules file applies to magic-link logins exactly the same way it does for OAuth: the user's e-mail address is matched against the email_regex column of each rule.

Because magic-link mints a session for any e-mail that can receive the link, it is refused at boot on the production profile (PYPLET_REQUIRE_AUTH=1, below) unless you opt in explicitly with PYPLET_ALLOW_MAGICLINK=1.

Production fail-closed startup (PYPLET_REQUIRE_AUTH)

On any non-local deployment, set PYPLET_REQUIRE_AUTH=1. With it, the server refuses to boot (exits non-zero with a logged error) rather than silently serving anonymously when the auth config is misdelivered — specifically when no auth method is configured, when auth_rules.json is missing, or when magic-link is enabled without PYPLET_ALLOW_MAGICLINK=1. Without the flag (the default), a deployment with no provider still starts but logs a loud WARNING that every request is served anonymously.

Three further production-profile guards ship with this posture. The server refuses to boot when PYPLET_DEBUG=1 under PYPLET_REQUIRE_AUTH=1 — Tornado debug mode enables autoreload and exposes traceback pages, so set PYPLET_DEBUG=0 in production. Behind a TLS-terminating reverse proxy, app.listen trusts X-Forwarded-For/X-Forwarded-Proto (xheaders) and WebSocket upgrades are origin-checked against the PYPLET_URL host (same-origin when PYPLET_URL is unset). At login, OIDC id_tokens are verified against the provider JWKS (RS256 signature, issuer, audience and expiry) before a session is established.

Configuration reference

Variable Description
PYPLET_COOKIE_SECRET Secret for signing session cookies
PYPLET_SECURE_COOKIES Force Secure attribute on auth cookies: 1/0
PYPLET_REQUIRE_AUTH Fail-closed switch: 1 refuses boot, default 0
PYPLET_ALLOW_MAGICLINK Opt magic-link IN on require-auth, default 0
PYPLET_SESSION_TTL_DAYS Session cookie lifetime in days, default 1
OAuth — Google
OAUTH_GOOGLE_CLIENT_ID Google OAuth2 client ID
OAUTH_GOOGLE_CLIENT_SECRET Google OAuth2 client secret
OAuth — Microsoft
OAUTH_MICROSOFT_CLIENT_ID Microsoft / Entra ID client ID
OAUTH_MICROSOFT_CLIENT_SECRET Microsoft / Entra ID client secret
OAUTH_MICROSOFT_TENANT Tenant ID or common (default: common)
Magic-link
MAGICLINK_SMTP_HOST SMTP server hostname (required to enable magic-link)
MAGICLINK_SMTP_PORT SMTP port (default: 587)
MAGICLINK_SMTP_USER SMTP login username
MAGICLINK_SMTP_PASSWORD SMTP login password
MAGICLINK_SMTP_TLS Use STARTTLS: 1 (default) or 0 for plain SMTP
MAGICLINK_FROM Sender address (defaults to MAGICLINK_SMTP_USER)
MAGICLINK_TOKEN_TTL Token validity in seconds (default: 900 = 15 min)
ACL
PYPLET_AUTH_RULES_FILE ACL rules path (default: apps/auth_rules.json)

PYPLET_COOKIE_SECRET must be persistent and is required under PYPLET_REQUIRE_AUTH=1 (the server refuses to boot when unset); PYPLET_SECURE_COOKIES, when unset, follows the PYPLET_URL scheme.

Advanced Features

DOM Manipulation

Pyplet provides utilities for working with the DOM:

from pyplet.shared.dom import create_element

# Create elements programmatically
button = create_element('button', {'class': 'btn btn-primary'}, 'Click me!')

Bootstrap Components

Built-in support for Bootstrap UI components:

from pyplet.shared.dom.bootstrap import create_button, create_card

button = create_button('Primary', style='primary')
card = create_card('Card Title', 'Card content goes here')

Environment Detection

Check whether code is running on server or client:

import pyplet

if pyplet.is_server:
    print("Running on server")

elif pyplet.is_client:
    print("Running in browser")

CLI Reference

# Create a new project
pyplet init <project_name>

# Start the development server
pyplet start

# Start server with custom options
pyplet start --port 3000 --address 0.0.0.0

# Start the tutorial
pyplet tutorial

# Run server directly with Python
python -m pyplet.server

Configuration

Server-wide configuration is managed in pyplet/server/config.py and can be set via environment variables or CLI flags:

# Using environment variables
export PYPLET_PORT=3000
export PYPLET_ADDRESS=0.0.0.0
pyplet start

# Using CLI flags
pyplet start --port 3000 --address 0.0.0.0

Available configuration options:

  • --address / PYPLET_ADDR - Server address (default: 127.0.0.1)
  • --port / PYPLET_PORT - Server port (default: 8080)
  • --apps / PYPLET_APPS - Apps directory (default: apps)
  • --debug / PYPLET_DEBUG - Debug mode (default 1; must be 0 in prod)
  • --pyodide-url / PYPLET_PYODIDE - Pyodide CDN URL
  • --url / PYPLET_URL - Custom URL override
  • PYPLET_WS_MAX_MESSAGE_MB - Max WebSocket frame size MB (default: 40)

See the Authentication section for OAuth-related variables.

Browser Compatibility

Pyplet should work in any modern browser that supports WebAssembly:

  • Chrome/Edge 57+
  • Firefox 52+
  • Safari 11+

Dependencies

Pyplet uses:

  • Tornado - Async web server
  • PyScript - Python in WebAssembly (via Pyodide)
  • Cython - Python to C compiler for performance

Adding Dependencies

For server-side code

Edit pyproject.toml under [project].dependencies

For testing

Edit pyproject.toml under [project.optional-dependencies].test

Examples

Check the apps/template/ directory for a working example demonstrating:

  • WebSocket communication
  • DOM manipulation
  • Async patterns
  • Client-server interaction

Limitations

Since client code runs in PyScript (WebAssembly):

  • Not all Python packages are available in the browser
  • Some stdlib modules have limited functionality
  • The js module gives direct access to the browser APIs

Contributing

Where development happens

Pyplet lives in two places, and they are not interchangeable:

  • GitLab seglab/pyplet (CETIC forge, git.cetic.be) is the canonical repository. Development lands there, on the default branch main, through merge requests.
  • GitHub cetic/Pyplet is the publication mirror. It is deliberately behind: nothing is developed there, and it is refreshed from GitLab main by a maintainer when a state is worth publishing.

The flow is one-way: GitLab main → GitHub. A change pushed straight to GitHub would be overwritten by the next publication.

Contributions are welcome! When contributing:

  1. Maintain clean separation between client and server code
  2. Use async/await for all I/O operations
  3. Test in both server (CPython) and client (PyScript) environments
  4. Follow the existing naming conventions
  5. Use the prek (pre-commit)
  6. Implement tests for the features your are adding

License

Apache License 2.0 - See LICENSE for details

Support

Roadmap

Future enhancements planned:

  • Hot reload during development
  • More built-in UI components
  • Better error handling and debugging tools
  • Enhanced documentation and tutorials
  • Package distribution via PyPI

Happy coding with Python everywhere! 🐍✨

Metadata

Release files for pyplet 0.0.16

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

Source distribution (sdist)

Source distribution for pyplet 0.0.16
File Size Uploaded
pyplet-0.0.16.tar.gz 79.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyplet 0.0.16
File Interpreter ABI Platform
pyplet-0.0.16-py3-none-any.whl Python 3 none any Details

Total release size: 159.7 kB

Release files / pyplet-0.0.16.tar.gz

Download URL pyplet-0.0.16.tar.gz
Size 79.1 kB
Tags Source
SHA-256 checksum
How to use checksums
c5ce8638a9a26a4604a0e9b87ba5b8d768d47346b2b34e424a6ef74d6175c0e4
BLAKE2b-256 checksum
How to use checksums
267326b1863be17b5964b30531514005161c3f925877f7ada3d8a3273073b233
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 Aug 28, 2026.

Transparency log

Release files / pyplet-0.0.16-py3-none-any.whl

Download URL pyplet-0.0.16-py3-none-any.whl
Size 80.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a8b604ac10921887fb2e9d62213cb1a906d3287b8abbc589685aaf6538180edb
BLAKE2b-256 checksum
How to use checksums
f054fe60b465849cf2728db11af59c107e438cb9b01842df3b177eaa31c187b5
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 Aug 28, 2026.

Transparency log

Release history Release notifications | RSS feed

0.1.1

1 release file

0.0.18

2 release files

This release

0.0.16 This release

2 release files

0.0.15

2 release files

0.0.14

2 release files

0.0.13

2 release files

0.0.10

2 release files

0.0.9

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.2

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