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
Release files for cancelscope 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| cancelscope-0.1.0.tar.gz | 17.9 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| cancelscope-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 29.7 kB
Release files / cancelscope-0.1.0.tar.gz
| Download URL | cancelscope-0.1.0.tar.gz |
|---|---|
| Size | 17.9 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
7bb2eeb209f592c1505c4d3025e7f9a023b40227536e23b9ab4334534c333d0d
|
|
BLAKE2b-256 checksum How to use checksums |
da53eddb5ecbcd363f39cd0824e063f047f0c8384c778931ec834d57bc993476
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.4
|
Release files / cancelscope-0.1.0-py3-none-any.whl
| Download URL | cancelscope-0.1.0-py3-none-any.whl |
|---|---|
| Size | 11.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
3c8b9c0fd38148d311b1d5bb20872f546c27aeb05267f04142516cf04a83b895
|
|
BLAKE2b-256 checksum How to use checksums |
479bd5b577feb015e2662b2de78b679e7c1e46bbc4ad90f5f551bf46a4ff021a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/6.2.0 CPython/3.14.4
|