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'(anddtype=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-lessfoo→foo_) and uses Fortran conventions. - You declare
restype/argtypesyourself; scalars pass by reference (ctypes.byref). - Choose f2py for numerical Fortran with arrays you have the source for; ctypes/cffi for a
prebuilt
.so/.dllor 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
- 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, forintent(inout), get an error). - 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.