A lazily awaited container for async operations.
QBox solves the "colored functions" problem by wrapping async operations in a transparent proxy that defers evaluation until the value is "observed."
Async/await creates a viral pattern where every function that uses async code must also be async:
# The traditional way - async spreads everywhere
async def get_user_data():
user = await fetch_user()
profile = await fetch_profile(user.id)
return await process_data(user, profile)
# Every caller must also be async
async def main():
data = await get_user_data()
if data.score > 100:
print("High score!")QBox lets you write normal synchronous code that transparently handles async operations:
from qbox import QBox
# Wrap async calls in QBox - think of the variable as your data
user = QBox(fetch_user())
profile = QBox(fetch_profile(user.id)) # Lazy - chains without blocking
score = (user.score + profile.bonus) * 2 # Still lazy!
# Only blocks when you actually need the value
if score > 100: # Observation happens here
print("High score!")Like Schrödinger's cat, a QBox value exists in superposition until observed:
import asyncio
import random
from qbox import QBox, observe
class Cat:
pass
class AliveCat(Cat):
def speak(self) -> str:
return "Meow!"
class DeadCat(Cat):
def speak(self) -> str:
return "..."
async def infernal_device() -> Cat:
"""The cat's fate is determined only when observed."""
await asyncio.sleep(0.1) # Quantum uncertainty...
return AliveCat() if random.random() > 0.5 else DeadCat()
# The cat exists in superposition
cat = QBox(infernal_device()) # cat is a QBox[Cat], fate undetermined
# Observation collapses the wave function
observed_cat = observe(cat) # NOW the cat is either alive or dead
print(observed_cat.speak()) # "Meow!" or "..."
# After observation, 'cat' IS the actual Cat object (not a QBox)
print(type(cat)) # <class 'AliveCat'> or <class 'DeadCat'>pip install qboximport asyncio
from qbox import QBox
async def fetch_number() -> int:
await asyncio.sleep(0.1) # Simulate async work
return 42
# Create a QBox - starts the async operation immediately (default)
number = QBox(fetch_number())
# Chain operations - all lazy, no blocking
result = (number + 8) * 2 - 10 # Returns QBox, not int
# Value is computed only when needed
if result > 50: # Blocks here, evaluates entire chain
print(f"Result: {result}") # Prints "Result: 90"async def log_and_fetch():
print("EXECUTING")
return await fetch_data()
data = QBox(log_and_fetch()) # Coroutine submitted NOW, starts running
print("Continuing...") # "EXECUTING" may print during this line
if data: # Blocks until result ready (may already be done)
print(data)data = QBox(log_and_fetch(), start='observed')
# "EXECUTING" has NOT printed - coroutine not submitted yet
print("Doing other work...") # Nothing happening in background
if data: # NOW coroutine submitted, blocks for result
print(data)
# Output:
# Doing other work...
# EXECUTING
# <the data>When to use each:
start= |
Use When |
|---|---|
'soon' (default) |
You'll likely need the value; want parallelism with other sync work |
'observed' |
Might not need the value; building lazy chains; deferring expensive work |
from qbox import QBox
from collections.abc import Mapping
# Basic creation (starts immediately by default)
user = QBox(some_coroutine())
# Deferred execution
config = QBox(load_config(), start='observed')
# With type hint for isinstance support
data = QBox(fetch_dict(), mimic_type=Mapping)
# With custom observation scope
result = QBox(fetch_data(), scope='locals') # 'locals', 'stack', or 'globals'
# With repr that triggers observation
debug_value = QBox(fetch_value(), repr_observes=True)QBox(
awaitable, # The coroutine or awaitable to wrap
mimic_type=None, # Type for isinstance mimicry (e.g., Mapping)
scope='stack', # Reference replacement scope
start='soon', # When to start: 'soon' or 'observed'
repr_observes=False, # Whether repr() triggers observation
cancel_on_delete=True, # Whether to cancel pending work on deletion
)QBox is transparent to static type checkers. For runtime checks, use
QBox._qbox_is_qbox() and prefer ABCs via mimic_type to keep laziness. See
Type Checking Guide and
Static Typing for details.
from qbox import QBox, observe
# Explicit observation (recommended - also replaces references)
value = observe(data)
# Safe observation (works on non-QBox values too)
value = observe(maybe_a_qbox)
# Or use in boolean/comparison context (auto-evaluates)
if data > 10:
print("Large!")- Arithmetic:
+,-,*,/,//,%,** - Bitwise:
&,|,^,<<,>> - Unary:
-data,+data,abs(data),~data - Item access:
data[key],data[start:end] - Attribute access:
data.attribute - Calling:
data(args) repr()with defaultrepr_observes=False
- Comparisons:
<,<=,==,!=,>=,> - Type conversions:
bool(),str(),int(),float() - Container operations:
len(),in, iteration - Math functions:
round(),math.floor(),math.ceil() repr()withrepr_observes=True
Since execution is lazy by default with start='observed', side effects and exceptions occur during observation, not during QBox creation.
async def write_to_database(record):
await db.insert(record) # Side effect!
return record.id
record_id = QBox(write_to_database(user_record), start='observed')
# Database write has NOT happened yet!
# To ensure side effects have occurred:
observe(record_id) # Write happens HEREasync def might_fail():
raise ValueError("Something went wrong")
result = QBox(might_fail())
# No exception raised yet - exceptions surface on observation, not creation
try:
if result: # Exception raised HERE on observation
print(result)
except ValueError:
print("Caught during observation")
# Exceptions are cached - subsequent access re-raises the same exceptionQBox aims to be invisible - when you observe a value, the box disappears:
user = QBox(fetch_user())
print(user.name) # After this, `user` IS the User objectUse ABCs for lazy type checking without forcing evaluation:
from collections.abc import Mapping
from qbox import QBox
data = QBox(fetch_dict(), mimic_type=Mapping)
isinstance(data, Mapping) # True, stays lazy!Or opt-in to full transparency:
from qbox import enable_qbox_isinstance
enable_qbox_isinstance()
isinstance(data, dict) # Works! Forces observation automaticallyOr use the context manager for scoped transparency:
from qbox import qbox_isinstance
with qbox_isinstance():
isinstance(data, dict) # Works within this block
# Original isinstance restored after blockUse the observe() function for explicit control:
from qbox import observe
value = observe(data) # Force evaluation, returns unwrapped value
value = observe(non_qbox) # Safe for non-QBox values (returns unchanged)See Type Checking Guide for details.
Exceptions from the wrapped coroutine are cached and re-raised on access:
async def failing():
raise ValueError("oops")
result = QBox(failing())
try:
observe(result) # Raises ValueError
except ValueError:
pass
observe(result) # Raises same ValueError (cached)QBox is designed for thread-safe access:
- A singleton background event loop runs on a daemon thread
- Value caching uses proper locking
- Multiple threads can safely access the same QBox
- Python 3.10+
- No runtime dependencies (pure stdlib)
Contributions are welcome! Please see CONTRIBUTING.rst for guidelines.
Distributed under the MIT License. See LICENSE.rst for details.