Can I run cleanup code in daemon threads in python?

Suppose I have some consumer daemon threads that constantly take objects from a queue whenever the main thread puts them there and performs some long operation (a couple of seconds) with them.

The problem is that whenever the main thread is done, the daemon threads are killed before they finish processing whatever is left in the queue.

I know that one way to solve this could be to wait for the daemon threads to finish processing whatever is left in the queue and then exit, but I am curious if there is any way for the daemon threads to "clean up" after themselves (i.e. finish processing whatever is left in the queue) when the main thread exits, without explicitly having the main thread tell the daemon threads to start cleaning up.

The motivation behind this is that I made a python package that has a logging handler class that puts items into a queue whenever the user tries to log something (e.g. with logging.info("message")), and the handler has a daemon thread that sends the logs over the network. I'd prefer if the daemon thread could clean up by itself, so users of the package wouldn't have to manually make sure to make their main thread wait for the log handler to finish its processing.

Minimal working example

# this code is in my package
class MyHandler(logging.Handler):
  def __init__(self, level):
    super().__init__(level=level)
    self.queue = Queue()
    self.thread = Thread(target=self.consume, daemon=True)
    self.thread.start()

  def emit(self, record):
    # This gets called whenever the user does logging.info, or similar
    self.queue.put(record)

  def consume(self):
    while True:
      record = self.queue.get()
      send(record) # send record over network, can take a few seconds (assume it never raises)
      self.queue.task_done()
# This is user's main code

# user will have to keep a reference to the handler for later. I want to avoid this.
my_handler = MyHandler()
# set up logging
logging.basicConfig(..., handlers=[..., my_handler])

# do some stuff...
logging.info("this will be sent over network")
# some more stuff...
logging.error("also sent over network")
# even more stuff

# before exiting must wait for handler to finish sending
# I don't want user to have to do this
my_hanler.queue.join()

You can use threading.main_thread.join() which will wait until shutdown like so:

import threading
import logging
import queue

class MyHandler(logging.Handler):
  def __init__(self, level):
    super().__init__(level=level)
    self.queue = queue.Queue()
    self.thread = threading.Thread(target=self.consume)  # Not daemon

    # Shutdown thread
    threading.Thread(
        target=lambda: threading.main_thread().join() or self.queue.put(None)
        ).start()
        
    self.thread.start()

  def emit(self, record):
    # This gets called whenever the user does logging.info, or similar
    self.queue.put(record)

  def consume(self):
    while True:
      record = self.queue.get()
      if record is None:
          print("cleaning")
          return  # Cleanup
      print(record) # send record over network, can take a few seconds (assume it never raises)
      self.queue.task_done()

Quick test code:

logging.getLogger().setLevel(logging.INFO)
logging.getLogger().addHandler(MyHandler(logging.INFO))
logging.info("Hello")
exit()