Supported subset and known differences¶
py2axum translates a closed subset of Python and of a list of libraries. Anything outside it is refused at
compile time with file:line (and counted by --report), so a construct is either translated with the
behaviour described in these pages or not translated at all. Method calls and attribute reads on values only
known at run time are checked by name: a name that no runtime type implements and that the project never
defines is refused (a name that exists on another type than the actual receiver is not detected). Each item of
these pages is covered by conformance cases that compare the binary with the Python application, response by
response.
"Differences" are the places where the binary is knowingly not identical to CPython; they are listed so you can decide whether they matter for your application.
New to py2axum? Start with the getting-started guide;
py2axum check tells you what these pages mean for your application, route by
route. To check the binary against your application, see Conformance and replay.
There is one backend since 0.4 (the statically-typed one was removed: it covered only simple CRUD handlers;
its list streaming moved to Large list responses). --backend auto and
--backend dyn are still accepted until 1.0 and change nothing; --backend typed is an error.
Where each part is described¶
| Topic | Page |
|---|---|
| Supported versions | Versions |
Python 3.12 to 3.15: what each version changes, sys.version_info |
Python versions |
| Applications, routers, factories, imports, routing, mounts, raw ASGI routes | Bigger applications |
| Path, query and header parameters, timestamps | Parameters |
JSON bodies, JSON decoding, strict_content_type |
Request body |
Form(), File(), UploadFile |
Forms and files |
| Dependencies, the session dependency | Dependencies |
| Security schemes (OAuth2, HTTP Bearer, HTTP Basic) | Security |
Responses, response classes, status codes, StreamingResponse, generator endpoints (JSON Lines, Server-Sent Events) |
Responses |
BackgroundTasks |
Background tasks |
Exception handlers, HTTPException, Starlette's error layers |
Handling errors |
Middleware (BaseHTTPMiddleware, raw ASGI, GZip, starlette_context), request attributes, configure(app) |
Middleware |
CORSMiddleware |
CORS |
| Shutdown (SIGTERM, SIGINT) | Graceful shutdown |
logging, python-json-logger, uvicorn's own lines (startup, access log, errors), tracebacks, uvicorn's command-line options |
Logging |
| WebSockets | WebSockets |
| Pydantic v2 | Models (Pydantic) |
| SQLAlchemy 2.0 (async and sync, PostgreSQL) | SQL databases |
| Python semantics, asyncio and threading | Python semantics |
| Lifespan | Lifespan events |
Sentry (sentry-sdk 2.x) |
Sentry |
Prometheus (prometheus_client 0.26) |
Prometheus metrics |
MCP servers (mcp 2.2) |
MCP servers |
Standard library, pickle |
Standard library |
| Libraries | Libraries |
| Hybrid deployments | Hybrid mode |
| Large list responses | Large list responses |
Runtime environment¶
The binary takes uvicorn's command-line options and prints uvicorn's lines and the application's logging
records as Python does (Logging; a traceback holds the project's frames only).
It reads its configuration from the environment (the list);
it does not read .env files (pydantic-settings reads environment variables only, see
Models): export the variables, as an orchestrator does.
Limits on untrusted input (an optional body cap, the stack size) and the differences they imply are in
Security § Request input.
Deeply nested JSON on Python 3.14+¶
From Python 3.14, json.loads is bounded by the C stack instead of the recursion limit: a document nested
beyond 10 000 levels decodes in CPython (up to roughly 50 000 levels on an 8 MiB stack, then
RecursionError: Stack overflow (used ... kB) while decoding a JSON array from a unicode string). The binary
keeps the 10 000-level limit of 3.12 and 3.13 on every Python (RecursionError: maximum recursion depth exceeded
while decoding a JSON array from a unicode string), a bound on untrusted input
(Security). Such a document usually exceeds the recursion limit later in Python
(FastAPI's jsonable_encoder when it is returned): the response is the same 500, the logged traceback differs
(where the exception is raised, and its message).
PyYAML¶
yaml.safe_load and the dump functions follow PyYAML 6.0's pure-Python implementation (its SafeLoader, which
safe_load always uses, even when libyaml is installed). Where the binary cannot follow it:
- An integer beyond 64 bits (
9223372036854775808,!!intof a long digit string) raisesOverflowError: py2axum: the YAML integer ... does not fit in 64 bits, asjson.loadsdoes; CPython returns a Python int. A digit string past 4 300 digits raises CPython's ownValueError(the int-conversion limit). - A
\uD800-\uDFFFescape in a double-quoted scalar (a lone surrogate) becomes U+FFFD: a Rust string cannot hold it (CPython keeps it in the str, which FastAPI then fails to encode as JSON: a 500). - The nesting depth at which the recursive steps raise
RecursionError: maximum recursion depth exceededis fixed: a collection nested 471 levels deep loads, one 472 deep does not; a list nested 316 deep dumps, one 317 deep does not. CPython's limit depends on the stack of the caller (those are its depths from an async endpoint under uvicorn with Python 3.12; a sync endpoint, another Python or a deeper call chain moves them). yaml.dump(the fullRepresenter) writes the builtin types as PyYAML does (!!python/tuple,!!python/complex,!!set,!!binary...); a value it would write as!!python/object...,!!python/name:...or!!python/object/apply:...(a project class, a Pydantic model, an ORM object, an Enum member,Decimal,UUID,time,timedelta...) raisesTypeError: py2axum: yaml.dump() of a ... object is not supported.yaml.safe_dumpraises PyYAML's ownRepresenterError('cannot represent an object', data)for them.- Anchors follow object identity: a list, dict, set or tuple met twice is written once with an anchor, as in
CPython; a
dateordatetimeobject met twice is not (the binary's dates are values, without identity), and an equal tuple written twice in the source is two objects in the binary where CPython may fold the constants into one. - The order of a
set(!!set,sort_keys=False) is its insertion order, not CPython's hash order (which varies from one process to the other for strings). encoding=accepts UTF-8, UTF-16, UTF-32 (with or without-le/-be), ASCII and Latin-1 (TypeErrorfor another codec); a text file read bysafe_loadis decoded as UTF-8 with universal newlines.
croniter¶
croniter 6.2 is ported line by line (Libraries); what the binary does not reproduce:
R(random) fields: Python draws a random number. A literal expression containing one is refused at compile time; one built at run time raisesNotImplementedError(py2axum) where Python would draw the number (after the errors croniter raises before it),is_validincluded.for x in croniter(...)andfor x in cron.all_next()/all_prev()(orlist()of them) raise a py2axumTypeError: these iterators are endless and iteration is materialised; read them withnext().croniter_range()is a lazy generator (next(),for,list()), but aforloop computes every value of the range before its first iteration.croniter.expand()of an expression holding an integer of more than 4300 digits: Python'sCroniterBadCronErrorcarries the formatted traceback of theValueError, the binary its message only.- The
CroniterUnsupportedSyntaxErrormessage prints each nth set with its numbers first, then'l'; Python's order of a set mixing both depends on the string hash seed (PYTHONHASHSEED). - Time zones:
datetime.timezoneandzoneinfo.ZoneInfo(pytz and dateutil zones are not translated); the zone rules are the runtime's tz database, Python's those of the system or of thetzdatapackage. - A start time that
gmtimecannot represent raises the platform'sOSError([Errno 75]on Linux). croniter_range(_croniter=),croniter.expand(from_timestamp=)andis_valid(encoding=)other than UTF-8 are refused at compile time; a start time other than adatetime, anintor afloat, andcroniter_rangebounds other than datetimes and numbers raise a py2axumTypeError.
Not supported¶
An application wrapped in a project ASGI class at module level (app = Wrapper(api), refused: the
binary would serve api without it) and mounted ASGI apps (left to Python with --python-side mount when
registered last), libraries not listed, C extensions, eval/exec, metaclasses, multiple inheritance of
project classes, OpenAPI /docs in the binary, and assigning an attribute of the application at module level
other than app.state and app.dependency_overrides[f] = g (app.router.route_class = ..., a custom
APIRoute, app.openapi = ... are refused with file:line). Not native, counted apart in the
documentation coverage: what only changes the OpenAPI schema or the documentation pages
(/openapi.json, /docs, custom docs assets), SQLModel, OpenTelemetry's FastAPI(telemetry=...), GraphQL
routers, ORJSONResponse/UJSONResponse, stdlib dataclasses as request or response types, security scopes
(SecurityScopes, Security(scopes=)), AfterValidator and other functional validators in Annotated.