Skip to content

Commit d0906c9

Browse files
authored
bpo-42340: Document issues around KeyboardInterrupt (GH-23255)
Update documentation to note that in some circumstances, KeyboardInterrupt may cause code to enter an inconsistent state. Also document sample workaround to avoid KeyboardInterrupt, if needed.
1 parent 755be9b commit d0906c9

File tree

3 files changed

+82
-0
lines changed

3 files changed

+82
-0
lines changed

Doc/library/exceptions.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,15 @@ The following exceptions are the exceptions that are usually raised.
254254
accidentally caught by code that catches :exc:`Exception` and thus prevent
255255
the interpreter from exiting.
256256

257+
.. note::
258+
259+
Catching a :exc:`KeyboardInterrupt` requires special consideration.
260+
Because it can be raised at unpredictable points, it may, in some
261+
circumstances, leave the running program in an inconsistent state. It is
262+
generally best to allow :exc:`KeyboardInterrupt` to end the program as
263+
quickly as possible or avoid raising it entirely. (See
264+
:ref:`handlers-and-exceptions`.)
265+
257266

258267
.. exception:: MemoryError
259268

Doc/library/signal.rst

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ This has consequences:
4646
arbitrary amount of time, regardless of any signals received. The Python
4747
signal handlers will be called when the calculation finishes.
4848

49+
* If the handler raises an exception, it will be raised "out of thin air" in
50+
the main thread. See the :ref:`note below <handlers-and-exceptions>` for a
51+
discussion.
4952

5053
.. _signals-and-threads:
5154

@@ -712,3 +715,70 @@ Do not set :const:`SIGPIPE`'s disposition to :const:`SIG_DFL`
712715
in order to avoid :exc:`BrokenPipeError`. Doing that would cause
713716
your program to exit unexpectedly also whenever any socket connection
714717
is interrupted while your program is still writing to it.
718+
719+
.. _handlers-and-exceptions:
720+
721+
Note on Signal Handlers and Exceptions
722+
--------------------------------------
723+
724+
If a signal handler raises an exception, the exception will be propagated to
725+
the main thread and may be raised after any :term:`bytecode` instruction. Most
726+
notably, a :exc:`KeyboardInterrupt` may appear at any point during execution.
727+
Most Python code, including the standard library, cannot be made robust against
728+
this, and so a :exc:`KeyboardInterrupt` (or any other exception resulting from
729+
a signal handler) may on rare occasions put the program in an unexpected state.
730+
731+
To illustrate this issue, consider the following code::
732+
733+
class SpamContext:
734+
def __init__(self):
735+
self.lock = threading.Lock()
736+
737+
def __enter__(self):
738+
# If KeyboardInterrupt occurs here, everything is fine
739+
self.lock.acquire()
740+
# If KeyboardInterrupt occcurs here, __exit__ will not be called
741+
...
742+
# KeyboardInterrupt could occur just before the function returns
743+
744+
def __exit__(self, exc_type, exc_val, exc_tb):
745+
...
746+
self.lock.release()
747+
748+
For many programs, especially those that merely want to exit on
749+
:exc:`KeyboardInterrupt`, this is not a problem, but applications that are
750+
complex or require high reliability should avoid raising exceptions from signal
751+
handlers. They should also avoid catching :exc:`KeyboardInterrupt` as a means
752+
of gracefully shutting down. Instead, they should install their own
753+
:const:`SIGINT` handler. Below is an example of an HTTP server that avoids
754+
:exc:`KeyboardInterrupt`::
755+
756+
import signal
757+
import socket
758+
from selectors import DefaultSelector, EVENT_READ
759+
from http.server import HTTPServer, SimpleHTTPRequestHandler
760+
761+
interrupt_read, interrupt_write = socket.socketpair()
762+
763+
def handler(signum, frame):
764+
print('Signal handler called with signal', signum)
765+
interrupt_write.send(b'\0')
766+
signal.signal(signal.SIGINT, handler)
767+
768+
def serve_forever(httpd):
769+
sel = DefaultSelector()
770+
sel.register(interrupt_read, EVENT_READ)
771+
sel.register(httpd, EVENT_READ)
772+
773+
while True:
774+
for key, _ in sel.select():
775+
if key.fileobj == interrupt_read:
776+
interrupt_read.recv(1)
777+
return
778+
if key.fileobj == httpd:
779+
httpd.handle_request()
780+
781+
print("Serving on port 8000")
782+
httpd = HTTPServer(('', 8000), SimpleHTTPRequestHandler)
783+
serve_forever(httpd)
784+
print("Shutdown...")
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Document that in some circumstances :exc:`KeyboardInterrupt` may cause the
2+
code to enter an inconsistent state. Provided a sample workaround to avoid
3+
it if needed.

0 commit comments

Comments
 (0)