I recently ran into one of those maddening bugs that make you question whether your toolchain is cursed. I was working on a Python interface generated with SWIG on macOS (El Capitan in my case), and after what looked like a clean build, Python immediately crashed with a fatal error. In this post, I’ll walk you through what happened, what the error really means, how I fixed it, and even how I added some practice functionality to make sure my build was truly healthy.
My First Build Attempt
Here’s how I originally built my SWIG wrapper:
swig -python erk_integrator.i
gcc -c -fPIC -O3 model.c auxiliary_functions.c timing_functions.c
gcc -c -fPIC -O3 erk_integrator.c erk_integrator_wrap.c -I. -I/usr/local/include/python2.7
gcc -lpython -dynamiclib model.o erk_integrator.o erk_integrator_wrap.o auxiliary_functions.o timing_functions.o -o _erk_integrator.so
When I ran my test script with Homebrew’s Python:
/usr/local/bin/python test_erk.py
I got:
Fatal Python error: PyThreadState_Get: no current thread
Abort trap: 6
Weirdly, if I used the system Python:
/System/Library/Frameworks/Python.framework/Versions/2.7/bin/python2.7 test_erk.py
it worked fine.
Understanding The Error
In plain terms, my extension module was compiled and linked against the wrong Python. By explicitly linking -lpython
, I was accidentally pulling in the system’s Python framework instead of Homebrew’s.
The two libpython
builds had different runtime states, and when my module was imported into Homebrew Python, it saw a foreign interpreter and immediately crashed.
The golden rule for Python extensions:
- Always compile and link against the same interpreter you plan to run.
- On macOS, do not link
-lpython
for extension modules. The interpreter already provides it. - Use
python-config
from your target Python to get the right flags.
The Fix: Building the Right Way
Here’s the corrected build sequence using Homebrew Python (/usr/local/bin/python2.7
):
PY=/usr/local/bin/python2.7
# 1) SWIG wrapper
swig -python erk_integrator.i
# 2) Compile C sources
gcc -c -O3 -fPIC model.c auxiliary_functions.c timing_functions.c
gcc -c -O3 -fPIC erk_integrator.c
# 3) Compile wrapper with correct includes
gcc -c -O3 -fPIC erk_integrator_wrap.c $($PY-config --includes)
# 4) Link as a bundle without -lpython
gcc -bundle -undefined dynamic_lookup \
-o _erk_integrator.so \
model.o erk_integrator.o erk_integrator_wrap.o auxiliary_functions.o timing_functions.o
Notice: no -lpython
.
I checked my build:
otool -L _erk_integrator.so
There was no stray libpython.dylib
dependency exactly what I wanted.
Then I ran my test:
$PY test_erk.py
And finally, no crash.
Adding Practice Functionality
To make sure my setup was really healthy, I added a tiny test function.
C code (practice.c
)
double add(double a, double b) { return a + b; }
SWIG interface (erk_integrator.i
)
%module erk_integrator
%{
double add(double a, double b);
%}
double add(double a, double b);
Recompile & Link
gcc -c -O3 -fPIC practice.c
gcc -bundle -undefined dynamic_lookup -o _erk_integrator.so \
model.o erk_integrator.o erk_integrator_wrap.o auxiliary_functions.o timing_functions.o practice.o
Python Test (test_erk.py
)
import sys
print("Python:", sys.executable)
import erk_integrator as ei
print("2 + 3 =", ei.add(2.0, 3.0))
Output:
Python: /usr/local/bin/python2.7
2 + 3 = 5.0
Final Thought
The “Fatal Python error: PyThreadState_Get: no current thread” wasn’t really about threading at all it was about linking against the wrong Python runtime. The moment I stopped hardcoding -lpython
and relied on the target interpreter’s flags, everything clicked into place. My takeaway: always respect the Python you plan to run, and let it provide its own runtime. With that in place, adding new functionality and testing it becomes smooth sailing.