Chapter 15 — Key Takeaways (Fortran-Python Interoperability)

A one-page reference for the two-language workflow: wrap Fortran with f2py, get NumPy arrays across the boundary, and measure the speedup. Keep it beside you the first few times you build an extension module.

The workflow in one picture

PYTHON: read config · set up arrays · loop · plot (matplotlib) · save · report
                                  |  the hot kernel only  |
FORTRAN: the loop that runs a billion times — the stencil, the solve, the sweep

Rule: whole-program speed is set by the hot loop's language. Write the 10% that runs a billion times in Fortran; write the 90% that runs once in Python.

f2py: the build and the call

Step Command / code Note
Compile-check the Fortran gfortran -std=f2018 -Wall -c kernel.f90 just validates; f2py does the real build
Build the extension module f2py -c -m NAME kernel.f90 NAME is what you import; on Py 3.12+ add --backend meson
Import and call (bare sub) import NAME; NAME.proc(...) procedure not in a Fortran module
Import and call (module sub) NAME.modname.proc(...) procedure inside Fortran module modname

intent shapes the Python signature

Fortran declaration f2py presents it as
intent(in) a Python argument
intent(out) a return value (removed from inputs)
intent(inout) an argument modified in place (needs F-contiguous, correct dtype)
!f2py intent(hide), depend(x) :: n = shape(x,0) drops n; infers it from x's shape

The intent-on-every-argument habit from Chapter 6 does double duty here: it is both the compiler's safety check and f2py's interface blueprint.

dtype must match, byte for byte

Fortran kind NumPy dtype
real(real64) / dp np.float64
real(real32) np.float32
integer(int32) np.int32
complex(real64) np.complex128

Trap: np.array([1, 2, 3]) is int64, not float64. Be explicit: np.array([...], dtype=np.float64).

The order='F' rule (the heart of the chapter)

Term Meaning NumPy check
C-contiguous row-major, last index fastest — NumPy default a.flags['C_CONTIGUOUS']
F-contiguous column-major, first index fastest — Fortran's layout np.isfortran(a)
  • f2py preserves your indexing: a[i, j]u(i+1, j+1). It does not transpose.
  • It reconciles the layout mismatch by copying a C-contiguous array on every call.
  • Fix once: create the array order='F' (and dtype=np.float64) and reuse it → zero-copy calls.
  • A 1-D array is both C- and F-contiguous, so 1-D never hits this.
  • intent(inout) + non-F-contiguous → f2py raises (it cannot write back through a copy).
u = np.zeros((n, n), dtype=np.float64, order='F')   # born F-contiguous
u = np.asfortranarray(u)                             # or convert (copies once)

ctypes / cffi: when f2py isn't the fit

  • Both call a prebuilt shared library through the C ABI, so the Fortran needs bind(c) (Chapter 14).
  • Without bind(c), gfortran mangles the name (module-less foofoo_) and uses Fortran conventions.
  • You declare restype/argtypes yourself; scalars pass by reference (ctypes.byref).
  • Choose f2py for numerical Fortran with arrays you have the source for; ctypes/cffi for a prebuilt .so/.dll or when avoiding a NumPy build step.

Honest benchmarking

  • Pure-Python element loops run ~10–100× slower than compiled Fortran — an order of magnitude, not a promise. Measure your own number.
  • The gap is interpreter overhead per element, not the arithmetic.
  • Always assert np.allclose(fast, slow) — a speedup with the wrong answer is a regression.
  • Time with time.perf_counter(); rigor (warm-up, repetition, variance) is Chapter 28.

Compile flags / commands introduced

Command Purpose
f2py -c -m NAME src.f90 build a Python extension module from Fortran
f2py -c --backend meson -m NAME src.f90 build on Python 3.12+ (needs meson, ninja)
gfortran -shared -fPIC src.f90 -o lib.so build a shared library for ctypes/cffi
.f2py_f2cmap file: {'real': {'dp': 'double'}} map a custom kind dp to C double for f2py

Project piece added this chapter

Wrapped the heat solver's step kernel with f2py (explicit-shape, dims hidden), drove the whole simulation loop from Python, and plotted the field with matplotlib — the f2py-speedup anchor climax. The Fortran kernel's signature is stable, so when the real physics arrives in Chapter 24 the Python driver keeps working. Save it as heat-solver/heat_kernel.f90.

The two things to memorize

  1. The boundary is a layout question. Match the dtype, create arrays order='F', reuse them — data crosses for free. Forget it and you copy on every call (or, for intent(inout), get an error).
  2. Hot 10% in Fortran, other 90% in Python. The payoff — 10-to-100× on the loop that dominates your runtime — is the whole reason the workflow exists.