How do you debug a Python script that crashes Python itself?

TL;DR

Use faulthandler.enable(); Python will print the stack trace to stderr before fully crashing.

Backstory

Today I tried to daemonize a Python script that calls requests.get on macOS, and it was insta-dying for no apparent reason. I tested without daemonization, but the problem only popped up when I daemonize it.

I was doing something like this:

from daemon import DaemonContext
from daemon.pidfile import PIDLockFile
import requests
import time


pid = PIDLockFile(path.join(home_dir, 'pidfile_watcher'))
fp  = open("out.log", "a+")
fpe = open("err.log", "a+")

with DaemonContext(
  pidfile=pid,
  stdout=fp,
  stderr=fpe
):
  while True:
    response = requests.get("https://example.com");
    print(response.text)
    time.sleep(60*10)

fp.close()
fpe.close()

Many researches, print()s and one final check on Console.app (the macOS standard logger) later, I found that it was segfaulting at requests.get().

Why did it take so long?

Because daemonized script don’t properly log its death even when you pass a file pointer for stderr.
Also, crash report found in Console.app does not provide much information unless you write most of your program (note the word ‘program’ - this includes not only your script, but also Python packages and/or even Python itself)! So the only realistic bet may be to spam print(), which is already tedious because you need to properly tell DaemonContext to redirect stdout to a file (I didn’t give any at first, which caused even more headaches).

The solution

It was faulthandler.enable() that I needed to call.

https://docs.python.org/3/library/faulthandler.html

faulthandler — Dump the Python traceback

This module contains functions to dump Python tracebacks explicitly, on a fault, after a timeout, or on a user signal. Call faulthandler.enable() to install fault handlers for the SIGSEGV, SIGFPE, ...

This causes Python to print stack trace to stderr when signal is received (in macOS, segmentation fault causes a signal SIGSEGV; probably all *nix systems also does.) instead of straight-up dying on the spot. This method enabled me to pinpoint exactly which call was causing my script to crash.

What was the problem, by the way?

Turns out, this is a known, (probably) unavoidable issue when using request package in macOS.

https://github.com/python/cpython/issues/74570

Segfault on OSX with 3.6.1 · Issue #74570 · python/cpython

BPO 30385 Nosy @ronaldoussoren, @ned-deily, @Birne94, @PythonCoderAS Superseder bpo-24273: _scproxy.so causes EXC_BAD_ACCESS (SIGSEGV) Files request_multiprocessing_crash.py: crashing program Note:...

To avoid this issue, you must drop proxy support as segfault occurs at process related to polling OS’s proxy configuration, which is “not fork safe”.

import os
os.environ['no_proxy'] = "*"