Python's GIL means the interpreter can only run one thread at a time. Here is a demonstration:
from multiprocessing import Process
def loop():
while True:
pass
p1 = Process(target=loop)
p1.start()
loop()
The result below shows two cores fully pegged:

from threading import Thread
def loop():
while True:
pass
t1 = Thread(target=loop)
t1.start()
loop()
The result below shows no core fully pegged — the threads are merely context-switching:

Because of the GIL, multiple threads can't be interpreted at the same time. So let the Python interpreter run the main thread, and put the other work in a dynamic library compiled from a statically typed language — that way you can make full use of multiple cores.
// loop.c, compile with: gcc loop.c -shared -o libloop.so
void loop(){
while(1){
;
}
}
from ctypes import *
from threading import Thread
def loop():
while True:
pass
lib = cdll.LoadLibrary('./libloop.so')
t1 = Thread(target=lib.loop)
t1.start()
loop()
The result below shows that multithreading now pegs two cores just like multiprocessing:
