"Fortran to Python" sounds like a compiler technology: feed in a deck, get Python source out. That product doesn't exist, and it mostly shouldn't — translating numerical code into another language is how subtle numeric differences and lost validation happen. What actually exists, and works well, is interop: Python calling the same compiled Fortran it always did. This guide is about that, honestly: what the existing tools give you, and where the unglamorous work actually sits.
The boundary layer is mostly solved
NumPy's f2py has connected Fortran and Python for decades and is still actively maintained; it parses Fortran and emits a Python extension module. f90wrap builds on it to handle modular Fortran, including derived types and type-bound procedures. A third path is hand-written ISO_C_BINDING shims plus ctypes, which some teams prefer for full control. If your problem is "one routine, well described, called once," any of these gets you there this week.
Where the work actually piles up
On a real legacy codebase — ours is a black-oil PVT model: seven fixed-form F77 decks under an F90 facade — the boundary layer is a few days. Everything around it is the project:
- Intent is inferred, not declared. Old decks rarely say INTENT(IN). Every argument is potentially in, out, or both; getting that wrong produces silent garbage, not errors. Someone (or a tool) has to figure out the direction of every array.
- COMMON blocks are global state. Two exposed routines mutating the same COMMON block means your Python calls are order-dependent. Interop tools don't fix that; the service contract has to account for it (nativegate's generated Fortran services hold one call at a time per process for exactly this reason).
- Build and packaging. A binding that builds on your machine is not a package. You need CMake or meson orchestration, scikit-build-core or setuptools glue, an installable wheel, and a story for gfortran on every machine you build on.
-
Evidence that the numbers did not move. The
moment you re-plan any compilation, someone will reasonably ask
whether the physics changed. A recorded baseline and a
verifythat compares against it answer that indefinitely. - Publishing beyond your team. And if people outside your team need the model over HTTP, you're into codegen for a FastAPI service, Docker image, Kubernetes manifests, health probes, auth. Each is small; the sum is weeks.
What a full pipeline looks like
nativegate exists because those five work-items are the same every time; it generates them from one configuration. The committed example in the repository is the petroleum service:
One configuration (nativegate.yaml) declares which
routines to expose; everything listed above that it can generate,
it does generate. What it does not generate it refuses with a
stated reason. And the generated artifacts stand on their own:
the wheel imports without depending on the generator, and the
service runs as an ordinary FastAPI app.
Practical advice, whatever tool you pick
- Expose a chosen surface — ten routines — not the whole file.
- Record a numerical baseline before the first binding change, and commit it.
- Treat COMMON-block state as part of the API contract, not an implementation detail.
- Make the wheel build in CI, not just on your laptop.
- Wait for the second user before building the HTTP service.
If you only remember one line from this post: the hard part of
Fortran-to-Python was never the
f2py -c command. It's the work afterwards, and that
work is what nativegate generates.