dexpot
Plain handlers. Real threads. Free-threaded Python.
A synchronous Python API framework that compiles routes once, validates JSON with msgspec, and adapts its concurrency model to the interpreter running it.
Quick start · Why dexpot · Execution model · Boundaries · Roadmap · Contributing
Why dexpot
Most Python API frameworks were designed around a permanent GIL: async I/O in one process, or several worker processes for CPU parallelism. Free-threaded CPython changes that tradeoff. Threads can execute Python simultaneously and share normal in-process state.
dexpot is designed around that runtime instead of hiding it behind an ASGI adapter:
- Plain synchronous handlers. No
async def, event loop, or coroutine bridge. - Compiled endpoint plans. Route matching metadata, argument sources, path conversions, and msgspec codecs are prepared when a handler is registered.
- Interpreter-adaptive scheduling. Free-threaded builds use one process and a real thread
per connection. GIL builds use a bounded thread pool with fast 503 overload shedding and
can fan out through
SO_REUSEPORTworkers. - msgspec request bodies. JSON decoding and validation happen together in compiled C codecs.
- A small, owned HTTP core. Routing, parsing, scheduling, draining, and response writes are dexpot code—not a wrapper around another web framework.
This is a full framework under active construction. The serving and routing foundation is shipped; production HTTP features such as middleware, OpenAPI, streaming, authentication, and hardened parser limits are tracked in the roadmap.
Quick start
1. Install
pip install "dexpot[cli]"
2. Define an application
Create main.py:
import msgspec
from dexpot import Dex
class ItemIn(msgspec.Struct):
name: str
price: float
class ItemOut(msgspec.Struct):
id: int
name: str
price: float
app = Dex()
@app.get("/items/{item_id}", response=ItemOut)
def get_item(item_id: int) -> ItemOut:
return ItemOut(id=item_id, name=f"item-{item_id}", price=9.99)
@app.post("/items", body=ItemIn, response=ItemOut)
def create_item(item: ItemIn) -> tuple[int, ItemOut]:
return 201, ItemOut(id=1, name=item.name, price=item.price)
The path capture is converted from text because item_id is annotated as int. The POST
body is decoded directly into ItemIn; malformed JSON or a validation failure returns 422.
3. Serve it
dexpot serve main:app --host 127.0.0.1 --port 8000
Call the real HTTP surface:
curl -s http://127.0.0.1:8000/items/7
curl -s -X POST http://127.0.0.1:8000/items \
-H 'Content-Type: application/json' \
-d '{"name":"keyboard","price":79.0}'
The responses are JSON:
{"id":7,"name":"item-7","price":9.99}
{"id":1,"name":"keyboard","price":79.0}
You can also run the file directly with app.serve():
if __name__ == "__main__":
app.serve(host="127.0.0.1", port=8000)
Route contract
Use @app.get, @app.post, @app.put, @app.patch, and @app.delete.
@app.post(
"/accounts/{account_id}/items",
body=ItemIn,
response=ItemOut,
)
def create_for_account(item: ItemIn, account_id: int) -> ItemOut:
return ItemOut(id=account_id, name=item.name, price=item.price)
The handler signature does not have to mirror URL order. dexpot binds path captures by name, treats the first non-path parameter as the declared body, preserves Python signature order, and supports keyword-only parameters. Put default-only parameters after that body parameter. Registration fails before serving when:
- a required parameter has no matching path capture, request body, or default;
- a path capture is not accepted by the handler;
- the handler uses
*argsor**kwargs; or - another route already owns the same method and structural path shape.
For example, GET /users/{id} and GET /users/{name} conflict because only one can ever
match a request.
A handler may return a JSON-encodable value, a msgspec struct, or (status, payload).
response= precompiles the successful-response encoder, but the current release does not
yet enforce the returned type at runtime.
Execution model
dexpot chooses its scheduler once when the module is imported.
| Runtime | Default serving model | Overload behavior |
|---|---|---|
Free-threaded CPython (sys._is_gil_enabled() == False) |
One process; each accepted connection owns a thread | No framework queue; OS and process limits apply |
| Standard GIL CPython | Bounded pool of CPU * 2 + 2 connection-owning threads |
Queue capped at 2 * pool; excess connections receive 503 |
Standard GIL CPython with DEXPOT_WORKERS>1 |
POSIX SO_REUSEPORT processes, each with its own bounded pool |
Each worker sheds independently |
A worker owns a keep-alive connection until it closes. This avoids putting idle keep-alive sockets back into a shared queue, where they can consume admission capacity and stall a worker waiting for the next request.
Tune the GIL scheduler before the process imports dexpot:
DEXPOT_POOL=16 DEXPOT_MAX_QUEUE=32 dexpot serve main:app
Use process fan-out on supported POSIX systems:
DEXPOT_WORKERS=4 dexpot serve main:app
DEXPOT_WORKERS>1 requires POSIX fork and SO_REUSEPORT; dexpot rejects that setting on
unsupported platforms rather than pretending multiprocess serving is active. Free-threaded
builds intentionally remain single-process because their threads can execute Python in
parallel.
SIGINT and SIGTERM stop admission and allow active connections up to five seconds to drain. The GIL supervisor restarts a worker that exits unexpectedly.
Current architecture
HTTP connection
|
v
accept + adaptive admission
|
+-- free-threaded Python --> connection-owned thread
|
+-- GIL Python -----------> bounded worker pool
|
+-- optional SO_REUSEPORT processes
|
v
parse request head and body
|
v
literal lookup / compiled parametric match
|
v
convert captures + msgspec decode/validate
|
v
plain Python handler
|
v
msgspec encode + one response write
A registered Route is the boundary between setup and traffic. It owns the immutable
handler plan: body decoder, response encoder, capture conversion metadata, and positional
versus keyword binding. Request processing consumes that plan without inspecting the
handler again.
Current boundaries
dexpot is alpha software and is not yet recommended for untrusted production traffic. Today:
- HTTP/1.1 requests with
Content-Lengthand keep-alive are supported; chunked request bodies are not. - The parser does not yet enforce request-line, header, body, or idle time limits.
- Query strings and headers are parsed internally but are not yet injectable handler parameters.
- There is no middleware, OpenAPI generation, authentication, TLS termination, streaming, WebSocket support, or proxy-header policy.
response=selects an encoder but does not validate the handler's return type.- Uncaught handler exception names and messages currently appear in 500 JSON responses. Do not put secrets in exception messages, and place dexpot behind a trusted boundary until stable public error handling lands.
- Multiprocess serving is POSIX-only. Windows users must use one process in the current release.
These are explicit roadmap items, not hidden features. See ROADMAP.md for the implementation order.
Give your coding agent dexpot context
Install project-local guidance for Claude Code, Cursor, Windsurf, GitHub Copilot, Cline, or OpenAI Codex:
# Auto-detect agents already configured in the project
dexpot add skills
# Or target one explicitly
dexpot add skills --agent claude
dexpot add skills --agent cursor
dexpot add skills --agent windsurf
dexpot add skills --agent copilot
dexpot add skills --agent cline
dexpot add skills --agent codex
# Install into another project
dexpot add skills --path ./my-api
The installed skill teaches the shipped route contract, msgspec body model, concurrency modes, operational boundaries, and verification requirements. Shared Copilot and Codex instruction files use a bounded managed block, so existing project guidance is preserved.
CLI reference
dexpot serve <module:attribute> [--host HOST] [--port PORT]
dexpot add skills [--agent AGENT] [--path DIRECTORY]
dexpot version
dexpot --version
The CLI extra is optional, so applications that call Dex.serve() directly do not need
Typer:
pip install dexpot # framework runtime
pip install "dexpot[cli]" # framework runtime + dexpot command
Examples
examples/minimal.py is the smallest runnable application. The
roadmap calls for examples to grow with the public framework surface: typed writes,
operational configuration, middleware, schema generation, and production deployment will
be added only as those capabilities ship.
Roadmap
The next work is organized around four gates:
- harden HTTP parsing and public failure behavior;
- complete the framework contract with request context, middleware, schemas, and richer response handling;
- publish reproducible GIL and free-threaded benchmarks with correctness parity; and
- add production operations without replacing the synchronous execution model.
The detailed milestones and non-goals live in ROADMAP.md.
Contributing
Start with CONTRIBUTING.md, then read
docs/reviewing.md before opening a change. Useful contributions
include:
- executable examples under
examples/; - real-client HTTP acceptance tests and malformed-request coverage;
- reproducible
wrkbenchmarks that compare successful equivalent responses; - parser, shutdown, and overload correctness;
- roadmap features with a focused issue and end-to-end tests; and
- documentation that clearly separates current behavior from planned architecture.
Set up and run the complete local gate with:
uv sync --all-extras
make check
User-facing changes require a Towncrier fragment. See
changelog.d/README.md.
License
MIT — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file dexpot-0.1.1.tar.gz.
File metadata
- Download URL: dexpot-0.1.1.tar.gz
- Upload date:
- Size: 677.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
56756cd6796d1b4fd5d2f079d6932c580e8db30e8d1deb397bcaeab4746f08c6
|
|
| MD5 |
2c60d0eb5429b6f265acb4d82e1f2cce
|
|
| BLAKE2b-256 |
a726b137e8c325cb8f13ce29ae289526dc0a17ffdbda38f78f07eb77fde0775d
|
Provenance
The following attestation bundles were made for dexpot-0.1.1.tar.gz:
Publisher:
release.yml on tugrulguner/dexpot
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dexpot-0.1.1.tar.gz -
Subject digest:
56756cd6796d1b4fd5d2f079d6932c580e8db30e8d1deb397bcaeab4746f08c6 - Sigstore transparency entry: 2612298372
- Sigstore integration time:
-
Permalink:
tugrulguner/dexpot@50865cd35801a47697710c24eaa4e0481e2723cc -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/tugrulguner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@50865cd35801a47697710c24eaa4e0481e2723cc -
Trigger Event:
push
-
Statement type:
File details
Details for the file dexpot-0.1.1-py3-none-any.whl.
File metadata
- Download URL: dexpot-0.1.1-py3-none-any.whl
- Upload date:
- Size: 22.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ff9fcd3494bd9e4e37487d18a9c2916ace17996611541a732f69078ee3ea3426
|
|
| MD5 |
0835a8f4607473c580a41ec68542627c
|
|
| BLAKE2b-256 |
c571332c70fdc885cd01b0bb7728e6c220e2da71270c23ffb9856cc5b0aea8d7
|
Provenance
The following attestation bundles were made for dexpot-0.1.1-py3-none-any.whl:
Publisher:
release.yml on tugrulguner/dexpot
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dexpot-0.1.1-py3-none-any.whl -
Subject digest:
ff9fcd3494bd9e4e37487d18a9c2916ace17996611541a732f69078ee3ea3426 - Sigstore transparency entry: 2612298377
- Sigstore integration time:
-
Permalink:
tugrulguner/dexpot@50865cd35801a47697710c24eaa4e0481e2723cc -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/tugrulguner
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@50865cd35801a47697710c24eaa4e0481e2723cc -
Trigger Event:
push
-
Statement type: