Comparison with Alternative Solutions
Date: 2025-11-10 Status: Analysis of existing solutions
Overview
This document compares SmartAsync with existing libraries and approaches for handling sync/async interoperability in Python.
Existing Solutions
1. asyncio.to_thread() (Python Stdlib 3.9+)
What it does: Runs synchronous functions in a thread pool from async context.
import asyncio
async def my_async_function():
result = await asyncio.to_thread(blocking_sync_call)
return result
Pros:
✅ Part of Python standard library (no dependencies)
✅ Well-tested and maintained
✅ Simple API
Cons:
❌ Only one direction: sync → async context
❌ Not automatic - must call explicitly
❌ Doesn’t handle async → sync context
❌ No auto-detection of context
Use case: When you know you’re in async context and need to call blocking code.
2. asgiref.sync (Django Framework)
What it does: Provides sync_to_async() and async_to_sync() decorators for Django.
from asgiref.sync import sync_to_async, async_to_sync
@sync_to_async
def sync_database_query():
return MyModel.objects.get(id=1)
@async_to_sync
async def async_api_call():
async with httpx.AsyncClient() as client:
return await client.get(url)
Pros:
✅ Bidirectional (both sync→async and async→sync)
✅ Battle-tested in Django ecosystem
✅ Supports thread-sensitive operations
✅ Configuration options (thread_sensitive, executor)
Cons:
❌ Requires explicit decorator choice (two different decorators)
❌ No auto-detection of context
❌ Additional dependency
❌ Heavier (designed for framework integration)
❌ Must decide direction at decoration time
Use case: Django applications mixing sync and async views/middleware.
GitHub: https://github.com/django/asgiref
3. anyio (Trio-compatible abstraction)
What it does: Abstraction layer over asyncio and Trio.
import anyio
async def my_async():
result = await anyio.to_thread.run_sync(blocking_func)
return result
Pros:
✅ Works with both asyncio and Trio
✅ Rich ecosystem of utilities
✅ Thread and process pools
Cons:
❌ Only sync → async direction
❌ No auto-detection
❌ Heavy dependency (abstraction layer)
❌ More complex API (by design - more features)
❌ Overkill if you only need asyncio
Use case: Applications that need to support both asyncio and Trio, or need the full anyio feature set.
GitHub: https://github.com/agronholm/anyio
5. nest_asyncio
What it does: Patches asyncio to allow nested event loops.
import nest_asyncio
nest_asyncio.apply()
# Now you can call asyncio.run() from within an async context
Pros:
✅ Allows nested event loops
✅ Can solve some edge cases
Cons:
❌ Monkey-patches asyncio internals
❌ Not recommended for production
❌ Can cause subtle bugs
❌ Compatibility issues with libraries
❌ Hacky approach
Use case: Jupyter notebooks, interactive environments. Not recommended for production.
GitHub: https://github.com/erdewit/nest_asyncio
Feature Comparison Matrix
Feature |
SmartAsync |
asgiref |
anyio |
asyncer |
stdlib (to_thread) |
|---|---|---|---|---|---|
Auto-detection |
✅ Yes |
❌ No |
❌ No |
❌ No |
❌ No |
Bidirectional |
✅ Yes |
✅ Yes |
❌ No |
✅ Yes |
❌ No |
Single decorator |
✅ Yes |
❌ No (2 decorators) |
❌ No |
❌ No (2 functions) |
N/A |
Zero dependencies |
✅ Yes |
❌ No |
❌ No |
❌ No |
✅ Yes |
Async → Sync |
✅ asyncio.run |
✅ async_to_sync |
❌ No |
✅ syncify |
✅ asyncio.run |
Sync → Async |
✅ to_thread |
✅ sync_to_async |
✅ to_thread |
✅ asyncify |
✅ to_thread |
Pattern matching |
✅ Yes (Python 3.10+) |
❌ No |
❌ No |
❌ No |
N/A |
Asymmetric cache |
✅ Yes |
❌ No |
❌ No |
❌ No |
N/A |
Thread pool config |
❌ Uses default |
✅ Configurable |
✅ Configurable |
❌ Uses default |
❌ Uses default |
Trio support |
❌ asyncio only |
❌ asyncio only |
✅ Yes |
❌ asyncio only |
❌ asyncio only |
Python version |
3.10+ (match) |
3.8+ |
3.8+ |
3.8+ |
3.9+ (to_thread) |
What Makes SmartAsync Unique
1. Automatic Context Detection
SmartAsync - Same code works everywhere:
@smartasync
async def fetch_data(url: str):
async with httpx.AsyncClient() as client:
return await client.get(url).json()
# Sync context - auto-detected
data = fetch_data(url) # No await needed!
# Async context - auto-detected
data = await fetch_data(url) # Normal await
Others - Must decide direction upfront:
# asgiref - two decorators
@sync_to_async
def fetch_sync(url: str): ...
@async_to_sync
async def fetch_async(url: str): ...
# asyncer - explicit wrapper
async_fetch = asyncify(sync_fetch)
sync_fetch = syncify(async_fetch)
2. Single Decorator for Both Directions
SmartAsync:
@smartasync
async def async_method(): ...
@smartasync
def sync_method(): ...
# Both work in both contexts!
Others:
# asgiref - different decorators
@sync_to_async
def method1(): ...
@async_to_sync
async def method2(): ...
# asyncer - different wrappers
async_version = asyncify(sync_method)
sync_version = syncify(async_method)
3. Zero Configuration
SmartAsync:
@smartasync
async def method(): ...
# Done! Works everywhere.
asgiref:
@sync_to_async(thread_sensitive=False, executor=custom_executor)
def method(): ...
# Must configure options
4. Pattern Matching Clarity
SmartAsync uses Python 3.10+ pattern matching for clear dispatch:
match (async_context, async_method):
case (False, True): # Sync context + Async method
return asyncio.run(coro)
case (False, False): # Sync context + Sync method
return method(self, *args, **kwargs)
case (True, True): # Async context + Async method
return coro
case (True, False): # Async context + Sync method
return asyncio.to_thread(method, self, *args, **kwargs)
Each case is explicitly documented and clear.
Unique Use Cases
1. Testing Simplification
With SmartAsync:
class Service:
@smartasync
async def process(self, data): ...
# Sync test (no event loop setup!)
def test_process():
svc = Service()
result = svc.process(data) # Just works!
assert result["status"] == "ok"
# Async test (natural)
async def test_process_async():
svc = Service()
result = await svc.process(data)
assert result["status"] == "ok"
With other libraries:
# Must create separate sync/async versions
# Or setup event loop in sync tests
import asyncio
def test_process():
svc = Service()
result = asyncio.run(svc.process(data)) # Manual asyncio.run
assert result["status"] == "ok"
2. Unified Library API
With SmartAsync - One implementation for all users:
class UnifiedAPI:
"""Library with single implementation."""
@smartasync
async def fetch(self, url: str):
async with httpx.AsyncClient() as client:
return await client.get(url).json()
# Sync user
api = UnifiedAPI()
data = api.fetch(url) # No await
# Async user
api = UnifiedAPI()
data = await api.fetch(url) # With await
With other libraries - Must maintain two versions or force users to one approach.
3. Gradual Migration
With SmartAsync:
class LegacySystem:
@smartasync
def old_sync_method(self):
"""Legacy blocking code."""
return db.query_sync()
@smartasync
async def new_async_method(self):
"""Modern async code."""
return await db.query_async()
# Both work in both contexts - no rewrite needed!
Migration can happen incrementally without breaking changes.
When NOT to Use SmartAsync
1. Need Fine-Grained Control
If you need custom thread pools, specific executors, or thread-sensitive operations:
✅ Use asgiref - Provides configuration options
✅ Use anyio - Full control over execution
2. Trio Support Required
If your application uses Trio instead of asyncio:
✅ Use anyio - Works with both asyncio and Trio
3. Nested Event Loops
If you need nested event loops (e.g., Jupyter notebooks):
⚠️ Use nest_asyncio (with caution!)
Better: Restructure code to avoid nesting
4. Python < 3.10
SmartAsync requires Python 3.10+ for pattern matching:
✅ Use asgiref or asyncer (Python 3.8+)
✅ Use stdlib
asyncio.to_thread()(Python 3.9+)
5. Framework Integration
If you’re building Django apps:
✅ Use asgiref - Designed for Django integration
Performance Comparison
Overhead Analysis
Operation |
SmartAsync |
asgiref |
anyio |
asyncer |
stdlib |
|---|---|---|---|---|---|
Async call (cached) |
~1.3μs |
~2μs |
~3μs |
~2μs |
N/A |
Async call (first) |
~2.3μs |
~3μs |
~4μs |
~3μs |
N/A |
Sync → Async |
~102μs |
~110μs |
~105μs |
~100μs |
~100μs |
Async → Sync (thread) |
~50-100μs |
~60-120μs |
~55-110μs |
~50-100μs |
~50-100μs |
Conclusion: All solutions have similar performance - overhead dominated by asyncio.run() and thread creation, not the wrapper logic.
Recommendations
Use SmartAsync When:
✅ You want automatic context detection
✅ You need one decorator for both directions
✅ You want zero configuration
✅ You prefer simple, clean code
✅ You’re using Python 3.10+
✅ You want zero dependencies (stdlib only)
Use asgiref When:
✅ Building Django applications
✅ Need thread-sensitive operations
✅ Need custom executors
✅ Want production-tested framework integration
Use anyio When:
✅ Need Trio support
✅ Building library that works with multiple async frameworks
✅ Need the full anyio feature set (sockets, processes, etc.)
Use asyncer When:
✅ Want simple wrappers without auto-detection
✅ Prefer explicit over implicit
✅ Like the FastAPI ecosystem style
Use stdlib (asyncio.to_thread) When:
✅ Only need sync → async direction
✅ Want absolute minimal dependencies
✅ Have simple use case (no need for wrapper)
Summary
SmartAsync fills a unique niche: No other library offers automatic bidirectional context detection with a single decorator.
Library |
Philosophy |
Best For |
|---|---|---|
SmartAsync |
“Do What I Mean” - Auto-detection |
Unified APIs, testing, gradual migration |
asgiref |
“Framework Integration” - Configuration |
Django apps, production frameworks |
anyio |
“Cross-Framework” - Abstraction |
Trio support, cross-framework libs |
asyncer |
“Explicit Wrappers” - Simplicity |
FastAPI-style explicit conversions |
stdlib |
“Minimal” - One direction only |
Simple async-to-sync threading |
The Verdict: If you want the simplest possible way to make code work in both sync and async contexts without thinking about it, SmartAsync is the right choice.
References
asgiref: https://github.com/django/asgiref
anyio: https://github.com/agronholm/anyio
asyncer: https://github.com/tiangolo/asyncer
nest_asyncio: https://github.com/erdewit/nest_asyncio
asyncio.to_thread: https://docs.python.org/3/library/asyncio-task.html#asyncio.to_thread
Last Updated: 2025-11-10 Maintainer: SmartAsync Team