Python semantics¶
py2axum compiles your project's code to Rust over a dynamic value type with CPython's semantics. This page
lists what is reproduced, what is refused (with file:line) and the known differences. The standard library is
in Standard library, third-party libraries in Libraries.
Values, functions, classes, modules¶
- Values and operators with CPython's semantics: int (64-bit: beyond is an
OverflowError), float formatting andrepr, str methods (isdigit,isdecimal,isnumeric,isalpha,isalnum,isspacefrom the Unicode database of the Python that transpiles),%/format/f-strings (presentationsd f % e g x o band their upper-case forms,#; notn,c,=with non-numbers), slicing, comparisons,**, bit operators, truthiness,hash()rules (unhashable Pydantic models and dataclasses unless frozen,__hash__ = None,__eq__without__hash__);hash(int)is CPython's, other hashes are stable but not CPython's (CPython randomizes str hashes anyway). Known difference: a set iterates in insertion order; CPython's order for strings changes from one process to the next (randomized hashes), so text built from a set of strings (", ".join(ALLOWED)) cannot be reproduced. - Builtins take CPython's keyword arguments where the runtime implements them (
sorted(key=, reverse=),min/max(key=, default=),enumerate(start=),sum(start=),round(ndigits=),int(base=),zip(strict=)); any other is refused. Difference:zip(strict=True)over iterables of different lengths raises at the call, where CPython raises after yielding the common prefix.range()builds its items (CPython's range is lazy): a range too large to hold is aMemoryErrorat the call, even if the loop over it would have stopped early. - Functions: keyword/default/
*args/**kwargsbinding with CPython'sTypeErrors, closures, lambdas, nested functions and decorators (functools.wraps, decorator factories; a decorated function is built once at startup), recursion,global(one cell per process), generators,match. A variable a nested function or lambda reads is a cell, as in CPython: read when the inner function runs, not when it is defined ([lambda: i for i in range(3)]all see the lasti; a variable assigned after thedefraises CPython'sNameErrorif read before).nonlocalrebinds a local of the enclosing function. Refused:nonlocalof a parameter of the enclosing function, a project decorator on a method. - Classes: plain classes (
__init__, methods, properties, static/class methods, class attributes,__slots__; single inheritance from another plain class and/orabc.ABC,@abstractmethod(instantiating a class left abstract raises CPython'sTypeErrorwhen called by name),super().__init__(...)),@dataclass(incl.frozen=True,__post_init__;dataclasses.astuple()/asdict(), recursive like CPython), exceptions (class attributes, methods,super().__init__ofHTTPException), Enums (methods,_missing_). Special methods:__str__,__repr__,__eq__(used bystr(), f-strings,==,in,index,count,remove),__enter__/__exit__,__aenter__/__aexit__; they aredefs (the only I/O one may do is a synchronous session's lazy load, a__repr__reading a deferred column: done blocking, as CPython does); other special methods are refused. In containers, an exception raised by__eq__counts as "not equal" (CPython propagates it). - Types as values:
list[X],X | None, library classes (BaseModel,AsyncSession...),isinstance/issubclasswith run-time types,inspect.isclass,typing.get_args/get_origin/ get_type_hints(annotations kept on decorated functions). type(x): compared with==/is/in, called (type(x)(...)for builtin types),__name__/__qualname__(CPython's names:Pattern,UUID,builtin_function_or_method; a Pydantic model class is aModelMetaclass, an Enum class anEnumType). Differences: values the runtime does not tell apart report the type they are stored as (frozenset→set, iterators andrange→list), a mapped class reportstype. A project class obtained as a value (cls(**data)in a classmethod,type(m)(a=1), a class passed as an argument) is called like the class named in the source: Pydantic model, settings (environment read again), dataclass, plain class, Enum,SimpleNamespace, with CPython'sTypeErrors. Difference: pydantic-settings init options (positional arguments,_env_prefix=…) on a settings class held as a value raise aTypeError.- Module globals are evaluated at startup in import order, like importing the app; module-level calls too, and
module-level
try,if,for,whileandwithstatements, whose bindings are module variables (except E as enames are deleted as in CPython). Atrywhose body only imports and assigns constants is an import fallback: its imports were resolved at compile time, so itsexceptbranches never run.if __name__ == "__main__":andif TYPE_CHECKING:blocks are skipped. Refused: a module variable bound by several module-level statements when one of them is compound (X = 1thentry: X = f()). Attributes and methods of a library object such asprometheus_client.REGISTRYare resolved at run time. A module-level assignment to an attribute of a library the binary knows nothing of (stripe.api_key = ...) is left to the Python side: every read or call of that library is refused already. Known difference: a module-level call of a project function that does not translate (typically a logging setup with handlers, formatters and filters, which the binary does not reproduce: it logs in its own format) fails at startup with anERROR:py2axum:module globalline on stderr and the binary goes on without its effects; the routes that read a global it would have set raise that error. importlib.import_module("pkg.mod")with a literal name of a project module returns a module object (getattr/hasattrwith run-time names, attribute calls,__name__). The module is compiled into the binary and its globals are evaluated at startup with the others (CPython: at the first import), so a "lazy import" saves neither memory nor startup time. A top-level name that does not translate raises when read. Refused: computed module names, library modules, modules withimport *.sys.settrace(f),threading.settrace(f),threading.settrace_all_threads(f),sys.gettrace(): when a project calls one of them, its compiled functions (methods, nested functions, lambdas) reportcall, thenreturnorexception(raised there, coming from a callee, or caught there) followed byreturnwith None, to a process-wide trace function that is not traced itself. Frames exposef_globals["__name__"],f_code.co_qualname/co_name/co_filename/co_firstlinenoandf_lineno; tracebackstb_lineno,tb_frame,tb_next, andtraceback.extract_tb. Differences: onecall/returnpair per invocation (a coroutine suspended by anawaitdoes not report each suspension and resumption), noline/opcodeevents, only project frames exist (library frames are absent from tracebacks, so depths count project frames), a frame's line is the line where its current statement starts. Without such a call the functions are compiled without any of this.- A module-level function is one object (
is, attributes set on it); a nested function naming itself reads its name when called, as CPython's closure cell. - Memory: values are reference-counted, without CPython's cycle collector. A nested function that calls
itself (bound once by its
def) refers to itself weakly and is freed as in CPython; ORM objects linked by relationships are released when their session ends. Other reference cycles the application builds (a.append(a), two nested functions calling each other, a decorated nested function naming itself, an object stored in a container it owns) are never freed: each request that builds one keeps it until the process exits. A finished task stored in the request's state (request.state.t = asyncio.create_task(...)) is freed with the request, unless it failed and its exception was never retrieved. typing.TypedDictclasses: calling one builds a plaindict(keys neither checked nor completed, as in CPython); as a Pydantic field or a response model, refused.map/filterreturn lists (materialized);frozensetbehaves asset;callable(); a builtin exception
asyncio and threading¶
- A call of an
async defthat is not awaited is a coroutine object (arguments evaluated at the call, body run at the firstawait; a secondawaitraises like CPython).asyncio.gather(concurrent tasks, argument order,return_exceptions),create_task+await task,add_done_callback,Semaphore,Lock,Queue,Event(set,clear,is_set,await wait()),Futureandloop.create_future()(set_result,set_exception,result,exception,done,cancel,add_done_callback,await; itsrepr()does not list the callbacks),InvalidStateError,wait_for(of a call, a task or a future),sleep,to_thread,open_connection(host, port)(the addresses ofgetaddrinfotried in order like asyncio, itsConnectionRefusedError/OSError("Multiple exceptions: ...")messages:[Errno 61] Connect call failed ('127.0.0.1', 9)under asyncio,[Errno 61] Connection refusedunder uvloop, which uvicorn uses when the project'suv.lockinstalls it (uvicorn[standard]);StreamWriter.write/drain/close/is_closing/wait_closed/get_extra_info("peername"|"sockname"),StreamReader.read/readline/at_eof;ssl=,limit=and the other keywords are refused, and so arereadexactly/readuntil). The runtime is multi-threaded: two tasks finishing at the same instant have no guaranteed order (asyncio follows creation order). - The delays of
asyncio.sleep(delay, result)andasyncio.wait_for(aw, timeout)are read like asyncio:delay <= 0first (astrraises itsTypeError), then the loop's float time plus the delay (aDecimalraisesTypeError, except forasyncio.sleepon uvloop, which rounds it); a timeout<= 0raisesTimeoutErrorwithout running the awaitable. Differences: a NaN delay or timeout raises a py2axumTypeError(asyncio and the uvloop versions each do something else), and with a timeout<= 0the awaitable's arguments are not evaluated (asyncio evaluates them, then cancels the coroutine unstarted). async with/withfollow CPython's protocol (the exception is passed to__exit__, a true result suppresses it).threading.Lock/RLock/Event/Thread/get_ident,asyncio.new_event_loop()+run_forever()in a thread,call_soon,run_coroutine_threadsafe+wrap_future,loop.run_in_executor(None, f): emulated on tokio. The event loop is one "thread" (all coroutines share its ident);Threadand executor calls run on their own OS threads. Locks block like CPython's.asyncio.run()raises CPython'sRuntimeError(always inside a running loop). Library calls that are not awaited (a bareasyncio.sleep(1)) run immediately.- Async generators run in lockstep with their consumer like CPython: the body starts at the first
__anext__,anext(),asend,athrow,aclose(GeneratorExitat theyield,finallyblocks run).@asynccontextmanagerfollowscontextlib(exception thrown in at theyield,generator didn't yield,generator didn't stop).contextlib.suppress(*excs). task.cancel(),task.cancelled():await taskthen raisesCancelledError. Difference: the task's coroutine is stopped at its currentawaitwithoutCancelledErrorbeing raised inside it (its ownexcept CancelledError/finallyblocks do not run).contextvars.ContextVar(getwith or without default,set,reset(token)): values belong to the request being served (or to the lifespan). Difference: a task created withcreate_taskshares its creator's values instead of a copy (a value it sets is seen by the creator).
The lifespan (FastAPI(lifespan=...)) is described in Lifespan events.