Skip to main content

dexpot

dexpot synchronous Python API framework

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.

CI PyPI version Python versions GitHub stars MIT License

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_REUSEPORT workers.
  • 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.

dexpot is alpha software and is not yet recommended for untrusted production traffic. Its socket core now fails closed on malformed framing and enforces request limits, but production operations such as access logging, metrics, trusted-proxy policy, TLS guidance, middleware, OpenAPI, streaming, and authentication remain on 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)

HTTP limits

Every connection enforces conservative defaults: an 8 KiB request line, 64 KiB request head, 100 headers, a 16 MiB body, a five-second idle read timeout, a ten-second absolute head deadline, and a thirty-second absolute body deadline. Override them as one typed, immutable policy rather than adding transport options to route decorators:

from dexpot import Dex, HttpLimits

app = Dex(
    limits=HttpLimits(
        request_line_bytes=4 * 1024,
        header_bytes=32 * 1024,
        header_count=64,
        body_bytes=2 * 1024 * 1024,
        idle_read_seconds=10.0,
        head_read_seconds=15.0,
        body_read_seconds=60.0,
    )
)

Values must be positive, and the total request-head allowance must exceed the request-line allowance. Oversized or timed-out requests receive a stable error and the connection closes.

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 *args or **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

The current alpha release has these boundaries:

  • HTTP/1.0 and HTTP/1.1 requests with validated Content-Length framing are supported; transfer encodings, including chunked request bodies, are rejected and the connection closes.
  • Request-line, total-header, header-count, and body limits plus idle and absolute head/body deadlines are enforced before request data can accumulate or drip indefinitely.
  • Request targets are currently origin-form only (/path?query); absolute-form proxy targets are rejected during this alpha milestone.
  • Routing distinguishes 404 from 405, returns Allow for method mismatches, percent-decodes UTF-8 paths safely, and treats duplicate or trailing slashes as distinct paths.
  • 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 exceptions produce a stable public 500 body while the traceback is logged server-side. Structured logging and request IDs remain production-operations work.
  • 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

The runnable progression under examples/ exercises the shipped framework through its real socket server:

  • minimal.py: typed path capture and response;
  • typed_crud.py: thread-safe shared state, typed JSON writes, and a complete create/read/update/delete lifecycle; and
  • bounded_api.py: custom HttpLimits, 413/422 failures, and 405 method handling.

Each example runs directly with uv run python examples/<name>.py, and the test suite launches every example as a subprocess and validates its public HTTP behavior. Examples will continue to grow only as middleware, schemas, deployment support, and other roadmap capabilities ship.

Roadmap

The remaining work is organized around three gates after the completed HTTP-hardening gate:

  1. complete the framework contract with request context, middleware, schemas, and richer response handling;
  2. publish reproducible GIL and free-threaded benchmarks with correctness parity; and
  3. 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 wrk benchmarks 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.

Open an issue before substantial handler, routing, parser, scheduler, protocol, or execution contract work. Focused fixes, tests, documentation, and maintenance may be direct pull requests when they do not introduce a new public contract.

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.

Support dexpot

  • Ask usage questions in GitHub Discussions.
  • Report reproducible failures through the issue forms.
  • Discuss substantial API or execution-model changes before implementation.
  • If you want to follow dexpot's progress, use the Star control on the repository.

License

MIT — see LICENSE.

Metadata

Release files for dexpot 0.2.0

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

Source distribution (sdist)

Source distribution for dexpot 0.2.0
File Size Uploaded
dexpot-0.2.0.tar.gz 699.3 kB Details

Built distribution (wheel)

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

Total release size: 728.4 kB

Release files / dexpot-0.2.0.tar.gz

Download URL dexpot-0.2.0.tar.gz
Size 699.3 kB
Tags Source
SHA-256 checksum
How to use checksums
9b63637be62d568f731abc9284e8eed2c55f625f9d059469623b3c42f354da44
BLAKE2b-256 checksum
How to use checksums
a051c038624ba0ca0d521620fbcc4b39bc4223d8d99f59ac8dd60f0540a43875
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 / dexpot-0.2.0-py3-none-any.whl

Download URL dexpot-0.2.0-py3-none-any.whl
Size 29.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
d2a1539b0d244b606e726eade508e8065dd709a6581928949ce3c993000a548d
BLAKE2b-256 checksum
How to use checksums
7f05fb7c9fce06840e517976fb8594a82f0d232a4c1cff3758eec9ee53096265
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.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

This release

0.2.0 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