Effects & Handlers
Unsafe C-extension boundary
- aleffy(
- func: (P) → R,
- /,
Opt a callable into the experimental unsafe C-extension boundary.
ctypesfunction pointers are unsupported. Native resources that remain live across an effect must be safe for multi-shot stack copying.
aleffy(func) explicitly opts a callable into native stack capture. It is
intended for calls into regular CPython C extensions when an effect may be
performed by a Python callback. The C prefix before the effect is not replayed;
each continuation resumes from the captured native instruction and stack
position.
Warning
This API copies native stack bytes. It cannot discover or clone ownership represented by pointers in C local variables. At every effect point, the caller must ensure that the captured C suffix is safe to execute once for every multi-shot branch. In particular, it must not carry unique owned Python references, allocations, file descriptors, library handles, or locks that multiple branches would release, mutate, or unlock.
Direct and indirect calls through ctypes are unsupported. Passing a
ctypes function pointer directly to aleffy raises TypeError;
indirect calls made by a wrapper cannot be detected reliably and must not
perform effects.
Capture and resume must occur in the same OS thread and interpreter, and resumes must be sequential. The current implementation supports CPython 3.12 through 3.14 on Linux x86-64, macOS x86-64/arm64, and Windows x64.
Unsupported C-boundary monitoring
Aleff enables C-boundary monitoring by default. When a continuation snapshot
crosses a C boundary without an installed continuation adapter or an explicit
aleffy() opt-in, Aleff emits
CFrameContinuationWarning. Disabling monitoring suppresses the
diagnostic only; it does not make an unsupported boundary safe.
Diagnostics for unsupported C boundaries captured by continuations.
- exception CFrameContinuationWarning
Bases:
RuntimeWarningA continuation snapshot crossed an unsupported C boundary.
Prohibited callback contexts
Warning
Effects must not be performed, and Resume and
ResumeAsync continuations must not be invoked, from any of the
following callback contexts:
Python data model:
__del__
built-in types:
dict.setdefaultstr.formatstr.format_map
sys:sys.addaudithooksys.settracesys.setprofilesys.monitoring.register_callback
threading:threading.settracethreading.settrace_all_threadsthreading.setprofilethreading.setprofile_all_threads
signal:signal.signal
weakref:weakref.refweakref.proxyweakref.WeakMethodweakref.finalize
gc:gc.callbacks
array:array.array
itertools:itertools.teeitertools._tee.__next__
codecs:codecs.register_error
sqlite3:sqlite3.adaptsqlite3.Cursor.executesqlite3.Cursor.executemanysqlite3.Cursor.fetchonesqlite3.Cursor.fetchmanysqlite3.Cursor.fetchallsqlite3.Cursor.__next__sqlite3.Connection.text_factorysqlite3.Connection.row_factorysqlite3.Cursor.row_factorysqlite3.Connection.create_functionsqlite3.Connection.create_aggregatesqlite3.Connection.create_window_functionsqlite3.Connection.create_collationsqlite3.Connection.set_authorizersqlite3.Connection.set_progress_handlersqlite3.Connection.set_trace_callback
xml.etree.ElementTree:xml.etree.ElementTree.XMLParser.feedxml.etree.ElementTree.XMLParser.close
xml.parsers.expat:xml.parsers.expat.xmlparser.Parsexml.parsers.expat.xmlparser.ParseFile
ssl:ssl.SSLContext.wrap_socketssl.SSLSocket.acceptssl.SSLSocket.connectssl.SSLSocket.connect_exssl.SSLSocket.do_handshakessl.SSLSocket.readssl.SSLSocket.recvssl.SSLSocket.recv_intossl.SSLSocket.sendssl.SSLSocket.sendallssl.SSLSocket.unwrapssl.SSLSocket.writessl.SSLObject.do_handshakessl.SSLObject.readssl.SSLObject.unwrapssl.SSLObject.write
readline:readline.set_completerreadline.set_completion_display_matches_hookreadline.set_pre_input_hookreadline.set_startup_hook
contextvars:contextvars.Context.run
os/posix/nt:os._exitos.accessos.add_dll_directoryos.chdiros.chflagsos.chmodos.chownos.chrootos.closeos.closerangeos.confstros.copy_file_rangeos.device_encodingos.dupos.dup2os.eventfdos.eventfd_reados.eventfd_writeos.execvos.execveos.fchdiros.fchmodos.fchownos.fdatasyncos.fpathconfos.fspathos.fstatos.fstatvfsos.fsyncos.ftruncateos.get_blockingos.get_handle_inheritableos.get_inheritableos.get_terminal_sizeos.getgrouplistos.getpgidos.getpriorityos.getrandomos.getsidos.getxattros.grantptos.initgroupsos.isattyos.killos.killpgos.lchflagsos.lchmodos.lchownos.linkos.listdiros.listmountsos.listxattros.lockfos.login_ttyos.lseekos.lstatos.majoros.makedevos.memfd_createos.minoros.mkdiros.mkfifoos.mknodos.niceos.openos.pathconfos.pidfd_openos.pipe2os.plockos.posix_fadviseos.posix_fallocateos.posix_openptos.posix_spawnos.posix_spawnpos.preados.preadvos.ptsnameos.pwriteos.pwritevos.reados.readintoos.readlinkos.readvos.removeos.removexattros.renameos.replaceos.rmdiros.scandiros.sched_get_priority_maxos.sched_get_priority_minos.sched_getaffinityos.sched_getparamos.sched_getscheduleros.sched_rr_get_intervalos.sched_setaffinityos.sched_setparamos.sched_setscheduleros.sendfileos.set_blockingos.set_handle_inheritableos.set_inheritableos.setegidos.seteuidos.setgidos.setnsos.setpgidos.setpriorityos.setregidos.setresgidos.setresuidos.setreuidos.setuidos.setxattros.spawnvos.spawnveos.spliceos.startfileos.statos.statvfsos.strerroros.symlinkos.sysconfos.tcgetpgrpos.tcsetpgrpos.timerfd_createos.timerfd_gettimeos.timerfd_gettime_nsos.timerfd_settimeos.timerfd_settime_nsos.truncateos.ttynameos.umaskos.unlinkos.unlockptos.unshareos.urandomos.utimeos.wait3os.wait4os.waitidos.waitpidos.WCOREDUMPos.WEXITSTATUSos.WIFCONTINUEDos.WIFEXITEDos.WIFSIGNALEDos.WIFSTOPPEDos.WSTOPSIGos.WTERMSIGos.writeos.writev
fcntl:fcntl.fcntlfcntl.ioctlfcntl.flockfcntl.lockf
termios:termios.tcgetattrtermios.tcsetattrtermios.tcsendbreaktermios.tcdraintermios.tcflushtermios.tcflowtermios.tcgetwinsizetermios.tcsetwinsize
select:select.selectselect.poll.registerselect.poll.modifyselect.poll.unregisterselect.epoll.registerselect.epoll.modifyselect.epoll.unregisterselect.devpoll.registerselect.devpoll.modifyselect.devpoll.unregisterselect.kevent
socket:socket.socketsocket.socket.sendsocket.socket.sendallsocket.socket.sendtosocket.socket.sendmsgsocket.socket.recv_intosocket.socket.recvfrom_intosocket.socket.recvmsg_intosocket.socket.setsockopt
mmap:mmap.mmapmmap.mmap.write
atexit:atexit.register
def effect
- effect(name: str, /) Effect[..., Any]
- effect(
- *functions: (...) → Any,
Create an effect or an effect-set decorator.
effect("name")— create a newEffectwith the given name.
# Create effects read = effect("read") write = effect("write")
@effect(e1, e2, ...)— decorate a function to declare which effects it uses. The decorated function gains an__effects__attribute (afrozensetof effects) that can be retrieved witheffects(fn).
# Use as decorator to declare effect dependencies @effect(read, write) def process(): s = read() return write(s)
class Effect
- class Effect[**P, R]
Bases:
Protocol,Generic[P,R]An algebraic effect declaration.
Effects are created via the
effect()factory and invoked like regular function calls. A handler intercepts these calls and provides the implementation at runtime.Pis the parameter types andRis the return type of the effect.# effect: () -> str read: Effect[[], str] = effect("read") # effect: str -> int write: Effect[[str], int] = effect("write") # Inside a handler, effects are called like regular functions: x = read() # :: str n = write("data") # :: int
class Resume
- class Resume[R, V]
-
Continuation passed to synchronous effect handlers.
Ris the type of the value passed to the continuation,Vis the return type of the handled computation.Calling
k(value)resumes the suspended computation with value and drives it to completion (or to the next effect). The return value ofk()is the final result of the handled computation.@h.on(read) def _read(k: Resume[str, int]) -> int: # k :: str -> int n = k("read from file") # resume with "read from file", get the final result return n
The handler may call
kzero times (abort), once (one-shot), or multiple times (multi-shot).
class ResumeAsync
- class ResumeAsync[R, V]
-
Continuation passed to asynchronous effect handlers.
Ris the type of the value passed to the continuation,Vis the return type of the handled computation.Same semantics as
Resume, butawait k(value)is required because the handler function isasync def.@h.on(read) async def _read(k: ResumeAsync[str, int]) -> int: # k :: str -> Awaitable[int] n = await k("read from file") return n
class Handler
- class Handler[V]
-
Protocol for synchronous effect handlers.
Create instances via
create_handler. Register effect implementations withon, then invoke the handler with a caller function:h: Handler[str] = create_handler(read, write) # h: () -> str @h.on(read) def _read(k: Resume[str, str]) -> str: return k("data") result = h(lambda: read()) # :: str
class AsyncHandler
- class AsyncHandler[V]
-
Protocol for asynchronous effect handlers.
Create instances via
create_async_handler. Handler functions areasync defand receive aResumeAsynccontinuation:h: AsyncHandler[str] = create_async_handler(read) # h: () -> Awaitable[str] @h.on(read) async def _read(k: ResumeAsync[str, str]) -> str: return await k("data") result = await h(lambda: read()) # :: str
- on(
- effect: Effect[P, R],
Register an async handler function for effect. Returns a decorator.
def create_handler
- create_handler( ) Handler[Any]
Create a synchronous handler that handles the given effects.
Register implementations with
on, then call the handler with a caller function to run the computation.If shallow is
True, the handler is removed from the handler stack after handling one effect occurrence. Subsequent occurrences of the same effect will not be caught by this handler.read: Effect[[], str] = effect("read") h: Handler[str] = create_handler(read) @h.on(read) def _read(k: Resume[str, str]): return k("hello") result = h(lambda: read() + " world") # result == "hello world"
def create_async_handler
- create_async_handler( ) AsyncHandler[Any]
Create an asynchronous handler that handles the given effects.
Handler functions are
async defand receiveResumeAsync. The caller function runs in a greenlet; effect invocations are synchronous from the caller’s perspective.If shallow is
True, the handler is removed from the handler stack after handling one effect occurrence.read: Effect[[], str] = effect("read") h: AsyncHandler[str] = create_async_handler(read) @h.on(read) async def _read(k: ResumeAsync[str, str]): return await k("hello") result = await h(lambda: read() + " world") # result == "hello world"
class EffectNotHandledError
- class EffectNotHandledError[**P, R]
Bases:
RuntimeError,Generic[P,R]Raised when an effect is invoked but no handler is active for it.
def effects
- effects(
- fn: (...) → Any,
Return the set of effects used by the given function.
The function must have been decorated with
@effect(e1, e2, ...).read: Effect[[], str] = effect("read") write: Effect[[str], int] = effect("write") @effect(read, write) def process(): s = read() return write(s) effects(process) # frozenset({read, write})
def unhandled_effects
- unhandled_effects(
- fn: (...) → Any,
- *handlers: Handler | AsyncHandler,
Return the set of effects used by fn that are not handled by handlers.
read: Effect[[], str] = effect("read") write: Effect[[str], int] = effect("write") @effect(read, write) def process(): ... h = create_handler(read) @h.on(read) def _read(k): return k("") unhandled_effects(process, h) # frozenset({write})