How to Fix Fatal Python Error: PyThreadState_Get When Using SWIG on macOS

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:

  1. Always compile and link against the same interpreter you plan to run.
  2. On macOS, do not link -lpython for extension modules. The interpreter already provides it.
  3. 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.

Related blog posts