cancelscope
Cooperative cancellation, timeouts, and cancel scopes for synchronous Python.
Python has no good story for cancelling synchronous work: KeyboardInterrupt is a
signal-handler hack, threads can't be killed, and trio's lovely cancel scopes are
async-only. cancelscope brings trio-style cancellation semantics — scopes,
checkpoints, deadlines, shielding — to plain blocking code and threads.
- Cooperative: nothing is interrupted preemptively. Code observes cancellation
at explicit
checkpoint()calls (or insidesleep()/guard()), so you are never left with a lock half-held or a file half-written. - Thread-safe: cancel a scope from any thread; workers observe it at their
next checkpoint.
sleep()wakes immediately. - Real deadlines: timeouts are enforced by a shared monitor thread, so a
deadline fires at the deadline — waking sleepers and running
on_cancelcallbacks — not merely at the next checkpoint. - Zero dependencies, fully typed, works on Python 3.9+.
Install
pip install cancelscope
Quickstart
import cancelscope as cs
# A timeout around blocking work
with cs.move_on_after(5) as scope:
for row in cs.guard(rows): # checkpoint before each item
process(row)
if scope.cancelled_caught:
print("timed out, partial results kept")
# Or raise on timeout
with cs.fail_after(5): # raises TimeoutError if too slow
crunch()
Cancel from another thread (the classic "Stop" button):
scope = cs.CancelScope()
def worker():
with scope:
while True:
cs.checkpoint() # raises cs.Cancelled once cancelled
step()
threading.Thread(target=worker).start()
...
scope.cancel("user clicked stop") # from any thread
Propagate a scope into a thread pool:
with cs.CancelScope() as scope:
# NB: bind() captures the scope that is current at the moment you
# call it — call it *inside* the `with` block, or it captures nothing
# and the worker silently becomes uncancellable.
futures = [pool.submit(cs.bind(handle), job) for job in jobs]
...
scope.cancel() # every worker's next checkpoint raises
Unblock third-party blocking calls via on_cancel:
with cs.fail_after(10) as scope:
sock = socket.create_connection(addr)
# shutdown() reliably wakes a thread blocked in recv() on all major
# platforms (close() does not, and races on the file descriptor).
scope.on_cancel(lambda s: sock.shutdown(socket.SHUT_RDWR))
data = sock.recv(65536) # returns b'' once shut down
cs.checkpoint() # surfaces the timeout as TimeoutError
The callback only unblocks the call — after shutdown(), recv()
returns b'' (or raises OSError on some paths). The checkpoint()
after it is what turns the cancellation into the TimeoutError that
fail_after promises; without it the block would end as a phantom EOF.
Semantics (the trio model, sync)
CancelScope(timeout=..., deadline=..., shield=..., name=...)is a context manager. Cancelling a scope cancels everything nested inside it.checkpoint()raisesCancelled— aBaseException, so strayexcept Exceptionblocks can't eat it — bound to the outermost cancelled scope. That scope's__exit__catches it and setscancelled_caught; intermediate scopes let it pass through.shield=Truedetaches a scope from its parent: cleanup code inside a shield keeps running even while everything around it is being cancelled.- Deadlines are absolute
time.monotonic()times;scope.deadlineis readable and writable while the scope is active (extend or tighten at will). - If the body finishes before anyone checkpoints, cancellation simply has no effect — cooperative means never yanking the rug.
API
| Name | What it does |
|---|---|
CancelScope(timeout=, deadline=, shield=, name=) |
the scope context manager |
scope.cancel(reason=None) |
cancel from any thread; idempotent |
scope.cancelled / scope.cancel_called / scope.cancelled_caught |
state |
scope.deadline / scope.remaining() |
inspect or move the deadline |
scope.on_cancel(fn) |
callback on cancellation; returns an unregister function |
checkpoint() |
raise Cancelled here if cancelled (cheap when not) |
sleep(seconds) |
time.sleep that wakes instantly on cancellation |
guard(iterable) |
checkpoint before each item |
bind(fn) |
carry the current scope into another thread |
move_on_after(seconds) |
timeout scope that exits silently |
fail_after(seconds) |
timeout scope that raises TimeoutError |
current_scope() / is_cancelled() |
introspection |
Caveats
- This is cooperative cancellation: code that never checkpoints (a C
extension crunching for minutes, a blocking socket read) is not interrupted.
Use
on_cancelto unblock such calls out-of-band — and note the callback only unblocks the call; checkpoint afterwards to raise. Prefer wake-up mechanisms likesocket.shutdown()overclose(), which does not wake a blockedrecv()on Linux and races on the descriptor. - The first deadline lazily starts one daemon monitor thread for the whole process; it stays alive thereafter.
on_cancelcallbacks run in whichever thread triggers the cancellation (the monitor thread, for deadlines): keep them short and thread-safe.
License
MIT
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 cancelscope-0.1.0.tar.gz.
File metadata
- Download URL: cancelscope-0.1.0.tar.gz
- Upload date:
- Size: 17.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7bb2eeb209f592c1505c4d3025e7f9a023b40227536e23b9ab4334534c333d0d
|
|
| MD5 |
05f762b85020c472e9184ed58ffe0202
|
|
| BLAKE2b-256 |
da53eddb5ecbcd363f39cd0824e063f047f0c8384c778931ec834d57bc993476
|
File details
Details for the file cancelscope-0.1.0-py3-none-any.whl.
File metadata
- Download URL: cancelscope-0.1.0-py3-none-any.whl
- Upload date:
- Size: 11.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.4
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
3c8b9c0fd38148d311b1d5bb20872f546c27aeb05267f04142516cf04a83b895
|
|
| MD5 |
69f74b5dc9ba4b2036f65a644fd6a2a1
|
|
| BLAKE2b-256 |
479bd5b577feb015e2662b2de78b679e7c1e46bbc4ad90f5f551bf46a4ff021a
|