Introduction

tags: book

Authors: Kjell Jorner,


In recent years, Python has taken over as the most commonly used language in applied science. Although Python itself as a scripting language is quite slow, it often serves as a glue between computationally demanding procedures written in compiled languages such as Fortran or C. Prime examples of this approch are the NumPy and SciPy packages that power applications such as machine learning via scikit-learn and quantum chemistry via PySCF.

There are a number of reasons why you would want to interface Python and Fortran, such as:

  • Making your Fortran application available within the Python eco-system
  • Speeding up slow parts of a Python code

There are two main approaches to accessing Fortran from Python:

  • Automatic interface generation
  • Writing an explicit C interface in Fortran

Behind the scenes, the automatic interface generators actually create a C interface for you, so the approaches don't really differ that much technically. But the user experience and ease of use is different.

Examples

Using your Fortran code in Python really isn't that difficult Here are two basic examples of the different approaches.

Automatic interface generation with f2py

We start with a simple Fortran function that adds two numbers:

integer function add(a, b)
  integer, intent(in) :: a, b
  add = a + b
end function add

We place this function in a file called add.f90 and use f2py to wrap it into a Python module called add

python -m numpy.f2py -c add.f90 -m add

A shared library file is created, in this case on MacOS with Python 3.9 it is called add.cpython-39-darwin.so, but the exact name is not important. This library can be imported directly into Python and we can use the function as a conventional Python function:

>>> from add import add
>>> add(1, 2)
3

C interface with ctypes

Adapting the function

We start with the same function from the previous section, but now we need to adapt it to be interoperable with C:

module mod_add
  use, intrinsic :: iso_c_binding, only: c_int
  implicit none

contains
  integer(c_int) function add(a, b) bind(c)
    integer(c_int), value, intent(in) :: a, b
    add = a + b
  end function add
end module mod_add

First, we needed to wrap the function in a module, mod_add that we put in a file named mod_add.f90. Second, we needed to add a number of code elements to ensure C interoperability:

use, intrinsic :: iso_c_binding, only: c_int
This imports the c_int integer type from the intrinsic module iso_c_binding which provides support for C interoperability.
integer(c_int)
Variable types need to be C interoperable.
bind(c)
Notifies the compiler that the function should be C interoperable.
value
Notifies the compiler that the variable will be passed by value rather than reference.

Compiling to a shared library

We now need to compile the function into a shared library, here using gfortran:

gfortran -shared -fPIC mod_add.f90 -o mod_add.so

This creates the shared library file mod_add.so

Importing the shared library from Python

We can now import and use the function in Python with the built-in ctypes library:

>>> from ctypes import CDLL
>>> lib = CDLL("mod_add.so")
>>> lib.add(1, 2)
3

Actually, we were a bit sloppy in the previous example as we should really match our variable types to conform to those of our Fortran function:

>>> from ctypes import CDLL, c_int
>>> lib = CDLL("mod_add.so")
>>> lib.add(c_int(1), c_int(2))
3

Pros and cons with the two approaches

At this point you might be thinking that the explicit C interface seems awfully complicated compared to automatic interface generation with f2py. Indeed, f2py is very convenient when wrapping individual functions or subroutines, but currently has some important limitations such as lack of support for derived types. Additionally, when writing more elaborate APIs, the explicit C approach is much more flexible and often preferable. It also gives you a bonus C interface that could be used from other programming languages than Python.