Distributing F2PY extensions with meson-python#

The Using via meson page covers building F2PY extensions using raw meson commands. This page shows how to package those extensions into installable Python distributions (sdists and wheels) using meson-python as the PEP 517 build backend.

This is the recommended approach for distributing F2PY-wrapped Fortran code as a Python package on PyPI or for local pip install workflows.

Note

meson-python replaced setuptools / numpy.distutils as the standard way to build and distribute compiled extensions in the NumPy and SciPy ecosystem. See Status of numpy.distutils and migration advice for background.

Prerequisites#

You need:

  • A C compiler

  • A Fortran compiler (gfortran, ifort, ifx, flang-new, etc.), if you use any Fortran code in your package

  • Python >= 3.10

  • meson, meson-python, and numpy (installed automatically during the build when listed in build-system.requires)

Minimal example#

The project below wraps a Fortran fib subroutine into an importable Python package called fib_wrapper.

Project layout:

fib_wrapper/           # project root
├── fib.f90            # Fortran source
├── fib_wrapper/       # Python package directory
│   └── __init__.py
├── meson.build
└── pyproject.toml

Fortran source#

Save the following as fib.f90:

subroutine fib(a, n)
  use iso_c_binding
  integer(c_int), intent(in) :: n
  integer(c_int), intent(out) :: a(n)
  integer :: i
  do i = 1, n
     if (i == 1) then
        a(i) = 0
     else if (i == 2) then
        a(i) = 1
     else
        a(i) = a(i - 1) + a(i - 2)
     end if
  end do
end subroutine fib

pyproject.toml#

[build-system]
# numpy>=2.0 is required for dependency('numpy') support in meson.build
requires = ["meson-python>=0.15.0", "numpy>=2.0"]
build-backend = "mesonpy"

[project]
name = "fib_wrapper"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["numpy"]

Two entries matter here:

  • build-backend = "mesonpy" tells build frontends to use meson-python.

  • requires lists build-time dependencies. numpy >= 2.0 is required so that f2py, the NumPy headers, and dependency('numpy') support in Meson are available during compilation.

meson.build#

project('fib_wrapper', 'c',
  version : '0.1.0',
  meson_version: '>=1.1.0',
  default_options : ['warning_level=2'],
)

add_languages('fortran', native: false)

py = import('python').find_installation(pure: false)

# NumPy >=2.0 provides include dirs via dependency()
np_dep = dependency('numpy')

incdir_f2py = run_command(py,
  ['-c', 'import numpy.f2py; print(numpy.f2py.get_include())'],
  check : true
).stdout().strip()

# f2py include dir (for fortranobject.h) is not in dependency('numpy'),
# so add it separately
f2py_dep = declare_dependency(
  include_directories : incdir_f2py,
)

# Generate the f2py wrappers
fib_source = custom_target('fibmodule.c',
  input : ['fib.f90'],
  output : ['fibmodule.c', 'fib-f2pywrappers.f'],
  command : [py, '-m', 'numpy.f2py', '@INPUT@', '-m', 'fib', '--lower']
)

py.extension_module('fib',
  ['fib.f90', fib_source, incdir_f2py / 'fortranobject.c'],
  dependencies : [np_dep, f2py_dep],
  subdir: 'fib_wrapper',
  install : true,
)

# Install the Python package files
py.install_sources(
  'fib_wrapper/__init__.py',
  subdir: 'fib_wrapper',
)

Note

The file is stored as meson_mesonpy.build in the documentation source tree to avoid collisions with other examples. In your project, name it meson.build.

The meson.build file does four things:

  1. Uses dependency('numpy') to locate NumPy headers, and a declare_dependency to add the F2PY include directory (for fortranobject.h).

  2. Runs f2py via custom_target to generate the C wrapper sources.

  3. Compiles the generated C code together with the Fortran source into a Python extension module using py.extension_module.

  4. Installs __init__.py into the package directory so the result is a proper Python package.

The subdir: 'fib_wrapper' argument on the extension module is required so that the compiled fib shared library is installed inside the fib_wrapper/ package directory, next to __init__.py. Without it the extension would be installed at the top level and import fib_wrapper would not find the fib extension. The resulting installed layout is:

site-packages/
└── fib_wrapper/
    ├── __init__.py        # from .fib import fib
    └── fib.cpython-*.so   # compiled extension module

__init__.py#

A minimal __init__.py re-exports the wrapped function:

from .fib import fib

Building and installing#

Editable install (development)#

pip install --no-build-isolation --editable .

--no-build-isolation reuses the current environment, which is useful when iterating. This requires meson-python, meson, ninja, and numpy to already be installed.

Building a wheel#

# If you don't yet have `pypa/build` installed: `pip install build`
python -m build --wheel

The resulting .whl file in dist/ can be uploaded to PyPI, or installed elsewhere with pip install dist/fib_wrapper-0.1.0-*.whl.

Verifying the install#

>>> from fib_wrapper import fib
>>> fib(10)
array([ 0,  1,  1,  2,  3,  5,  8, 13, 21, 34], dtype=int32)

Customizing the Fortran compiler#

meson-python delegates compiler selection to meson. By default, meson will choose the first Fortran compiler it finds on the PATH. If you want more control over Fortran compiler selection, set the FC environment variable before building:

FC=ifx python -m build --wheel

For more control, use a Meson native file:

; native.ini
[binaries]
fortran = 'ifx'
c = 'icx'
python -m build --wheel -Csetup-args="--native-file=native.ini"

Adding dependencies (BLAS, LAPACK, etc.)#

Use dependency() in meson.build to link against system libraries:

lapack_dep = dependency('lapack')

py.extension_module('mymod',
  [sources, generated, incdir_f2py / 'fortranobject.c'],
  dependencies : [np_dep, f2py_dep, lapack_dep],
  install : true,
)

meson resolves dependencies through pkg-config, CMake, or its own detection logic. See the Meson dependency documentation for details.

Differences from the scikit-build-core workflow#

The scikit-build-core approach documented in Using via scikit-build uses CMake under the hood. meson-python provides:

  • Native Fortran compiler support in meson (no CMake layer).

  • Direct integration with pip / build via PEP 517.

  • The same build system used by NumPy and SciPy themselves.

Further reading#