Skip to main content

interruptible

Guaranteed, transparent Ctrl+C for Python programs.

interruptible makes Ctrl+C work promptly even when your program is blocked inside a native C/Rust extension that never returns to the Python bytecode evaluator -- for example an HTTP request through a library that does not poll for interrupts, a database driver, or a long-running computation in NumPy.

import sys
import interruptible

def main():
    # Anything at all here, including blocking C calls.
    ...
    return 0

if __name__ == "__main__":
    sys.exit(interruptible.run(main))

Press Ctrl+C and the program stops, now.

Why this is needed

Python delivers signals by setting a flag that the interpreter checks between bytecodes. If your code is blocked inside a C extension, the interpreter is never reached, so KeyboardInterrupt is not raised until the call returns -- which may be never. On Windows it is worse: a blocking call is not interrupted at all.

There is no way to fix this from inside the blocked process. So interruptible runs your main in a child process. The parent does nothing but wait, so it is always able to react to Ctrl+C immediately; on interrupt it forwards the signal to the child and, if the child is one of the ill-behaved ones, forcibly terminates the child's entire process tree.

Transparency

The design goal is that a wrapped program is indistinguishable from an unwrapped one, apart from interrupts always working:

Behaviour interruptible result
main() returns 0 exit code 0
main() returns N exit code N
main() raises traceback on stderr, exit code 1
sys.exit(N) exit code N
Ctrl+C, child exits on its own exit code 130 (128 + SIGINT)
SIGTERM exit code 143 (128 + SIGTERM)
Ctrl+C, child ignores it child tree killed after kill_timeout; exit code 130
timeout= expires child signalled, exit code 127
stdout/stderr forwarded live to the parent's streams

The signal the user sent determines the exit code, so Ctrl+C always reports 130, exactly as an unwrapped program would.

Install

pip install interruptible

No runtime dependencies. Python 3.10+.

Usage

Function

sys.exit(interruptible.run(main, timeout=60, kill_timeout=5.0))

Decorator

@interruptible.interruptible(timeout=300)
def main():
    ...

if __name__ == "__main__":
    sys.exit(main())

API

def run(
    target: Callable[..., int | None],
    *args,
    timeout: float | None = None,
    kill_timeout: float = 5.0,
    passthrough_signals: tuple[int, ...] = (signal.SIGINT, signal.SIGTERM),
    inherit_environ: bool = True,
    **kwargs,
) -> int: ...
  • target -- the function to run. On spawn platforms (Windows, macOS) it must be picklable, which means a module-level function in a real file; see the FAQ.
  • timeout -- maximum runtime in seconds (None = unlimited). On expiry the child is signalled and 127 is returned.
  • kill_timeout -- seconds to wait for a graceful exit after forwarding a signal before force-killing the child's process tree.
  • passthrough_signals -- signals received by the parent that are forwarded to the child.
  • inherit_environ -- whether the child inherits the environment.

FAQ

Why a subprocess? Because there is no other way to be responsive while the main thread is stuck inside native code. A thread or an asyncio task cannot help: they cannot preempt a blocked C call either.

Doesn't signal.set_wakeup_fd / faulthandler solve this? Those change how the interpreter notices signals; they do not make a blocked native call return.

What about child processes I spawn myself? On POSIX the child runs in its own process group, and the whole group is signalled during cleanup. On Windows the child is attached to a Job Object configured to kill all member processes when the job closes, so grandchildren die too.

Nested use. If run() is called from inside a child (detected via the INTERRUPTIBLE_CHILD environment variable), the target runs inline instead of spawning another process.

Why did I get Can't pickle local object? On Windows, and on macOS since Python 3.8, the child is started with spawn, which sends the target to the new interpreter by pickling it by module and name. Two things therefore cannot be used as a target:

  • a nested function, lambda, closure, or bound method defined inside another function;
  • a function defined in a python -c string or an interactive session, because its __main__ has no importable name.

Both fail with AttributeError once the child tries to unpickle the target. Use a module-level function in a real file:

# do this
def main():
    ...

if __name__ == '__main__':
    sys.exit(interruptible.run(main))


# not this -- main cannot be pickled
if __name__ == '__main__':
    def main():
        ...

    sys.exit(interruptible.run(main))

Platform notes

run() uses the platform's default multiprocessing start method. That is spawn on Windows and macOS and fork on Linux, and the difference matters: spawn requires a picklable (module-level) target, while fork accepts anything. Forcing fork on Linux was deliberately rejected -- it is unsafe in a process with threads and it would hide this requirement from anyone developing on Linux. To check your code under the stricter spawn rules on Linux, set the start method before calling run():

import multiprocessing

multiprocessing.set_start_method('spawn')
  • Spawn platforms (Windows, macOS) -- the target must be picklable.
  • POSIX -- the parent forwards the exact signal it received (so the child sees a normal KeyboardInterrupt), then SIGKILLs the process group if the child has not exited within kill_timeout.
  • Windows -- a child started by multiprocessing shares the parent's console process group, so the console delivers Ctrl+C to it directly; no explicit forwarding is needed. The Job Object guarantees the whole tree is cleaned up if the child ignores it.

License

MIT

Release files for interruptible 1.0.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for interruptible 1.0.0
File Size Uploaded
interruptible-1.0.0.tar.gz 22.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for interruptible 1.0.0
File Interpreter ABI Platform
interruptible-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 38.4 kB

Release files / interruptible-1.0.0.tar.gz

Download URL interruptible-1.0.0.tar.gz
Size 22.6 kB
Tags Source
SHA-256 checksum
How to use checksums
e93f1f04c2a3dc4c1220020d2d45040becd52e4fda68e7e4227eff64ff1f2837
BLAKE2b-256 checksum
How to use checksums
d4ec323a4acca707c9e09feb071186117ccebdb3da0616b1ac76081b4b9c3d73
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Release files / interruptible-1.0.0-py3-none-any.whl

Download URL interruptible-1.0.0-py3-none-any.whl
Size 15.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2f706b115fc7431f18829c96f97402c9cf6cf70ec97704c9a18763bf6faffce6
BLAKE2b-256 checksum
How to use checksums
ba65906f5a4c41a7fcbb6cb7993a691374266f4367dbeea1f38eb49f1b3c22b6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 21, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page