Audit of the f2py-removal branch (46 commits, branched off main at 986b8b3035, Mar 2026),
asking: which of the problems found during that effort are defects in the existing wrappers and
surrounding scipy code — i.e. still present in main today and never upstreamed?
Sources: all 46 commit messages/bodies, f2py_removal_plan.md, f2py_removal_session_summary.md,
the diff against the merge base (the diff against current main is misleading — main has moved on
by ~5 months), and verification of each candidate against current main sources. Nothing was
executed; all findings are from source reading.
The branch is not a repository of unreported f2py-wrapper bugs. 42 of its 46 commits fix defects
in the new pyf parser / Cython generator built for the experiment: missing pre-call pivot
increments, Python-3 b'V'[0] bytes indexing, the lwork == -1 sentinel clash, _lwork
scalar/array confusion, callback arity, output ordering, and so on. Every segfault chased during that
effort — integrate Radau/BDF, lobpcg, test_batch, gejsv, cheevd — root-caused to the new
generator, not to the .pyf.src specs.
What genuinely belongs to main is the short list in sections A and B below. Sections C–E record
constraints, disproven suspicions, and loose ends, mostly so they aren't re-investigated.
Scope note: main no longer builds BLAS with f2py — fblas*.pyf.src is gone and _fblas /
_fblas_64 are C++ (scipy/linalg/src/blas/*.cpp). LAPACK is still f2py (flapack*.pyf.src plus
flapack_64.pyf.src), so only the LAPACK-side findings are live.
scipy/linalg/flapack_gen_tri.pyf.src:136 and :173:
<ftype2> intnet(in), dimension(n) :: d ! line 136 (real gtsvx)
<ftype2c> intnet(in), dimension(n) :: d ! line 173 (complex gtsvx)intnet is not an f2py attribute; crackfortran drops it silently, so d falls back to f2py's
default (input) intent and observable behaviour is currently unchanged. Still worth fixing: the
declared intent is not the applied one, and this is exactly the failure mode that bit the
reimplementation (arrays with no effective intent got no dtype conversion, so garbage was passed to
LAPACK). These are the only two attribute typos in ~1770 declarations across all .pyf.src files
(verified by an attribute-token scan of every declaration line).
flapack_gen_tri.pyf.src:146and:183—integer intent(hide) :: ldx = MAX(1, shape(b, 0))has nodepend(b), unlike the adjacentldb, which does. f2py infers the dependency; a non-f2py generator does not, which is what forced the branch's "defer hidden args that reference output array shapes" workaround.flapack_other.pyf.src:27(gejsv) —depend(a), check(m>=n), depend(n):depend()twice.flapack_gen_banded.pyf.src:124and:163— same duplicate-dependpattern.flapack_other.pyf.src:2239—integer intent(in,hide) :: ldc = max(1,shape(c,0)), nodepend(c).
scipy/linalg/_decomp_cholesky.py:210-212:
batch_shape = a.shape[:-2]
ret_lower = np.tile(lower, reps=batch_shape)np.tile always returns an ndarray, so for ordinary unbatched input cho_factor(A)[1] is
array(False) rather than False, contradicting the documented (array, bool) return. Low impact,
trivial fix (np.tile(...) if batch_shape else lower).
scipy/linalg/_decomp_cholesky.py:285-288. In batch mode cho_factor returns lower tiled to
batch_shape. _cho_solve is decorated with @_apply_over_batch(('c', 2), ('b', '1|2')), so
lower is an "other" argument and the entire batch-shaped boolean array — not a per-slice
scalar — is handed to potrs(..., lower=lower) for every slice.
This works only because f2py's scalar converter silently accepts an ndarray where an integer is
expected. Any non-f2py wrapper rejects it; on the branch it broke 12 cho_solve batch tests and had
to be papered over with bool(np.asarray(lower).flat[0]) inside _cho_solve.
The cleaner fix, identified during that session but never implemented, is in the batch framework:
inherently-scalar flags (lower, trans, …) should stay scalar instead of being broadcast to the
batch shape. This is the one item here that materially blocks moving the LAPACK wrappers off
f2py.
Each of these cost real debugging time and will resurface in any replacement:
- Silent ndarray → int-scalar coercion for scalar flags — see B2.
- Read-only inputs to in-place post-processing.
_apply_over_batchpassesbroadcast_toviews; f2py works on its own copies, somainis fine, but a wrapper that decrements pivot arrays in place hits read-only buffers. - 0-D and 1-D inputs auto-reshaped for 2-D parameters.
scipy/linalg/tests/test_lapack.py:1954callsgejsv(1.)with a scalar and assertsu.shape == (1, 1); other tests pass a 1-Dband expect a 1-D result back. Both are f2py conveniences that the tests encode as API contract. check()directives are load-bearing for the test suite. ~36test_fblastests (test_x_bad_size,test_stride_assert, …) assert that invalid input raises, originally with f2py's module-levelerrortype.mainhas relaxed these toValueErroras part of the C++ BLAS port; the LAPACK half still depends on f2py-generated checks.- Wrappers used as class attributes.
test_fblas.pydoesblas_func = fblas.saxpyat class scope. f2py fortran objects and CPyCFunctions don't implement the descriptor protocol, but Cythondeffunctions do and silently become bound methods (selflands in the first argument). Any Cython-based replacement needsstaticmethod()in that file.
sspr2/dspr2/chpr2/zhpr2andstbsv"missing from the pyf files." They are present —fblas_l2.pyf.src:317,subroutine <prefix><s,s,h,h>pr2. The branch's parser lost them because template expansion emitsend subroutine<prefix>foowith no space and the regex used\b.- Four
cython_blasutility routines "missing from f2py." Confirmed never exposed by f2py either. sgttrsargument order. An attempted reorder (bbeforeipiv) was wrong and was reverted; the pyf order matches Fortran.tzrzf_lworkrequiresm <= n;tgsen_lwork/trsen_lwork"need arrays". These came from the branch's own generic_lworksmoke calls, not from scipy's tests —test_lapack.py:1122and:1198correctly usem, n = 10, 15. Not a scipy defect.
test_gttrf_gttrssegfaults under pytest on the branch while passing in a direct interpreter; concluded to be latent corruption from some other generated wrapper. Never root-caused.- ILP64 variants (
_pyblas_64/_pylapack_64) were never implemented. _cython_signature_generator.pystill importsnumpy.f2py.crackfortran(dev-only tool, also true inmain).
intnet→intentinflapack_gen_tri.pyf.src:136,173. One-liner, no behaviour change.cho_factorscalarlower(B1), plus a test asserting an unbatchedcho_factor(A)[1]is a Python bool.- Batch framework scalar passthrough (B2) — the substantive one. Worth doing on its own merits, and a prerequisite for taking the LAPACK wrappers off f2py.
- Optionally fold the A2 hygiene items into the same
.pyf.srccleanup.
Verification for any of the above: pixi run build, then
pixi run -e test spin python --no-build -- -m pytest scipy/linalg/tests/test_lapack.py -k gtsvx
and scipy/linalg/tests/test_batch.py -k cho.