Why python raises ValueError: signal only works in main thread, and how to structure a bot's entry point to avoid it entirely.
Fixing "signal only works in main thread" in Python
ValueError: signal only works in main thread of the main interpreter
This error is Python being very literal about a hard constraint: signal handlers can only ever be registered from the main thread. If you're hitting this — often from inside a library you didn't expect to be touching signals at all — here's exactly why, and how to structure your code so it doesn't come up.
Table of Contents
- Why the Constraint Exists
- The Usual Trigger
- Reproducing It Minimally
- The Fix: Call From the Main Thread
- How StayPresent Handles This Gracefully
- When You Genuinely Can't Use the Main Thread
- Full Example
- FAQs
- Conclusion
Why the Constraint Exists
Signal delivery in CPython is fundamentally tied to the main thread — the interpreter only processes signals on it, regardless of which thread technically calls signal.signal(). This isn't a Python design choice you can configure around; it reflects how signal handling works at the OS/interpreter level. Trying to register a handler from any other thread raises ValueError immediately, rather than silently failing or working unpredictably.
The Usual Trigger
This most commonly shows up when something that installs signal handlers — your own code, or a library like StayPresent that handles SIGINT/SIGTERM for graceful shutdown — gets called from inside a threading.Thread, rather than directly from your script's main execution path:
import threading
import staypresent
def start_bot():
staypresent.run("bot.py") # called from a background thread
threading.Thread(target=start_bot).start()
Reproducing It Minimally
Stripped down to the actual Python behavior involved, without any library at all:
import signal
import threading
def register():
signal.signal(signal.SIGTERM, lambda s, f: None)
threading.Thread(target=register).start()
# ValueError: signal only works in main thread of the main interpreter
The Fix: Call From the Main Thread
The straightforward fix is simply not launching the signal-handling code from a background thread in the first place:
import staypresent
staypresent.run("bot.py") # called directly, from the main thread
If your application genuinely needs other background threads for unrelated work, that's fine — the constraint is specifically about where signal handlers get registered, not about whether other threads can exist at all.
How StayPresent Handles This Gracefully
Rather than letting this bubble up as an unhandled ValueError and crashing your application, StayPresent checks proactively: if run() is called from a non-main thread, it logs a warning and simply skips installing its own signal handlers, continuing to run normally otherwise:
import threading
import staypresent
def start():
staypresent.run("bot.py")
# WARNING logged: signal handlers can only be installed from the
# main thread — graceful shutdown handling skipped
threading.Thread(target=start).start()
The web server and bot subprocess still start and run correctly in this case — you simply lose the automatic Ctrl+C/SIGTERM graceful shutdown coordination, rather than the whole thing crashing outright.
When You Genuinely Can't Use the Main Thread
If your architecture requires run() to be launched from a non-main thread — for example, it's one component inside a larger application that owns the main thread for something else — you have two reasonable options:
- Accept the tradeoff and let StayPresent skip its own signal handling (the default, graceful behavior described above), relying on your outer application's own shutdown mechanism to eventually terminate the process (which will still tear down the subprocess, just without StayPresent's own coordinated cleanup step running first).
-
Set
install_signal_handlers=Falseexplicitly, and implement your own coordinated shutdown logic from wherever your application's actual main thread does live.
staypresent.run(
"bot.py",
install_signal_handlers=False,
)
Full Example
import staypresent
# Correct: called directly from the script's own main execution path
staypresent.web.json({"status": "running"})
staypresent.run(
"bot.py",
restart_on_crash=True,
)
python main.py
# Ctrl+C works correctly — no ValueError, no warning
FAQs
Does this affect the bot subprocess itself?
No — this is entirely about StayPresent's own signal handler registration in the orchestrating process. Your bot script, running as a separate subprocess, isn't affected by which thread launched staypresent.run().
Can I register my own signal handler from a background thread instead?
No — the same Python-level constraint applies to any code calling signal.signal(), not just StayPresent's internal handling.
Is this a bug in StayPresent?
No — it's a fundamental Python/OS constraint that StayPresent specifically detects and handles gracefully (a warning and a skip) rather than letting it crash your application with an unhandled exception.
Conclusion
"Signal only works in main thread" isn't a bug to work around cleverly — it's a hard constraint on where signal.signal() can be called from at all. The simplest fix is calling staypresent.run() directly from your script's main execution path; if that's not possible, StayPresent degrades gracefully with a clear warning instead of crashing, and install_signal_handlers=False gives you full control if you need to manage shutdown yourself.
pip install staypresent[prod]
Top comments (0)