a.k.a. "palette" — reflects the library’s purpose: blending execution models (sync/async) like colors on an artist’s palette, enabling harmony between Python’s concurrency approaches.
⚠️ Experimental Warning: This library is experimental and not production-ready. Use at your own risk.
Inspired by Running async code from sync in Python asyncio by lemon24 and related discussions such as Celery #9058.
A lightweight bridge between synchronous and asynchronous Python code, maintaining a persistent event loop in a background thread. It allows you to call async def
functions directly from regular (sync) code without blocking or complex event loop reentry.
Feature | palitra |
asyncio.run() |
nest_asyncio |
asgiref.AsyncToSync |
xloem/async_to_sync |
miyakogi/syncer |
Haskely/async-sync |
---|---|---|---|---|---|---|---|
Loop Persistence | ✅ Persistent (background thread) | ❌ Per call | ✅ Patches | ✅❌ Per call (if no running event loop in main thread) | ✅ Persistent (background thread) | ❌ Per call | ❌ Per call |
Concurrency | ✅ Full | ❌ One-shot | ✅ | ✅❌ Per call (if no running event loop in main thread) | ✅ | ❌ Single-thread blocking | ❌ Blocking |
No Monkey Patching | ✅ Yes | ✅ Yes | ❌ Required | ✅ Yes | ✅ Yes | ✅ Yes | ✅ Yes |
palitra
: Should be ideal for long-running synchronous apps (e.g. Flask, CLI, Celery) that need to reuse async state across multiple calls. Avoids monkey-patching and global loop interference by running a persistent background event loop thread.asyncio.run()
: Best for short-lived scripts where a one-time coroutine needs to be run synchronously.nest_asyncio
: Patches the global event loop to allow nested async calls. Can work in Jupyter or limited contexts but is fragile for production.asgiref.AsyncToSync
: Meant firstly for Django/ASGI internals. Not for general async wrapping—uses a per-call scheduling model with strict thread management.xloem/async_to_sync
: A lightweight wrapper that synchronously runs a coroutine using loop in background thread.syncer
: Simplesync
/async
wrappers usingrun_until_complete
.async-sync
: Lightweight utility; wraps async-to-sync calls usingloop.run_until_complete()
.
While palitra
and asgiref.sync.AsyncToSync
both enable running async code from sync code, they differ significantly in architecture and use cases:
Aspect | palitra |
asgiref.sync.AsyncToSync |
---|---|---|
Event Loop | Persistent background loop (one per runner) | Reuses loop if possible, else creates temporary |
Execution Model | Dedicated thread runs the event loop | Coroutine scheduled into existing thread/loop |
Loop Lifetime | Explicitly managed or global singleton | Per-call if there is none in main thread (usually short-lived) |
Thread Handling | Coroutines run in background thread, sync caller blocks | Complex dance to preserve thread affinity |
Performance (Multi-call) | Efficient — no repeated loop creation | Overhead from loop setup/teardown |
State Preservation | Loop state preserved across sync calls | State lost unless explicitly preserved |
Shutdown Control | shutdown_global_runner() available |
No manual lifecycle management |
Use palitra
when:
- You need to call async code from sync repeatedly or over a long lifetime (e.g. Flask, CLI tools, Celery).
- You want to maintain event loop state between calls (e.g. reuse aiohttp sessions, connection pools).
- You want a lightweight, self-contained solution with explicit lifecycle control.
Use asgiref.sync.AsyncToSync
when:
- You’re building on top of Django/ASGI and already using
asgiref
. - You need compatibility with Django’s sync/async internals (e.g. views, middleware, ORM).
- Thread affinity is critical (e.g. thread-sensitive DB connections in Django).
Only use in production after careful evaluation in your environment.
- ✅ Runs a persistent asyncio event loop in a background thread
- ✅ Simple, thread-safe API for running coroutines from sync code
- ✅ No monkey patching or global loop overrides
- ✅ Automatic cleanup via
atexit
and weakref to global runner (if used) - ✅ Lightweight: no external dependencies
from flask import Flask, jsonify
import palitra
import aiohttp
import asyncio
app = Flask(__name__)
async def fetch_url(session, url):
async with session.get(url) as response:
return await response.json()
@app.route('/api/comments')
def get_comments():
async def fetch_all():
# this is not ideal, but in real world sometimes it's okay just to make thing work
async with aiohttp.ClientSession() as session:
urls = [
'https://jsonplaceholder.typicode.com/comments/1',
'https://jsonplaceholder.typicode.com/comments/2',
'https://jsonplaceholder.typicode.com/comments/3',
]
return await asyncio.gather(*[fetch_url(session, url) for url in urls])
comments = palitra.run(fetch_all())
return jsonify(comments)
if __name__ == '__main__':
app.run()
import palitra
from celery import Celery
import asyncio
import time
celery_app = Celery('tasks', broker='pyamqp://guest@localhost//')
async def async_processing(data: str) -> dict:
await asyncio.sleep(0.5) # simulate async I/O
return {"input": data, "processed": True, "timestamp": time.time()}
@celery_app.task(name="process_async")
def sync_celery_wrapper(data: str):
return palitra.run(async_processing(data))
These top-level functions create and reuse a singleton EventLoopThreadRunner
under the hood.
Run a coroutine from synchronous code.
- Creates a shared event loop thread on first use.
- Internally calls
EventLoopThreadRunner.run(...)
.
from palitra import run
result = run(my_async_func())
gather(*coros: Coroutine, return_exceptions: bool = False, timeout: float | None = None) -> list[Any]
Run multiple coroutines concurrently from sync code.
- Like
asyncio.gather(...)
, but callable from sync. - Uses the global shared runner.
from palitra import gather
results = gather(coro1(), coro2(), coro3())
Check whether the global event loop runner currently exists and is alive.
Explicitly shut down the global event loop runner and release resources.
- After calling this, subsequent calls to
run
orgather
will create a new runner instance. - Useful for cleanup or to reset the runner state.
Use the class directly if you need more control or isolation (e.g., separate event loop threads).
Run a coroutine on this runner’s background loop.
-
Returns: The coroutine result
-
Raises:
TypeError
if input is not a coroutineasyncio.TimeoutError
if timeout expires- Exceptions from the coroutine itself
gather(self, *coros: Coroutine, return_exceptions: bool = False, timeout: float | None = None) -> list[Any]
Run multiple coroutines concurrently via this runner.
- Returns list of results or exceptions (if
return_exceptions=True
). - Raises exceptions same as
run
.
Get the event loop managed by this runner.
Useful if you want to schedule coroutines directly.
Stop the event loop and background thread.
- Idempotent — safe to call multiple times.
- Cleans up resources and stops the thread.
- Starts a background thread that runs an
asyncio
event loop. - Internally uses
asyncio.run_coroutine_threadsafe(...)
for thread-safe execution. - Ensures cleanup via
atexit
and context manager support. - If used via high-level api (
run
,gather
), global runner object created using weakref.
Pull requests are welcome! Please:
- Document known issues or caveats
- Include test coverage for new features
- Keep the code as simple and minimal as possible
- Prefer clarity over cleverness
Things that need more work:
- Proper stress testing
- Verifying thread safety in edge cases
- Detecting and eliminating memory leaks
- Ensuring reliable shutdown under all conditions
BSD-3-Clause