If you're wrapping Fortran with f2py by hand — a reasonable decision, as established in our evaluations — this walkthrough covers the actual sequence for legacy code specifically, where the problems live. For a library you'd write today (free-form, module-based, explicit intents), the f2py documentation's own quickstart is better than anything we could paraphrase. This post is about machines that predate those assumptions.
Step 0: scope what you're wrapping
On a multi-hundred-deck legacy model, wrap a chosen surface: the routines a caller would actually want, and everything they transitively call. Establish that list before generating a single signature line.
Step 1: reconcile the source the compiler will see
Fixed-form F77 decks are typically composed with INCLUDE
statements and macros, so the file f2py parses is not the file
the compiler compiles. Expand the INCLUDEs into a generated copy
(never edit originals), resolve parameterized kind aliases
(real(dp) with a local integer, parameter ::
dp will otherwise fail f2py's wrapper in ways gfortran
reports far from the cause), and strip column-1 comment
conventions your shop added.
Step 2: decide directions you can't infer safely
Old code rarely declares intent. For each argument of each
exposed routine, decide: input-only, output-only, or in-place?
Record that decision either as f2py directives in the source
copy, or in a side manifest if you want the source untouched.
The silent-integer-string case is the classic: an assumed-length
CHARACTER*(*) argument can fall through to implicit
integer typing and bind as an int — a binding that compiles and
then hands a pointer where a string descriptor is expected.
Step 3: generate and build
Expect the build to name errors far from their causes when something in step 1 was wrong — the resolution is almost always back in the expansion, not the generated wrapper.
Step 4: test against known answers, immediately
As soon as the module imports, capture the outputs of known configurations and commit them. Every change after this — regenerating the wrapper, upgrading gfortran, changing flags — is measured against that baseline. Without it, step 5 is unfalsifiable.
Step 5: everything downstream
Packaging (scikit-build-core, wheel locks, CI builds), concurrency policy for COMMON-block state, and — if other teams will consume the model — HTTP service, container, manifests. Those are real weeks on your calendar, and they're the part nativegate generates so that days 2–30 don't repeat day 1.
The walkthrough makes the honest point explicitly: wrapping via f2py is a fine one-time exercise. The modernization is the work after the module compiles; generating that work is the tool.