Skip to content

Commit c26af2b

Browse files
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. (cherry picked from commit d0906c9) Co-authored-by: benfogle <[email protected]>
1 parent 5f0305b commit c26af2b

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
@@ -234,6 +234,15 @@ The following exceptions are the exceptions that are usually raised.
234234
accidentally caught by code that catches :exc:`Exception` and thus prevent
235235
the interpreter from exiting.
236236

237+
.. note::
238+
239+
Catching a :exc:`KeyboardInterrupt` requires special consideration.
240+
Because it can be raised at unpredictable points, it may, in some
241+
circumstances, leave the running program in an inconsistent state. It is
242+
generally best to allow :exc:`KeyboardInterrupt` to end the program as
243+
quickly as possible or avoid raising it entirely. (See
244+
:ref:`handlers-and-exceptions`.)
245+
237246

238247
.. exception:: MemoryError
239248

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

@@ -677,3 +680,70 @@ Do not set :const:`SIGPIPE`'s disposition to :const:`SIG_DFL`
677680
in order to avoid :exc:`BrokenPipeError`. Doing that would cause
678681
your program to exit unexpectedly also whenever any socket connection
679682
is interrupted while your program is still writing to it.
683+
684+
.. _handlers-and-exceptions:
685+
686+
Note on Signal Handlers and Exceptions
687+
--------------------------------------
688+
689+
If a signal handler raises an exception, the exception will be propagated to
690+
the main thread and may be raised after any :term:`bytecode` instruction. Most
691+
notably, a :exc:`KeyboardInterrupt` may appear at any point during execution.
692+
Most Python code, including the standard library, cannot be made robust against
693+
this, and so a :exc:`KeyboardInterrupt` (or any other exception resulting from
694+
a signal handler) may on rare occasions put the program in an unexpected state.
695+
696+
To illustrate this issue, consider the following code::
697+
698+
class SpamContext:
699+
def __init__(self):
700+
self.lock = threading.Lock()
701+
702+
def __enter__(self):
703+
# If KeyboardInterrupt occurs here, everything is fine
704+
self.lock.acquire()
705+
# If KeyboardInterrupt occcurs here, __exit__ will not be called
706+
...
707+
# KeyboardInterrupt could occur just before the function returns
708+
709+
def __exit__(self, exc_type, exc_val, exc_tb):
710+
...
711+
self.lock.release()
712+
713+
For many programs, especially those that merely want to exit on
714+
:exc:`KeyboardInterrupt`, this is not a problem, but applications that are
715+
complex or require high reliability should avoid raising exceptions from signal
716+
handlers. They should also avoid catching :exc:`KeyboardInterrupt` as a means
717+
of gracefully shutting down. Instead, they should install their own
718+
:const:`SIGINT` handler. Below is an example of an HTTP server that avoids
719+
:exc:`KeyboardInterrupt`::
720+
721+
import signal
722+
import socket
723+
from selectors import DefaultSelector, EVENT_READ
724+
from http.server import HTTPServer, SimpleHTTPRequestHandler
725+
726+
interrupt_read, interrupt_write = socket.socketpair()
727+
728+
def handler(signum, frame):
729+
print('Signal handler called with signal', signum)
730+
interrupt_write.send(b'\0')
731+
signal.signal(signal.SIGINT, handler)
732+
733+
def serve_forever(httpd):
734+
sel = DefaultSelector()
735+
sel.register(interrupt_read, EVENT_READ)
736+
sel.register(httpd, EVENT_READ)
737+
738+
while True:
739+
for key, _ in sel.select():
740+
if key.fileobj == interrupt_read:
741+
interrupt_read.recv(1)
742+
return
743+
if key.fileobj == httpd:
744+
httpd.handle_request()
745+
746+
print("Serving on port 8000")
747+
httpd = HTTPServer(('', 8000), SimpleHTTPRequestHandler)
748+
serve_forever(httpd)
749+
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)