Ctrl+C terminates the main thread, but because your threads aren't in daemon mode, they keep running, and that keeps the process alive. We can make them daemons:

f = FirstThread()
f.daemon = True
f.start()
s = SecondThread()
s.daemon = True
s.start()

But then there's another problem - once the main thread has started your threads, there's nothing else for it to do. So it exits, and the threads are destroyed instantly. So let's keep the main thread alive:

import time
while True:
    time.sleep(1)

Now it will keep print 'first' and 'second' until you hit Ctrl+C.

Edit: as commenters have pointed out, the daemon threads may not get a chance to clean up things like temporary files. If you need that, then catch the KeyboardInterrupt on the main thread and have it co-ordinate cleanup and shutdown. But in many cases, letting daemon threads die suddenly is probably good enough.

Answer from Thomas K on Stack Overflow
🌐
Raspberry Pi Forums
forums.raspberrypi.com › board index › programming › python
[SOLVED] Handling Ctrl-C / Kill in threads - Raspberry Pi Forums
Here, to kill a process, you can simply call the method: yourProcess.terminate() Python will kill your process (on Unix through the SIGTERM signal, while on Windows through the TerminateProcess() call). Pay attention to use it while using a Queue or a Pipe! (it may corrupt the data in the Queue/Pipe) ... Tue Mar 19, 2019 8:43 am Not sure if it's applicable wrt signal stuff but I usually either set daemon = True Which seems to work well for me. I use signal, I have 5 threads running. All shuts down fine with a ctrl-c.
Discussions

[SOLVED] Handling Ctrl-C / Kill in threads - Raspberry Pi Forums
Here, to kill a process, you can ... Python will kill your process (on Unix through the SIGTERM signal, while on Windows through the TerminateProcess() call). Pay attention to use it while using a Queue or a Pipe! (it may corrupt the data in the Queue/Pipe) ... Tue Mar 19, 2019 8:43 am Not sure if it's applicable wrt signal stuff but I usually either set daemon = True Which seems to work well for me. I use signal, I have 5 threads running. All shuts down fine with a ctrl-... More on raspberrypi.org
🌐 raspberrypi.org
March 18, 2019
multithreading - Python threads exit with ctrl-c in Python - Stack Overflow
Bring the best of human thought ... at your work. Explore Stack Internal ... I am having the Python Multi-threaded program as below. If I press ctrl+c within 5 seconds (approx), It is going inside the KeyboardInterrupt exception. Running the code longer than 15 seconds failed to respond to ctrl+c. If I press ctrl+c after 15 seconds, It is not ... More on stackoverflow.com
🌐 stackoverflow.com
python 3.x - Ctrl+c not stopping a Thread in Windows + python3.7 - Stack Overflow
I'm trying this simple thread with a while loop inside. When I'm inside the while loop, Ctrl+C has no effect in stopping my program. Once I go do something else after the while loop, the script sto... More on stackoverflow.com
🌐 stackoverflow.com
python - Ctrl-C doesn't work when using threading.Timer - Stack Overflow
This is a possible workaround: using time.sleep() instead of Timer means a "graceful shutdown" mechanism can be implemented ... for Python3 where, it appears, KeyboardInterrupt is only raised in user code for the main thread. Otherwise, it appears, the exception is "ignored" as per here: in fact it results in the thread where it occurs dying immediately, but not any ancestor threads, where problematically it can't be caught. Let's say you want Ctrl... More on stackoverflow.com
🌐 stackoverflow.com
🌐
Python
bugs.python.org › issue35935
Issue 35935: threading.Event().wait() not interruptable with Ctrl-C on Windows - Python tracker
February 8, 2019 - This issue tracker has been migrated to GitHub, and is currently read-only. For more information, see the GitHub FAQs in the Python's Developer Guide · This issue has been migrated to GitHub: https://github.com/python/cpython/issues/80116
🌐
Mandricmihai
mandricmihai.com › 2021 › 01 › Python threads how to timeout and use KeyboardIntrerupt CTRL C.html
Python threads, how to timeout, use KeyboardIntrerupt (CTRL + C) and exit all threads
September 26, 2021 - SIGINT is the signal for CTRL+C and the signal_handler is the function to be executed when the SIGINT is detected. A new signal handler can be set only from the main-thread otherwise an exception is raised. #!/usr/bin/env python3 import signal import sys import threading from time import sleep def process(): """Long lasting operation""" sleep(300) print("finished work") def signal_handler(signal, frame): print("Closing main-thread.This will also close the background thread because is set as daemon.") sys.exit(0) def main(argv): signal.signal(signal.SIGINT, signal_handler) t = threading.Thread(target=process) t.daemon = True t.start() while t.is_alive(): print("Waiting for background thread to finish") sleep(1) print("Thread finished task, exiting") return 0 if __name__ == "__main__": sys.exit(main(sys.argv))
🌐
Python
bugs.python.org › issue1167930
Issue 1167930: threading.Thread.join() cannot be interrupted by a Ctrl-C - Python tracker
This issue tracker has been migrated to GitHub, and is currently read-only. For more information, see the GitHub FAQs in the Python's Developer Guide · This issue has been migrated to GitHub: https://github.com/python/cpython/issues/41740
🌐
GitHub
gist.github.com › ruedesign › 5218221
Python thread sample with handling Ctrl-C · GitHub
Thanks for sharing your code. I've had trouble stopping threads using Ctrl-C and clearing up processes. This code works perfect for me.
🌐
Miguel Grinberg
blog.miguelgrinberg.com › post › how-to-kill-a-python-thread
How to Kill a Python Thread - miguelgrinberg.com
October 19, 2020 - If the thread is configured as a daemon thread, it will just stop running abruptly when the Python process ends. If the thread is not a daemon thread, then the Python process will block while trying to exit, waiting for this thread to end, so ...
🌐
py4u
py4u.org › blog › python-program-with-thread-can-t-catch-ctrl-c
Python Threads Not Catching Ctrl+C: How to Handle SIGINT and Terminate Threads Properly
When you press Ctrl+C and your Python program with threads doesn’t exit, it’s almost always due to one of these issues: The main thread is often blocked waiting for worker threads to finish (via thread.join()).
Find elsewhere
Top answer
1 of 2
3

After the first execution of

threads = [t.join(1) for t in threads if t is not None and t.isAlive()]

your variable threads contains

[None, None, None, None, None, None, None, None, None, None]

after the second execution, the same variable threads contains:

[]

At this point, len(threads) > 0 is False and you get out of the while loop. Your script is still running since you have 10 threads still active, but since you're not anymore in your try / except block (to catch KeyboardInterrupt), you can't stop using Ctrl + C

Add some prints to your script to see what I described:

#!/usr/bin/python

import os, sys, threading, time

class Worker(threading.Thread):
  def __init__(self):
    threading.Thread.__init__(self)
    # A flag to notify the thread that it should finish up and exit
    self.kill_received = False

  def run(self):
      while not self.kill_received:
          self.do_something()

  def do_something(self):
      [i*i for i in range(10000)]
      time.sleep(1)

def main(args):

    threads = []
    for i in range(10):
        t = Worker()
        threads.append(t)
        t.start()
        print('thread {} started'.format(i))

    while len(threads) > 0:
        print('Before joining')
        try:
            # Join all threads using a timeout so it doesn't block
            # Filter out threads which have been joined or are None
            threads = [t.join(1) for t in threads if t is not None and t.isAlive()]
            print('After join() on threads: threads={}'.format(threads))

        except KeyboardInterrupt:
            print("Ctrl-c received! Sending kill to threads...")
            for t in threads:
                t.kill_received = True
    print('main() execution is now finished...')

if __name__ == '__main__':
  main(sys.argv)

And the result:

$ python thread_test.py
thread 0 started
thread 1 started
thread 2 started
thread 3 started
thread 4 started
thread 5 started
thread 6 started
thread 7 started
thread 8 started
thread 9 started
Before joining
After join() on threads: threads=[None, None, None, None, None, None, None, None, None, None]
Before joining
After join() on threads: threads=[]
main() execution is now finished...

Actually, Ctrl + C doesn't stop to work after 15 seconds, but after 10 or 11 seconds. This is the time needed to create and start the 10 threads (less than a second) and to execute join(1) on each thread (about 10 seconds).

Hint from the doc:

As join() always returns None, you must call isAlive() after join() to decide whether a timeout happened – if the thread is still alive, the join() call timed out.

2 of 2
0

to follow up on the poster above, isAlive() got renamed to is_alive() tried on Python 3.9.6

full code:

#!/usr/bin/python

import os, sys, threading, time

class Worker(threading.Thread):
  def __init__(self):
    threading.Thread.__init__(self)
    # A flag to notify the thread that it should finish up and exit
    self.kill_received = False

  def run(self):
      while not self.kill_received:
          self.do_something()

  def do_something(self):
      [i*i for i in range(10000)]
      time.sleep(1)

def main(args):

    threads = []
    for i in range(10):
        t = Worker()
        threads.append(t)
        t.start()
        print('thread {} started'.format(i))

    while len(threads) > 0:
        print('Before joining')
        try:
            # Join all threads using a timeout so it doesn't block
            # Filter out threads which have been joined or are None
            threads = [t.join(1) for t in threads if t is not None and t.is_alive()]
            print('After join() on threads: threads={}'.format(threads))

        except KeyboardInterrupt:
            print("Ctrl-c received! Sending kill to threads...")
            for t in threads:
                t.kill_received = True
    print('main() execution is now finished...')

if __name__ == '__main__':
  main(sys.argv)
Top answer
1 of 4
7

The way threading.Thread (and thus threading.Timer) works is that each thread registers itself with the threading module, and upon interpreter exit the interpreter will wait for all registered threads to exit before terminating the interpreter proper. This is done so threads actually finish execution, instead of having the interpreter brutally removed from under them. So when you hit ^C, the main thread receives the signal, decides to terminate and waits for the timers to finish.

You can set threads daemonic (with the setDaemon method) to make the threading module not wait for these threads, but if they happen to be executing Python code while the interpreter exits, you get confusing errors during exit. Even if you cancel the threading.Timer (and set it daemonic) it can still wake up while the interpreter is being destroyed -- because threading.Timer's cancel method just tells the threading.Timer not to execute anything when it wakes up, but it has to actually execute Python code to make that determination.

There is no graceful way to terminate threads (other than the current one), and no reliable way to interrupt a thread that's blocked. A more manageable approach to timers is usually an event loop, like the ones GUIs and other event-driven systems offer you. What to use depends entirely on what else your program will be doing.

2 of 4
2

There is a presentation by David Beazley that sheds some light on the topic. The PDF is available here. Look around pages 22--25 ("Interlude: Signals" to "Frozen Signals").

🌐
CSDN
devpress.csdn.net › python › 62fd297cc677032930802df3.html
Cannot kill Python script with Ctrl-C_python_Mangs-Python
August 18, 2022 - I also tried adding a handler for ... How can I resolve this? Ctrl+C terminates the main thread, but because your threads aren't in daemon mode, they keep running, and that keeps the process alive....
🌐
GitHub
gist.github.com › rldotai › b88cf8a2e60e2537d5a69864bf4f73c6
Python threads that can be safely terminated via KeyboardInterrupt (e.g., ctrl-c) · GitHub
Python threads that can be safely terminated via KeyboardInterrupt (e.g., ctrl-c) - interruptible_threads.py
🌐
Reddit
reddit.com › r/learnpython › issue with keyboardinterrupt and threads in python
Issue with KeyboardInterrupt and Threads in Python : r/learnpython
May 4, 2024 - Subreddit for posting questions and asking for general advice about all topics related to learning python. ... Sorry, this post was deleted by the person who originally posted it. Share ... I wanted to clarify the problem I'm having handling `KeyboardInterrupt` in this project. The problem is not catching general exceptions, but specifically the behavior of `KeyboardInterrupt` when threads are involved. When I press Ctrl+C, I would expect the program to stop and if I had set the KeyboardInterrupt it would execute a specific block of code that I have prepared to handle this interruption.
🌐
GitHub
github.com › python › cpython › issues › 96827
RuntimeError after ctrl-C interrupt when asyncio is closing the threadpool · Issue #96827 · python/cpython
September 14, 2022 - Test program based on a SO question; press Ctrl-C after "coro stop" and before "thread stop": import asyncio import time async def main(): print("coro start") loop = asyncio.get_running_loop() loop.run_in_executor(None, blocking) await a...
Author   xitop
🌐
CopyProgramming
copyprogramming.com › howto › python-program-with-thread-can-t-catch-ctrl-c
Mastering Ctrl+C Handling in Python: Signals, Threads, and Multiprocessing in 2026 - Python catch ctrl-c
February 28, 2026 - Python's signal module lets programs catch asynchronous events like Ctrl+C via SIGINT. The signal.signal() function registers a handler callable that receives the signal number and frame, executed in the main thread only. Handlers should perform minimal work—set a flag, not acquire locks or call complex functions—to avoid deadlocks.
🌐
Regexprn
regexprn.com › 2010 › 05 › killing-multithreaded-python-programs.html
Killing Multithreaded Python Programs with Ctrl-C
May 17, 2010 - If you have ever done multithreaded ... hit Ctrl-C in the terminal and have it exit like a normal Python process. Instead you have to put the process in the background (Ctrl-D) and then either "kill %%" or kill the PID. The good news is that it doesn't have to be this way.