Speeding up Python with NIM

Lee Hawthorn April 29, 2025 #Nim #Python

We see in the Python community there is a big push to get it faster by disabling the GIL - this will probably see benefits for CPU-bound calculations, but only time will tell.

I wanted to explore how far we can go with regular Python, using native code.

One popular way to benchmark this is to calculate Fibonacci sequence.

The naive way to compute this is with recursion:


def fib(n):
   if n <= 1:
       return n
   else:
       return(fib(n-1) + fib(n-2))

Instead of using recursion, we can flatten the code and use a loop to improve performance.

from time import perf_counter

def fib(n):
  if n == 0:
    return 0
  elif n < 3:
    return 1

    prev, curr = 1, 1
    for _ in range(3, n + 1):
        prev, curr = curr, prev + curr
    return curr

if __name__ == "__main__":

  n = 50

  print('Measuring Python...')

  start_py = perf_counter()

  for i in range(1, n):
    print(fib(i))
  end_py = perf_counter()

  print('Python Seconds Elapsed: {:.6f}'.format(end_py - start_py))

With my laptop, this outputs Python Seconds Elapsed: 0.000187

Single-threaded, I was surprised how fast this executed (Python 3.13).

This shows how we should try and optimize the code we have before thinking about running on lots of cores.

Nevertheless, I wanted to see the difference using native code.

If you want to give this a go on your own machine, we will need to install NIM and UV.

I set up this code with NIM file: pure_fib.nim - this language is compiled to C, so we expect a boost. I tried to keep the code similar.

import times

proc fib(n: int): int =
  var prev = 0
  var curr = 1
  for i in 2 .. n:
    let next = prev + curr
    prev = curr
    curr = next

  return curr

# Main block to call fib and time its execution
when isMainModule:
  var result: int = 0
  var n: int = 50
  let startTime = cpuTime() # Record the start time

  for a in 1 .. n-1:
    result = fib(a)
    echo result # Call the fib function

  let endTime = cpuTime() # Record the end time

  echo "Fibonacci(50): ", result
  echo "Execution time: ", (endTime - startTime), " seconds"

You can compile this code with: nim c -d:release pure_fib.nim

To calculate a fib of 50, this takes: Execution time: 0.000140 seconds, which is a bit faster than Python.

We can use this function in Python by making a small change to the code.

Initialize a UV project in a folder of your choice. First, make a file called .python-version and enter the value 3.13.

This will tell UV to install Python 3.13 into the project.

CD into your project folder and type uv init.

Activate your environment.

First, install nimpy:

nimble install nimpy

We need to add a PyPI library called nimporter, and this needs setuptools. We can do this with UV:

uv add nimporter
uv add setuptools

After running uv add, there will be a .venv environment created in your project folder. By keeping this isolated, you will have an easier cleanup later.

Change the NIM file by adding import nimpy and adding a pragma {.exportpy.} - this is how you export the procedure for use in Python.

import nimpy
import times

proc fib(n: int): int {.exportpy.} =
  var prev = 0
  var curr = 1
  for i in 2 .. n:
    let next = prev + curr
    prev = curr
    curr = next

  return curr

# Main block to call fib and time its execution
when isMainModule:
  var result: int = 0
  var n: int = 50
  let startTime = cpuTime() # Record the start time

  for a in 1 .. n-1:
    result = fib(a)
    echo result # Call the fib function

  let endTime = cpuTime() # Record the end time

  echo "Fibonacci(50): ", result
  echo "Execution time: ", (endTime - startTime), " seconds"

We will use a new Python file to test this nim proc. Create main.py in the same folder as the nim file.


import nimporter
from time import perf_counter
import pure_fib # Nim imports

n = 50

print('Measuring Nim...')
start_nim = perf_counter()

for i in range(1, n):
  print(pure_fib.fib(i))

end_nim = perf_counter()

print('Nim Elapsed: {:.6f}'.format(end_nim - start_nim))

When you run this code, the nim file will be compiled, and a binary library will be stored in the __pycache__ folder. Run the Python for a second time.

I got: Execution time: 0.0001198939 seconds.

In summary, we can see:

TypeMS
Pure Python187
Pure Nim140
Python/Nim120

Note the fib function when used in Python is compiled as a library, and we're calling it from Python 50 times. I didn't bother to use any caching/memoization as these speeds are already fast. As an exercise, you can add caching in Python and/or NIM to see if it makes a difference.

The nimporter library is much more powerful. It actually lets you bundle up your Python/Nim as a PyPI package. Users don't have to have Nim installed to use it - similar to how we don't need a C compiler to use Numpy.

In summary, we can optimize Python code to speed up execution. Nim code is not too different from Python code either. I believe Nim is an option open to Python developers to use native code to get faster execution of code, where it's needed.