A lazily awaited container for async operations - Schrödinger's box for futures
Project description
QBox
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."
The Problem
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!")
The Solution
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!")
The Quantum Metaphor
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'>
Installation
pip install qbox
Quick Start
import 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"
When Does Code Execute?
Default behavior (start='soon'):
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)
Deferred behavior (start='observed'):
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 |
API Reference
Creating a QBox
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)
Constructor Parameters
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
)
Type Checking with QBox
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.
Accessing the Value
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!")
Lazy Operations (return new QBox)
- 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
Force Evaluation (return concrete values)
- Comparisons:
<,<=,==,!=,>=,> - Type conversions:
bool(),str(),int(),float() - Container operations:
len(),in, iteration - Math functions:
round(),math.floor(),math.ceil() repr()withrepr_observes=True
Side Effects and Exceptions
Since execution is lazy by default with start='observed', side effects and exceptions occur during observation, not during QBox creation.
Side Effects
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 HERE
Exceptions
async 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 exception
Type Mimicry
QBox 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 object
isinstance Support
Use 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 automatically
Or 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 block
Explicit Observation
Use 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.
Error Handling
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)
Thread Safety
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
Requirements
- Python 3.10+
- No runtime dependencies (pure stdlib)
Contributing
Contributions are welcome! Please see CONTRIBUTING.rst for guidelines.
License
Distributed under the MIT License. See LICENSE.rst for details.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file qbox-0.1.1.tar.gz.
File metadata
- Download URL: qbox-0.1.1.tar.gz
- Upload date:
- Size: 125.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e0647976b1ce7dab2e5f1da3873ecc0af1a3e1f30f8706d3882da62804b3d577
|
|
| MD5 |
94ffb15edec58f5e815bf642646b04b2
|
|
| BLAKE2b-256 |
1a464b28e0347c28691ba0b8befeecaaff3f6f6e306aa75a48c0cdcdbf421d92
|
Provenance
The following attestation bundles were made for qbox-0.1.1.tar.gz:
Publisher:
release.yml on gtwohig/qbox
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qbox-0.1.1.tar.gz -
Subject digest:
e0647976b1ce7dab2e5f1da3873ecc0af1a3e1f30f8706d3882da62804b3d577 - Sigstore transparency entry: 833835316
- Sigstore integration time:
-
Permalink:
gtwohig/qbox@753fe06f5685a0bc3333e0ccdf2af9416986082a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/gtwohig
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@753fe06f5685a0bc3333e0ccdf2af9416986082a -
Trigger Event:
push
-
Statement type:
File details
Details for the file qbox-0.1.1-py3-none-any.whl.
File metadata
- Download URL: qbox-0.1.1-py3-none-any.whl
- Upload date:
- Size: 22.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f1f3dbfa33da2911941968a2464e2a5429566d1091a080c1305ea8ff63b56b74
|
|
| MD5 |
da30c0d273365c815fc4f976900558e4
|
|
| BLAKE2b-256 |
f2b59d621faf175c6e1dc6e642824a22fd6b8e94db4bcbc236ad14d007578574
|
Provenance
The following attestation bundles were made for qbox-0.1.1-py3-none-any.whl:
Publisher:
release.yml on gtwohig/qbox
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
qbox-0.1.1-py3-none-any.whl -
Subject digest:
f1f3dbfa33da2911941968a2464e2a5429566d1091a080c1305ea8ff63b56b74 - Sigstore transparency entry: 833835321
- Sigstore integration time:
-
Permalink:
gtwohig/qbox@753fe06f5685a0bc3333e0ccdf2af9416986082a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/gtwohig
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@753fe06f5685a0bc3333e0ccdf2af9416986082a -
Trigger Event:
push
-
Statement type: