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
Metadata
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 | Provenance | Actions |
|---|---|---|---|---|
| cancelscope-0.1.0.tar.gz | 17.9 kB | Without provenance | Details for cancelscope-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| cancelscope-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for cancelscope-0.1.0-py3-none-any.whl |
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
|
Security for cancelscope 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.