Effects & Handlers

Unsafe C-extension boundary

aleffy(
func: (P) → R,
/,
) (P) → R

Opt a callable into the experimental unsafe C-extension boundary.

ctypes function 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: RuntimeWarning

A continuation snapshot crossed an unsupported C boundary.

c_warnings_enabled() bool

Return whether unsupported C-boundary monitoring is active.

disable_c_warnings() None

Disable C-boundary warnings and release Aleff’s monitoring tool ID.

enable_c_warnings() None

Enable unsupported C-boundary warnings for Aleff snapshots.

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.setdefault

    • str.format

    • str.format_map

  • sys:

    • sys.addaudithook

    • sys.settrace

    • sys.setprofile

    • sys.monitoring.register_callback

  • threading:

    • threading.settrace

    • threading.settrace_all_threads

    • threading.setprofile

    • threading.setprofile_all_threads

  • signal:

    • signal.signal

  • weakref:

    • weakref.ref

    • weakref.proxy

    • weakref.WeakMethod

    • weakref.finalize

  • gc:

    • gc.callbacks

  • array:

    • array.array

  • itertools:

    • itertools.tee

    • itertools._tee.__next__

  • codecs:

    • codecs.register_error

  • sqlite3:

    • sqlite3.adapt

    • sqlite3.Cursor.execute

    • sqlite3.Cursor.executemany

    • sqlite3.Cursor.fetchone

    • sqlite3.Cursor.fetchmany

    • sqlite3.Cursor.fetchall

    • sqlite3.Cursor.__next__

    • sqlite3.Connection.text_factory

    • sqlite3.Connection.row_factory

    • sqlite3.Cursor.row_factory

    • sqlite3.Connection.create_function

    • sqlite3.Connection.create_aggregate

    • sqlite3.Connection.create_window_function

    • sqlite3.Connection.create_collation

    • sqlite3.Connection.set_authorizer

    • sqlite3.Connection.set_progress_handler

    • sqlite3.Connection.set_trace_callback

  • xml.etree.ElementTree:

    • xml.etree.ElementTree.XMLParser.feed

    • xml.etree.ElementTree.XMLParser.close

  • xml.parsers.expat:

    • xml.parsers.expat.xmlparser.Parse

    • xml.parsers.expat.xmlparser.ParseFile

  • ssl:

    • ssl.SSLContext.wrap_socket

    • ssl.SSLSocket.accept

    • ssl.SSLSocket.connect

    • ssl.SSLSocket.connect_ex

    • ssl.SSLSocket.do_handshake

    • ssl.SSLSocket.read

    • ssl.SSLSocket.recv

    • ssl.SSLSocket.recv_into

    • ssl.SSLSocket.send

    • ssl.SSLSocket.sendall

    • ssl.SSLSocket.unwrap

    • ssl.SSLSocket.write

    • ssl.SSLObject.do_handshake

    • ssl.SSLObject.read

    • ssl.SSLObject.unwrap

    • ssl.SSLObject.write

  • readline:

    • readline.set_completer

    • readline.set_completion_display_matches_hook

    • readline.set_pre_input_hook

    • readline.set_startup_hook

  • contextvars:

    • contextvars.Context.run

  • os / posix / nt:

    • os._exit

    • os.access

    • os.add_dll_directory

    • os.chdir

    • os.chflags

    • os.chmod

    • os.chown

    • os.chroot

    • os.close

    • os.closerange

    • os.confstr

    • os.copy_file_range

    • os.device_encoding

    • os.dup

    • os.dup2

    • os.eventfd

    • os.eventfd_read

    • os.eventfd_write

    • os.execv

    • os.execve

    • os.fchdir

    • os.fchmod

    • os.fchown

    • os.fdatasync

    • os.fpathconf

    • os.fspath

    • os.fstat

    • os.fstatvfs

    • os.fsync

    • os.ftruncate

    • os.get_blocking

    • os.get_handle_inheritable

    • os.get_inheritable

    • os.get_terminal_size

    • os.getgrouplist

    • os.getpgid

    • os.getpriority

    • os.getrandom

    • os.getsid

    • os.getxattr

    • os.grantpt

    • os.initgroups

    • os.isatty

    • os.kill

    • os.killpg

    • os.lchflags

    • os.lchmod

    • os.lchown

    • os.link

    • os.listdir

    • os.listmounts

    • os.listxattr

    • os.lockf

    • os.login_tty

    • os.lseek

    • os.lstat

    • os.major

    • os.makedev

    • os.memfd_create

    • os.minor

    • os.mkdir

    • os.mkfifo

    • os.mknod

    • os.nice

    • os.open

    • os.pathconf

    • os.pidfd_open

    • os.pipe2

    • os.plock

    • os.posix_fadvise

    • os.posix_fallocate

    • os.posix_openpt

    • os.posix_spawn

    • os.posix_spawnp

    • os.pread

    • os.preadv

    • os.ptsname

    • os.pwrite

    • os.pwritev

    • os.read

    • os.readinto

    • os.readlink

    • os.readv

    • os.remove

    • os.removexattr

    • os.rename

    • os.replace

    • os.rmdir

    • os.scandir

    • os.sched_get_priority_max

    • os.sched_get_priority_min

    • os.sched_getaffinity

    • os.sched_getparam

    • os.sched_getscheduler

    • os.sched_rr_get_interval

    • os.sched_setaffinity

    • os.sched_setparam

    • os.sched_setscheduler

    • os.sendfile

    • os.set_blocking

    • os.set_handle_inheritable

    • os.set_inheritable

    • os.setegid

    • os.seteuid

    • os.setgid

    • os.setns

    • os.setpgid

    • os.setpriority

    • os.setregid

    • os.setresgid

    • os.setresuid

    • os.setreuid

    • os.setuid

    • os.setxattr

    • os.spawnv

    • os.spawnve

    • os.splice

    • os.startfile

    • os.stat

    • os.statvfs

    • os.strerror

    • os.symlink

    • os.sysconf

    • os.tcgetpgrp

    • os.tcsetpgrp

    • os.timerfd_create

    • os.timerfd_gettime

    • os.timerfd_gettime_ns

    • os.timerfd_settime

    • os.timerfd_settime_ns

    • os.truncate

    • os.ttyname

    • os.umask

    • os.unlink

    • os.unlockpt

    • os.unshare

    • os.urandom

    • os.utime

    • os.wait3

    • os.wait4

    • os.waitid

    • os.waitpid

    • os.WCOREDUMP

    • os.WEXITSTATUS

    • os.WIFCONTINUED

    • os.WIFEXITED

    • os.WIFSIGNALED

    • os.WIFSTOPPED

    • os.WSTOPSIG

    • os.WTERMSIG

    • os.write

    • os.writev

  • fcntl:

    • fcntl.fcntl

    • fcntl.ioctl

    • fcntl.flock

    • fcntl.lockf

  • termios:

    • termios.tcgetattr

    • termios.tcsetattr

    • termios.tcsendbreak

    • termios.tcdrain

    • termios.tcflush

    • termios.tcflow

    • termios.tcgetwinsize

    • termios.tcsetwinsize

  • select:

    • select.select

    • select.poll.register

    • select.poll.modify

    • select.poll.unregister

    • select.epoll.register

    • select.epoll.modify

    • select.epoll.unregister

    • select.devpoll.register

    • select.devpoll.modify

    • select.devpoll.unregister

    • select.kevent

  • socket:

    • socket.socket

    • socket.socket.send

    • socket.socket.sendall

    • socket.socket.sendto

    • socket.socket.sendmsg

    • socket.socket.recv_into

    • socket.socket.recvfrom_into

    • socket.socket.recvmsg_into

    • socket.socket.setsockopt

  • mmap:

    • mmap.mmap

    • mmap.mmap.write

  • atexit:

    • atexit.register

def effect

effect(name: str, /) Effect[..., Any]
effect(
*functions: (...) → Any,
) (P → R) → P → R

Create an effect or an effect-set decorator.

  • effect("name") — create a new Effect with 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 (a frozenset of effects) that can be retrieved with effects(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.

P is the parameter types and R is 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
property name: str

The name of the effect, as passed to effect("name").

class Resume

class Resume[R, V]

Bases: Protocol, Generic

Continuation passed to synchronous effect handlers.

R is the type of the value passed to the continuation, V is 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 of k() 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 k zero times (abort), once (one-shot), or multiple times (multi-shot).

class ResumeAsync

class ResumeAsync[R, V]

Bases: Protocol, Generic

Continuation passed to asynchronous effect handlers.

R is the type of the value passed to the continuation, V is the return type of the handled computation.

Same semantics as Resume, but await k(value) is required because the handler function is async 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]

Bases: Protocol, Generic

Protocol for synchronous effect handlers.

Create instances via create_handler. Register effect implementations with on, 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
property effects: frozenset[Effect[..., Any]]

The effects declared for this handler.

property shallow: bool

Whether this is a shallow handler.

on(
effect: Effect[P, R],
) (EffectHandler[P, V, R]) → EffectHandler[P, V, R]

Register a handler function for effect. Returns a decorator.

check(caller: () → V) None

Raise ValueError if any declared effect has no registered handler.

__call__(
caller: () → V,
*,
check: bool = True,
) V

Run caller with the registered effect handlers active.

class AsyncHandler

class AsyncHandler[V]

Bases: Protocol, Generic

Protocol for asynchronous effect handlers.

Create instances via create_async_handler. Handler functions are async def and receive a ResumeAsync continuation:

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
property effects: frozenset[Effect[..., Any]]

The effects declared for this handler.

property shallow: bool

Whether this is a shallow handler.

on(
effect: Effect[P, R],
) (AsyncEffectHandler[P, V, R]) → AsyncEffectHandler[P, V, R]

Register an async handler function for effect. Returns a decorator.

check(
caller: () → V | Coroutine[Any, Any, V],
) None

Raise ValueError if any declared effect has no registered handler.

async __call__(
caller: () → V | Coroutine[Any, Any, V],
*,
check: bool = True,
) V

Run caller with the registered async effect handlers active.

def create_handler

create_handler(
*effects: Effect[..., Any],
shallow: bool = False,
) 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(
*effects: Effect[..., Any],
shallow: bool = False,
) AsyncHandler[Any]

Create an asynchronous handler that handles the given effects.

Handler functions are async def and receive ResumeAsync. 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,
) frozenset[Effect[..., 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,
) frozenset[Effect[..., Any]]

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})