Glue, run and ship Python applications. Zero dependencies.
Pyjoyment is one environment for the whole life of a Python service: the toolkit it is made of, the framework that puts its parts together with the applications of other frameworks, the servers that run them, the files that deploy them, and the clients that connect them to the services around them.
Pyjoyment has no runtime dependencies. The HTTP and WebSocket server and client, the event loop, the parsers of HTML, JSON, YAML, TOML and INI, JSON Schema, the templates and the test harness are all in its package, on top of the Python standard library. An application that installs Pyjoyment adds one package to its supply chain, and nothing that the package would pull in after it. Each release comes with the attestations of its build and an SBOM.
Pyjoyment does not compete with Flask, Django or FastAPI: it complements them. It mounts their applications next to its own routes, on the same server, glues them to other services, and runs and deploys them together.
Pyjo is the toolkit, which every other part is made of, and which works on
its own, without the framework:
- HTTP and WebSocket client and server, with TLS.
- Event loop with promises and
async/await, with TCP, UDP and UNIX domain sockets, timers and child processes. - HTML/XML parser with CSS selectors.
- JSON with JSON Pointers and JSON Schema, from type hints and validated, YAML, TOML and INI, all loaded and dumped.
- JSON-RPC 2.0, its server and its client, over HTTP or any other transport.
- Templates and template strings, with automatic escaping.
- Containers for URLs, paths, query strings, headers, cookies and HTTP messages.
- Collections and lazy iterators, whose chains run with
forandasync foralike. - Command line options, event emitter, logger and utility functions.
- Test library and test harness.
A script uses the toolkit without a server: the user agent requests the HTML
specification, and the CSS selectors of its DOM pick the rows of the table of
the named character references, whose names and code points the script
prints, as Aacute; U+000C1.
The processing is concurrent. The request blocks here, as a script expects,
and the same request is non-blocking with get_p, whose promise is awaited
or chained, so that a script or an application runs many of them at once in
its event loop and processes each response as it comes. The DOM is searched
with iterate, a lazy chain, which passes the elements on one by one, so that
the first row is printed before the last one is picked:
import Pyjo.UserAgent
from Pyjo.String.Unicode import u
# Extract named character references from HTML spec
tx = Pyjo.UserAgent.new().get('https://html.spec.whatwg.org')
for n in tx.res.dom().iterate('#named-character-references-table tbody > tr'):
u(n.at('td > code').text + ' ' + n.children('td')[1].text).trim().say()The event loop of Pyjoyment is the one of asyncio by default, so the
coroutines and the tasks of the libraries of asyncio run in the same loop as
its servers and clients. A reactor is where the event loop is replaced, when
an application needs another I/O library: Pyjo.Reactor.Poll runs without
asyncio, on select.poll, and on select.select on Windows, with a standard
installation of Python and nothing else. On PyPy, whose JIT compiles it, it
serves requests as fast as the servers of Python whose event loop and HTTP
parser are compiled from C or Rust. A distribution of its own adds another
reactor, as Pyjo.Reactor.Twisted, which PYJO_REACTOR selects, with no
change of the application.
Pyjo.Test is a library of assertions and Pyjo.Prove a harness for test
scripts that print the Test Anything Protocol
(TAP), modelled on Test::More and prove, the standard ones of Perl, and
Pyjo.Test.Pyjo tests an application with its requests, without a server.
The framework itself is tested with them.
An application is tested with any test framework, as an assertion of
Pyjo.Test raises AssertionError once raise_failures() is called, and
pytest is integrated out of the box: Pyjoyment comes with a plugin of pytest,
which pytest loads where Pyjoyment is installed, and which runs the test
scripts of Pyjo.Test as tests of pytest, and gives the tests of pytest the
fixture t, the Pyjo.Test.Pyjo of the application.
Pyjoyment is the framework: routes, controllers, the renderer with
templates and template strings, plugins, configuration in Python, JSON, YAML,
TOML and INI files, the commands, and Pyjoyment.Lite, where a single file
is a whole application:
from Pyjoyment.Lite import *
get('/', {'text': 'I ♥ Pyjoyment!'})
app.start()The applications of other frameworks are its components. The plugin Mount
puts each under a path or a host, next to the routes of the application, in
the same process and on the same server:
- WSGI applications, as those of Flask and Django;
- ASGI applications, as those of Starlette and FastAPI and the servers of the MCP SDK, whose responses stream at the pace of the client, with their WebSockets;
- FastCGI applications, as PHP on php-fpm, which it connects to;
- CGI programs, which it runs, as a web server does;
- event functions of AWS Lambda, which it runs in a process of their own, as Lambda does;
- other applications of Pyjoyment.
import Pyjo.App.Lambda
from Pyjo.File import curfile
from Pyjoyment.Lite import *
# The WSGI application of hello_wsgi.py, the ASGI application of hello_asgi.py and the event function of AWS Lambda of
# hello_lambda.py, next to this script, on one server, next to the routes of this application
plugin('Mount', {'/wsgi': 'hello_wsgi:app'})
plugin('Mount', {'/asgi': 'hello_asgi:app'})
plugin('Mount', {'/lambda': Pyjo.App.Lambda.new(function='hello_lambda.handler', task_root=curfile().dirname())})
get('/', {'text': 'I ♥ Pyjoyment!'})
app.start()Pyjoyment works in two roles for each interface that Python and the web servers have: it hosts the applications of that interface, and it is one of them, on the server of another.
| Interface | Pyjoyment hosts | Pyjoyment runs on |
|---|---|---|
| WSGI | Flask, Django, any WSGI application | any WSGI server, as gunicorn |
| ASGI | Starlette, FastAPI, any ASGI application | any ASGI server, as uvicorn |
| FastCGI | PHP on php-fpm, any FastCGI application | a web server in front of it, as Caddy |
| CGI | CGI programs, as Python or PHP scripts | a web server that runs CGI programs |
| AWS Lambda | event functions, as the runtime of Lambda does | Lambda, as an event function or a custom runtime |
Pyjo.HelloWorld, the application that answers Your Pyjo is working!, on
uvicorn, as the ASGI application that Pyjo.Server.ASGI makes of it:
import Pyjo.HelloWorld
import Pyjo.Server.ASGI
import uvicorn
app = Pyjo.Server.ASGI.new(app=Pyjo.HelloWorld.new()).to_asgi_app()
uvicorn.run(app, port=3000)The servers of Pyjoyment run an application on its event loop, from one process to many:
daemon, the non-blocking HTTP and WebSocket server, with TLS;prefork, with worker processes, for Linux, macOS and other systems withfork;prethread, with worker threads, for Windows, and for the free-threaded build of Python anywhere;pyjo-hot, with worker processes and deployment without downtime, whichHUPstarts, for systems withfork;pyjo-reload, or the commandreload, for development, which restarts the server when a file changes.
Each stops gracefully on TERM, finishing the requests in progress, and runs
the hooks of the application around its life, before_server_start and
after_server_stop, in each process and thread. The plugin Health answers
the probes of liveness, readiness and startup, whose readiness fails as soon as
the server starts to stop.
$ pyjo-reload hello.py
Web application available at http://127.0.0.1:3000The commands of an application write what deploys it:
./myapp.py generate pyproject # the project, for uv
./myapp.py generate dockerfile # the image of a container
./myapp.py generate kubernetes # the manifests of Kubernetes, with Kustomize
./myapp.py generate lambda-zip # the ZIP files of an event function of AWS LambdaThe server listens on the port of PORT, as Knative, Cloud Run, Render,
Koyeb and Heroku set it, and stops gracefully on the TERM of its container.
On AWS Lambda, an application is an event function, which answers the events
of API Gateway, Function URLs and Application Load Balancers:
import Pyjo.HelloWorld
import Pyjo.Server.Lambda
# The handler "hello_world_lambda.handler" of an event function of AWS Lambda
handler = Pyjo.Server.Lambda.new(app=Pyjo.HelloWorld.new()).to_lambda_app()On a custom runtime, as provided.al2023, its bootstrap runs it with the
runtime client of Pyjoyment:
#!/bin/sh
# The bootstrap of a custom runtime of AWS Lambda, as provided.al2023, which runs the handler of the function, as
# "hello_world_lambda.handler" of _HANDLER, with the runtime client of Pyjoyment
exec python3 -m Pyjo.Server.LambdaThe public layers of
Pyjoyment-Lambda-Layers
bring Python, of any version, and Pyjoyment to such a function, which carries
its application only, as generate lambda-zip --function-only writes it.
An application talks to the services around it from the same event loop that serves its requests, without blocking it:
- the user agent of HTTP and WebSocket, with promises and
async/await; - the client of JSON-RPC 2.0;
- event functions of AWS Lambda, which it runs as the runtime of Lambda does, as components of the application;
- the libraries of asyncio, as the asynchronous AWS SDK, whose coroutines run in the same event loop.
A page of the application opens a WebSocket to it and sends it a URL, which
the application requests with its user agent, without blocking the other
connections, and sends back the title of the page. The template of the page is
in the docstring of the script, after the line __DATA__:
"""
__DATA__
@@ index.html.ep
% url = url_for('title')
<script>
const ws = new WebSocket('<%= url.to_abs() %>');
ws.onmessage = function (event) { document.body.innerHTML += event.data };
ws.onopen = function (event) { ws.send('https://mojolicious.org') };
</script>
"""
from Pyjoyment.Lite import *
# Render template "index.html.ep" from the DATA section
@get('/')
def index(c):
c.render(template='index')
# WebSocket service used by the template to extract the title from a website
@websocket('/title')
def title(c):
@c.on('message')
def message(c, msg):
c.ua().get_p(msg).then(lambda tx: c.send(tx.result.dom().at('title').text))
app.start()Pyjo-MCP is
the Model Context Protocol made of the
parts of Pyjoyment: its server is a route of an application, whose tools,
prompts and resources are functions with the schemas of their type hints, its
client is the user agent, and Pyjo.MCP.Test tests a server with the
requests of the client, without starting it. It depends on Pyjoyment and on
nothing else:
pip install Pyjo-MCPThe tests of Pyjoyment run it with the software on the other side of each interface:
- WSGI: on Gunicorn, uWSGI, Waitress and Werkzeug, and Flask mounted in Pyjoyment.
- ASGI: on uvicorn and Hypercorn, both on asyncio and on uvloop, on Daphne and on Granian, and Starlette and the servers of the MCP SDK mounted in Pyjoyment.
- FastCGI: behind Caddy, and PHP on php-fpm mounted in Pyjoyment.
- CGI, for the legacy applications: on the CGI server of
http.serverand on uWSGI, and the CGI scripts of Python and of PHP on php-cgi mounted in Pyjoyment. - AWS Lambda: on the images of Lambda for Python and on the custom runtime
provided.al2023, with the emulator of the Runtime API, and Starlette on Mangum as an event function that Pyjoyment runs. - Containers: the image of
generate dockerfileon Docker, and the manifests ofgenerate kuberneteson a cluster of kind. - Event loops: asyncio, the loop of
Pyjo.Reactor.Poll, and Twisted, whose reactor passes the whole test suite of Pyjoyment. - Libraries: pytest, with pytest-xdist, the asynchronous AWS SDK on the
event loop of the server, and the JSON Schemas of pydantic next to those of
the type hints of
Pyjo.JSON.Schema.
Inside the application, the classes of Pyjo take the types of the Python
standard library and turn back into them, so that the code around Pyjoyment
needs no adapters:
Pyjo.URLfrom and to theSplitResultofurllib.parse;Pyjo.Datefrom and todatetime, andfloat(date)is the epoch;Pyjo.Fileis a path-like object, andto_path()is apathlib.Path;Pyjo.Headersfrom and to theMessageofemail.message;- the requests and the responses of HTTP from and to those of
urllib.requestandhttp.client; - the cookies from the
Morselofhttp.cookies; Pyjo.Collectionis alist,Pyjo.String.Unicodeastr, and a promise is awaited in a coroutine of asyncio.
The text files that Pyjoyment reads are Unicode, not only UTF-8: a byte order mark of UTF-8, UTF-16 or UTF-32, as Windows PowerShell and the editors of Windows write it, gives the encoding of a configuration file, a template or a document of JSON or YAML, so that a file made on Windows loads as it is.
Pyjoyment is installed from PyPI:
pip install Pyjoymentor, in a project managed with uv:
uv add PyjoymentA new project or script comes from its generator, which uv runs without installing Pyjoyment first:
uvx --from Pyjoyment pyjo generate app MyApp
uvx --from Pyjoyment pyjo generate lite-app myapp.pyThe skills for coding agents that write applications with Pyjoyment are a
plugin of Claude Code, whose
SKILL.md files other agents read too:
/plugin install pyjoyment --marketplace pyjoyment/Pyjoyment
or from the shell, as the extensions of the editors have no /plugin:
claude plugin install pyjoyment --marketplace pyjoyment/PyjoymentPyjoyment is an evolution of Mojolicious, the real-time web framework for Perl. It keeps the API of Mojolicious with Python names, so its documentation and experience carry over, and the tests of Mojolicious, ported with each module, are the specification that the code follows. A ported module names the Mojolicious version it was ported from in its documentation.
Pyjoyment requires Python 3.12 or newer and runs on PyPy on a best-effort
basis. It runs on Linux, macOS and Windows. On Windows, Python has no
os.fork and no UNIX domain sockets, so Pyjo.Server.Prefork,
Pyjo.Server.Hot, Pyjo.Server.Reload and Pyjo.IOLoop.Subprocess do not
run there, and Pyjo.Server.Prethread runs the workers in threads instead.
The event loop watches up to 512 sockets there, with the select of Windows,
and a server stops with Ctrl-C, gracefully, or with Ctrl-Break, immediately.
What needs another library, or follows a specification that is revised more
often than Pyjoyment is released, is a distribution of its own, which adds a
module to a namespace of Pyjoyment, as Pyjo.Reactor for a reactor,
Pyjoyment.Plugin for a plugin or Pyjo for a protocol.
- Pyjo.AWS - Amazon Web Services for Pyjoyment
- Pyjo.GCP - Google Cloud for Pyjoyment
- Pyjo.MCP - Model Context Protocol for Pyjoyment
- Pyjo.Reactor.Gevent - Low-level event reactor on gevent
- Pyjo.Reactor.Trio - Low-level event reactor on Trio
- Pyjo.Reactor.Twisted - Low-level event reactor with Twisted support
- Pyjo.Reactor.Uvloop - Low-level event reactor on uvloop
- Pyjo.Reactor.Winloop - Low-level event reactor on Winloop
The documentation is on Read the Docs.
Copyright (C) 2014-2026, Piotr Roszatycki.
Based on Mojolicious: Copyright (C) 2008-2025, Sebastian Riedel and others.
This program is free software, you can redistribute it and/or modify it under the terms of the Artistic License version 2.0.
The pages of errors bundle Bootstrap, with
Popper, under the MIT License, and highlight.js
under the BSD 3-Clause License, whose texts are next to them in
Pyjoyment/resources/public/pyjo.