How does that solve the problem of interrupting critical scopes of code, like finally? How does a 3rd party library go about coping with this problem without language support?
That may actually be a decent Idea if it weren’t for the fact that it seems python will always raise the exceptions in the main thread even if sent to a specific one if I understand the documentation of signal.pthread_kill correctly signal — Set handlers for asynchronous events — Python 3.14.2 documentation
…
However, if the target thread is executing the Python interpreter, the Python signal handlers will be executed by the main thread of the main interpreter. Therefore, the only point of sending a signal to a particular Python thread would be to force a running system call to fail withInterruptedError.
Could still maybe be done with a C extension but it seems hard to accomplish with just python.
I’d absolutely love it if sending a KeyboardInterrupt to the main thread would automatically send a ThreadInterrupt(KeyboardInterrupt) to all the threads by default
Most code works fine with KeyboardInterrupt, even despite the developers not thinking about KeyboardInterrupt, precisely because of features like with blocks. I expect the same would apply to almost all threads.
I do see the issue of code where a ThreadInterrupt would be disastrous not having any guard rails build in. But such rare cases can hopefully be dealt with using the procedures of deprecation, like we do with other features that aren’t 100% backwards compatible.
I think the point in the original post is precisely the opposite of this, @ryanhiebert correct me if I’m wrong.
Most code is not resilient to KeyboardInterrupt because that exception may be raised inside context manager methods, regardless of threads.
Personally, my priority is for Ctrl-C to interrupt a long-running process. So I’d like KeyboardInterrupt to cancel long-running threads. That’s the key requirement for me. I’m not massively concerned about robustness, as I’m normally expecting the process to get aborted anyway - I’d like there to be a good chance that cleanup happens so any outputs are in a consistent state, but a small chance that hitting a critical piece of code results in some cleanup not happening properly doesn’t bother me that much.
It’s pretty easy to cause weird results (even in single-threaded code) if you repeatedly hit Ctrl-C rapidly, so I’m fine with making it more likely that a single Ctrl-C stops the application, to encourage people not to resort to extreme measures like hitting Ctrl-C repeatedly or going for Ctrl-Break or kill.
I agree that most code doesn’t handle KeyboardInterrupt 100% robustly. But generally, I feel that it’s good enough, and extending support to cover cancelling threads, even if only to a similarly “good enough” level, would be useful.
People who want robust behaviour can (and likely should) disable the automatic generation of KeyboardInterrupt, and write their own Ctrl-C handler.
Thank you for your input Paul. I do feel like it’s a very biased comment based on your own needs. Which is totally fine.
What I have in mind coming to this problem is the perspective of a library author. If we had ThreadInterrupt without any other language change and I wanted to use that in my library, I’d have to tell my users that “your threads might get interrupted by library internals and your context managers may not get to clean up your resources.”
And if I had to write that, I would just not do it. I would avoid using ThreadInterrupt completely, out of fear that my users would, rightly, start looking for alternatives. Which possibly results in a new feature used by no one.
I think these two things are a bit contradictory, though:
If the critical piece of clean up code that doesn’t happen properly is a lock.release(), then you may have caused a deadlock when hitting Ctrl-C, and some of your threads would never exit. (And you would have to kill the process through the OS.)
Note that you caused the deadlock by hitting Ctrl-C. A provably deadlock-free program, without considering the existence of KeyboardInterrupt, may still have a deadlock due to interrupts. Which is bad no metter how you look at it.
“Biased” is a bit strong. Yes, it’s my use case so of course it’s describing what I need. I’m not suggesting that’s all that needs to be addressed, just that in looking at the needs of people who do want strong reliability guarantees, let’s not forget the people who have less strict requirements. What I’m concerned about is that we don’t decide to do nothing because we can’t provide a fully robust solution - even thread cancellation with some rough edges would be of real benefit to at least some people.
And that’s where I think you’re wrong. If the language had a way to cancel threads that cause ThreadInterrupt to be raised in threads that got cancelled, I’d be more than happy if the description of ThreadInterrupt was that it got raised in the thread, and it was up to the code in the thread to catch and respond to it if they needed to do cleanup. That doesn’t mean that I’d demand libraries all do that - to be blunt, most libraries don’t do that for KeyboardInterrupt today.
So I’d be perfectly happy for your library to simply not do any special handling of ThreadInterrupt. And if your library wanted to cancel threads, I’d be perfectly happy for you to say “this library cancels threads in such-and-such conditions, it’s the library user’s responsibility to ensure that threads handle cleanup on ThreadInterrupt if needed”.
If you aren’t willing to use thread cancellation unless it provides 100% cleanup guarantees, then IMO you are ignoring the use cases of people who want cancellation, but don’t need 100% guarantees.
Yeah. So what? That’s precisely what I mean by “I don’t need 100% reliability”. If Ctrl-C cancels my long-running threaded application 99% of the time, and in 1% of cases it causes a deadlock which needs me to bring out the “big guns” and kill via Ctrl-Break, that’s still 99% better than the current situation, where Ctrl-C leaves the code hanging while it waits for the thread pool to complete running, which it will never do because the driver code got interrupted.
To be completely clear, my use case is multi-threaded long-running command line applications. Think for example of a command that downloads thousands of packages from PyPI in parallel, to do some analysis[1]. If I realise I gave the wrong command options, I want to hit Ctrl-C and have the command stop, so I can change the options and rerun it. In that situation, I don’t care about correctness, I just want the program to stop. I could use Ctrl-Break, except that my current keyboard doesn’t have a “Break” key. I could use Stop-Process, but then I’d need to find the process ID and run the Stop-Process command in a new terminal. Why should I have to do this when Ctrl-C works just fine for every other application?
This is a real example of code that I wrote - and I had to write a bunch of code, including my own
SIGINThandler, to get that “99% reliable” result, which IMO should be the default. ↩︎
The problem with this is the same as with keyboard interrupt. There are critical sections where raising an exception will break things, and this is why apps that care about this disable it entirely (by installing their own signal handler instead).
I’m a firm -1 on throwing exceptions into threads, this being functionality that people are expected to just catch when we already know this approach doesnt work would probably signal a larger transition away from python for me.
I think this is where we fundamentally disagree - you think keyboard interrupt has a problem, I think that it’s perfectly fine[1].
We’re clearly not going to agree, and I’ve made the point I wanted to make, so I’ll leave it at that.
Edit: Whoops, sorry - I didn’t realise this post was by someone different. I think my reply stands, although the “we’re clearly not going to agree” doesn’t apply any more.
Not perfect, but an extremely effective solution in 99% of situations ↩︎
I do think KeyboardInterrupt has a fundamental problem, though currently, users have the tools to appropriately handle that fundamental problem if it matters to them.
Python isn’t just used for simple scripting. If it was, and that was explicitly the target audience, I’d have fewer issues with this proposal.
As it stands, the signal documentation already says that only the main thread will receive KeyboardInterrupt by default, as well as that all signal handling should occur in the main thread. While a new Exception would technically not violate that existing documentation, I don’t agree that it is reasonable to use that line of reasoning to argue that this wouldn’t be incredibly disruptive.
This is long-standing documented behavior that people have a reason to rely on being the case, and that changing it will break existing users who relied on that documented behavior as an intentional way to shield from arbitrary cancellation exceptions if we start automatically throwing exceptions into threads they created, because some other developers didn’t even consider cancellation.
I’d be more okay with more abstractions that have to be opted into that give a clear indication to the interpreter “hey, this is disposable, throw an exception into it when closing the interpreter”, than things that attach this mechanism to all threads in general.
@dpdani Thinking more on how we can do interrupts safely, I note that the suggested solution to delay interrupts while __enter__ is being called by a with block could break down under the current pattern of ExitStack.
For example, here’s this example:
important_value: int | None = get_important_value()
with ExitStack() as stack:
if important_value is not None:
stack.enter_context(important_context_manager(important_value))
do_function_that_may_want_important_context_manager_context()
Setting aside that this particular example is probably a bad one (the code I’m writing that started out that way is being written a better way), note that the call to stack.enter_context is not protected by a with block.
Thinking over how this pattern could be implemented, I suspect that enter_context could internally use a with block with a custom wrapper context manager. Now I’m curious if that’s what it already does, since I’ve just assumed that it just called __enter__ and __exit__ directly.
My 2c on interrupt in threads automatically: I’m not sure that’s a wise default. Maybe a Thread constructor parameter like “auto_interrupt” could be good. But for at least some applications auto-interrupting threads is not going to be what we want, and since thread creation isn’t always controlled by the caller, this can be hard to control the way we want.
I’ll give an example. I’ve run Celery on Amazon ECS and on Heroku. When shutting down an instance, Amazon ECS will send a SIGTERM to the main process, and then give a certain amount of time for it to gracefully shut down before it kills the whole container.
Heroku sends SIGTERM to all processes immediately. This causes ungraceful shutdown for Celery tasks, even when the graceful shutdown would have worked just fine. The fine folks maintaining Celery were (rightly, I think) frustrated that they couldn’t handle shutdown of parallel processes gracefully in their own way.
Hm, I must’ve missed the part where the proposal became to interrupt all threads on a KeyboardInterrupt, and then introduce this flag to opt threads in or out of this behavior.
But why would we do this? From the start I was assuming the plan was to simply add a way to explicitly interrupt a thread, maybe via a method like Thread.interrupt() or something.
That would allow us to build a thread version of a TaskGroup (a ThreadGroup?), and if you used it properly on the main thread, you would essentially get this interrupting shutdown scenario (since a KeyboardInterrupt on the main thread would shut down the ThreadGroup, just like any exception shuts down a TaskGroup). But this is completely opt-in - if you don’t use a ThreadGroup and you don’t call Thread.interrupt()there’s no change whatsoever.
This seems to me like the simplest and safest way to progress this.
This is a part I object to. It shouldn’t be added to all threads; only be available when the creator of the thread specifically indicates that the work in the thread is designed to be interruptable.
This is already a problem with asyncio.cancel, where people expect every task to be safe to cancel without having to think about what they are cancelling. As a library author, I should be able to hand users an object that wraps interaction with a thread (especially since the current best way on this is just subclassing Thread…) without having to hide the thread object away so that users don’t interrupt it. (having the thread accessible to users without having to hide it away is also beneficial for users to do debugging.).
That’s most certainly NOT what I was thinking of, and I’m fairly certain
it’s not what the OP intended either.
To be clear: The only time a thread will be interrupted is if another
thread explicitly interrupts it. Any external signals sent to the
process, including SIGINT, would continue to be handled by the main
thread only.
So I was thinking about this. In the cases of using threads that I’ve done, the creator of the thread (the one that calls the Thread() constructor) and the caller that will be calling interrupt() are one and the same. Because it’s not the code that is running in the thread that will be saying “it’s OK to interrupt me” in either case, it feels like we already have the intent just from the .interrupt() method. We could also add an interruptible kwarg to Thread.__init__, but I’m not seeing what the additional value would be in doing that.
To my intention with the thread: I want to be able to cancel threads. Explicitly is good for me, and implicitly seems dangerous to me (though something like a global task group sounds like a nice touch for an easy API). But really, my main motivation was to see if I could find a way to avoid a blocker that has come up previously, that interrupting threads is inherently dangerous because they can be raised in inappropriate places. I hoped that by solving that problem, it could remove the main reason that prior discussions of cancelling threads have stalled out.
This raises a related question I’ve just though of: What should happen
if you call |Thread.interrupt()| on a thread that hasn’t declared itself
willing to be interrupted?
There are a couple of possibilities:
- Nothing happens – the thread simply ignores the request.
- An exception is raised letting you know you’ve attempted something
that is never going to work.
Not something we need to decide right now, just a question to keep in mind.
Ideally, the method doesn’t exist on threads that haven’t declared themselves interruptible. Combined with good type information, this would mean most modern IDEs will warn about calling a method that doesn’t exist long before even deploying the code, let along reaching that situation at runtime.
Having a seperate type for interruptible threads (even if it’s just a simple subclass of Thread that adds the functionality) avoids all of the backwards compatibility issues, signposts when code has been updated and intends to be compatible with the new functionality, and should entirely unblock the ability for those who want it to have it without breaking existing code.
I wonder if you’d be willing to construct a scenario where just adding Thread.interrupt() would cause trouble? I understand a theoretical concern with behavior that could be different, but I’m struggling to see how having a different class solves any practical problem. As far as I can see, the “owner” that would choose the different class and the one that would use the .interrupt() method would most typically be one and the same.