Although thinking about it more, and in the context of this thread, the idea of an operation to "raise exception X in thread Y" doesn't seem unreasonable. After all, this is basically what happens to the main thread from the signal handler already; the C signal handler caches a bit that gets picked up by PyErr_CheckSignals(), which raises things like KeyboardInterrupt at completely arbitrary points w.r.t. user code.
One possible approach (at a very schematic level, not thinking about impl details yet) would be to have a function like Thread.raise(e: BaseException) which stuffs the exception into thread-local storage, which then gets checked at similar times to signals. (Probably not the exact same times since I'm sure there's plenty of code depending implicitly on that function being a noop outside the main thread) Basically, it would be having the Python interpreter doing the cooperative part of cross-thread interruption, so that it would look non-cooperative from the user side. You could even have a Thread.raiseWhen to do the equivalent of setting an alarm.
This would definitely be a "use it only if you really mean it" function, but it could be quite useful. Implementing things like watchdog timers is hard right now because of a lack of this sort of feature, but this could open up some nice options.