What does enabling Python's faulthandler give you when a process dies from a segfault?
answer
- The process vanished without printing anything
- Fatal signals bypass exception handling
- A stdlib module that prints on crash
- One environment variable arms it at startup
- faulthandler.enable and PYTHONFAULTHANDLER
basics
~20 sIt installs handlers for fatal signals such as SIGSEGV and SIGABRT. When one fires, the interpreter writes a Python stack for each thread to stderr and then lets the process die exactly as it would have.
solid answer
~40 sBy default a fatal signal kills CPython before its exception machinery ever runs, so you get an exit status and nothing else. `faulthandler.enable()` installs low-level handlers for `SIGSEGV`, `SIGFPE`, `SIGABRT`, `SIGBUS` and `SIGILL` that dump the Python stack of every thread to `sys.stderr` and then re-raise the signal with the default handler, so the exit status and any core file are unchanged. You can arm it without touching the code by setting `PYTHONFAULTHANDLER=1` or passing `-X faulthandler`, which is the version that also covers crashes during startup. `faulthandler.is_enabled()` reports the current state and `faulthandler.disable()` removes the handlers. The dump is deliberately minimal - file, line and function per frame, no local variables and no source lines - because it has to be safe to write from inside a signal handler.
code
console · 1 linePYTHONFAULTHANDLER=1 python3 -c "import faulthandler; print(faulthandler.is_enabled())"go deeper
Be ready to say that a segfault normally prints nothing, and that faulthandler.enable() or PYTHONFAULTHANDLER=1 makes the interpreter print a Python stack before the process dies.
Explain the mechanics: handlers for SIGSEGV, SIGFPE, SIGABRT, SIGBUS and SIGILL, a dump of every thread's Python frames, then the signal re-raised so the exit status is unchanged; and why the dump has no locals.
Show you know the startup gap - only the environment variable or -X faulthandler covers crashes during import - and that you assert faulthandler.is_enabled() rather than trusting the deployment to carry the variable.
Own the policy: enabling it fleet-wide costs nothing until a fault, but only pays off if the destination stream is actually captured and retained, so treat the dump as part of the crash-evidence contract with the platform.
## Why a fatal signal leaves you with nothing A normal Python traceback is a product of the interpreter's exception machinery: an exception object is raised, frames are unwound, and the default handler formats the chain on the way out. A fatal signal skips all of that. When a compiled extension dereferences a bad pointer, or the interpreter itself hits an assertion and calls `abort()`, the operating system delivers `SIGSEGV` or `SIGABRT` and the default disposition is to terminate the process immediately. No Python-level `except` runs, no `atexit` handler runs, and nothing is printed. From the outside you see a process that vanished with exit status 139 or 134, or a container that restarted, and no clue about which Python code was on the stack at the time. ## What faulthandler actually installs `faulthandler.enable()` registers C-level signal handlers for `SIGSEGV`, `SIGFPE`, `SIGABRT`, `SIGBUS` and `SIGILL`. When one of those signals arrives, the handler: 1. walks the frame objects the interpreter has already recorded and writes them out as text, 2. then restores the default disposition for that signal 3. and re-raises it. That last step matters: **faulthandler is not a recovery mechanism**. The process still dies, with the same exit status, and a core file is still produced if the system is configured to produce one. All faulthandler adds is a few lines of text on the way down. ## The dump is austere on purpose A signal handler runs in a **hostile context**. The interpreter may be mid-allocation, holding locks, or in an inconsistent state, so the handler avoids allocating memory and avoids calling back into Python. It writes directly to a file descriptor. The consequence is that the output is thinner than a real traceback: for each thread you get the thread identifier and then a frame list of file path, line number and function name, most recent first. - There are no source lines, no local variables, no exception message and no chained cause. - It also cannot show you the C frames of a compiled extension - it shows the Python frames that called into it. That is usually enough to say "the crash happened under this call site", which is the question you actually need answered at 3 a.m. ## Three ways to arm it, and why one of them is different - Calling **`faulthandler.enable()`** from code is the flexible option: you can pass `file=` to send the dump somewhere other than `sys.stderr`, and `all_threads=False` if you only want the faulting thread. - Setting the environment variable **`PYTHONFAULTHANDLER`** to a non-empty value, or passing **`-X faulthandler`** on the command line, enables it during interpreter startup instead. That difference is not cosmetic: a crash inside an import, in an extension module's initialisation, or in anything that runs before your own first line of code is only covered by the environment variable or the command-line option, because your `enable()` call has not executed yet. For a service, the environment variable is the one to reach for, since it needs no code and cannot be skipped by an early failure. ## Checking and undoing it `faulthandler.is_enabled()` returns a boolean, which is worth asserting at startup if you depend on the handler being present - it is easy for an environment variable to be dropped by a deployment change. `faulthandler.disable()` removes the handlers and restores the previous behaviour. Disabling is rarely wanted in production; the common reasons are: - a test that deliberately crashes a child process, - or a host process that installs its own crash reporter and wants to own the signal. ## What it costs, and what it does not cover The cost until a fault occurs is a handful of installed signal handlers and **effectively no runtime overhead** - there is no sampling, no timer and no extra work per bytecode. That is why leaving it on by default in a long-running service is close to free. What it cannot do is report a death that never delivers one of those signals: a process terminated with `SIGKILL`, whether by the kernel's out-of-memory killer or by an orchestrator's hard stop, cannot run any handler at all, so silence there is expected and tells you to look at the platform rather than at the interpreter.
- Why would you prefer PYTHONFAULTHANDLER over calling faulthandler.enable() in your own startup code?The environment variable is applied while the interpreter starts, before any of your modules import. A crash inside an import, or inside a compiled extension's initialisation, happens before your `enable()` call would ever run, so only the environment variable (or `-X faulthandler`) covers it. It also survives refactoring of your startup path and needs no code at all.
- Does enabling faulthandler stop the process from crashing, or change its exit status?Neither. After writing the dump the handler restores the default disposition for that signal and re-raises it, so the process dies from the original signal with the original exit status and still writes a core file if the system allows one. faulthandler is purely additive output; anything relying on the exit code keeps working.
- Why does the faulthandler dump show no local variables or source lines?It runs inside a signal handler, where the interpreter may be mid-allocation or holding locks, so it must not allocate memory or call back into Python. It writes plain text straight to a file descriptor: thread id, then file, line and function per frame. That austerity is what makes it safe to emit from a process that is already broken.
It is a flight recorder rather than a parachute: it does not stop the crash, it just makes sure something readable survives it.
saying these in an interview costs you the question
- Claiming faulthandler catches the crash and keeps the process alive
- Thinking it turns a segfault into a catchable Python exception
- Expecting locals or source lines in the dump
- Assuming it is on by default in CPython 3.14
- Believing enable() covers crashes during interpreter startup
- Expecting a dump when the kernel sends SIGKILL