asyncio.to_thread runs a synchronous callable in a worker thread and returns an awaitable for its result.
Python asyncio.to_thread: carry context into blocking work
Operation contract
The request label is stored in a ContextVar before the thread call. The blocking function reads that label and returns a bounded receipt string. The caller awaits completion before printing, so output order is fixed. This is appropriate for a small blocking operation that cannot be made asynchronous without replacing its API; it does not turn CPU-heavy Python work into an automatic throughput win.
Failure and ownership boundary
Cancelling the awaiting coroutine does not forcibly terminate a thread that has already begun work. If the blocking function performs an external write, the application needs a stop or idempotency rule independent of task cancellation. Python ContextVar: task-local labels without sharing one global binding, Python thread pools: collect results and observe worker failures and Python asyncio shield: caller cancellation does not transfer task ownership cover those limits.
Working program
import asyncio
from contextvars import ContextVar
request_label = ContextVar("request_label", default="unset")
def blocking_receipt_lookup(identifier):
return request_label.get() + ":" + identifier
async def main():
token = request_label.set("review-7")
try:
print(await asyncio.to_thread(blocking_receipt_lookup, "R41"))
finally:
request_label.reset(token)
asyncio.run(main())Output
review-7:R41Costs and limits
Submitting work and crossing thread boundaries add scheduling cost. The returned string is tiny; a real blocking call needs timeout, cancellation and capacity limits for its worker pool. Context propagation copies context references, not deep snapshots of mutable values.
Common Mistakes
- A cancelled await does not guarantee the worker stopped.
- ContextVar propagation does not make shared mutable payloads thread-safe.
Connected lessons
Python ContextVar: task-local labels without sharing one global binding, Python thread pools: collect results and observe worker failures, Python asyncio shield: caller cancellation does not transfer task ownership.
