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 packagePython >= 3.10
meson,meson-python, andnumpy(installed automatically during the build when listed inbuild-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 usemeson-python.requireslists build-time dependencies.numpy >= 2.0is required so thatf2py, the NumPy headers, anddependency('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:
Uses
dependency('numpy')to locate NumPy headers, and adeclare_dependencyto add the F2PY include directory (forfortranobject.h).Runs
f2pyviacustom_targetto generate the C wrapper sources.Compiles the generated C code together with the Fortran source into a Python extension module using
py.extension_module.Installs
__init__.pyinto 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/buildvia PEP 517.The same build system used by NumPy and SciPy themselves.
Further reading#
SciPy’s meson build configuration (real-world F2PY usage)
Using via meson (raw meson build without
meson-python)Using via scikit-build (alternative using
scikit-build-core/ CMake)1 Migrating to meson (migration from
distutils)