Skip to content

BUG: fix nanmax/nanmin initial argument misbehaving in slow path - #31102

Open
joshuaswanson wants to merge 4 commits into
numpy:mainfrom
joshuaswanson:fix/nanmax-initial
Open

joshuaswanson wants to merge 4 commits into
numpy:mainfrom
joshuaswanson:fix/nanmax-initial

Conversation

@joshuaswanson

Copy link
Copy Markdown
Contributor

Fixes #31087.

nanmin/nanmax misbehave with the initial argument in the slow path (non-ndarray input like lists). Two issues:

  1. nanmax([1.0], initial=nan) returns nan because the slow path uses np.amax which propagates NaN from initial, unlike np.fmax.reduce used in the fast path. Fixed by stripping NaN initial before passing to amax/amin, since nanmax/nanmin should ignore NaN in initial the same way they ignore NaN in array elements.

  2. nanmax([], initial=1.0) returns nan because the all-NaN check (np.all(mask)) returns True on an empty mask and unconditionally overwrites the result with NaN. Fixed by using initial as the result for all-NaN slices when it's a valid (non-NaN) value.

@ngoldbaum ngoldbaum modified the milestone: 2.4.5 release Apr 1, 2026
@ngoldbaum

Copy link
Copy Markdown
Member

I'm going to try to look at this before 2.6 is branched. Thanks for the contribution, sorry it got lost in the churn.

@charris

charris commented May 15, 2026

Copy link
Copy Markdown
Member

@ngoldbaum ping

@charris charris added the 09 - Backport-Candidate PRs tagged should be backported label May 15, 2026
@charris charris modified the milestones: 2.4.5 release, 2.5.0 Release May 15, 2026
@ngoldbaum

Copy link
Copy Markdown
Member

I don't think this is the right fix.

First and foremost, this introduced a regression for one of the cases from #31087. Before the PR, np.nanmin([], initial=np.nan) triggers a RuntimeWarning for me. After applying the patch from the PR to current main, I get the following exception instead:

Traceback (most recent call last):
  File "<string>", line 1, in <module>
    import numpy as np; np.nanmin([], initial=np.nan)
                        ~~~~~~~~~^^^^^^^^^^^^^^^^^^^^
  File "/Users/goldbaum/Documents/numpy/build-install/usr/lib/python3.15/site-packages/numpy/lib/_nanfunctions_impl.py", line 368, in nanmin
    res = np.amin(a, axis=axis, out=out, **kwargs)
  File "/Users/goldbaum/Documents/numpy/build-install/usr/lib/python3.15/site-packages/numpy/_core/fromnumeric.py", line 3302, in amin
    return _wrapreduction(a, np.minimum, 'min', axis, None, out,
                          keepdims=keepdims, initial=initial, where=where)
  File "/Users/goldbaum/Documents/numpy/build-install/usr/lib/python3.15/site-packages/numpy/_core/fromnumeric.py", line 83, in _wrapreduction
    return ufunc.reduce(obj, axis, dtype, out, **passkwargs)
           ~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
ValueError: zero-size array to reduction operation minimum which has no identity

The same problem exists for where= masks.

The bug isn't that NaN is present in the keywords dictionary, it's about NaN propagation. I think instead you want to detect that initial is NaN using e.g. np.isnan and then replace NaN with infinity in the keywords dict.

@joshuaswanson

Copy link
Copy Markdown
Contributor Author

Thanks for the review. You're right. I've pushed a commit that replaces NaN initial with the operation's identity (+inf for nanmin, -inf for nanmax) and keeps the original RuntimeWarning + NaN behavior for the all-NaN path. Added tests covering both the empty-input case and where= masks that exclude everything.

@ngoldbaum

ngoldbaum commented May 18, 2026 •

Copy link
Copy Markdown
Member

Overall much better.

Minor comment: please rebase on or merge with current main.

Two big comments:

  • This is a (minor) behavior change and needs a release note documenting it as such. See doc/release/upcoming_changes/README.rst for instructions.

In particular, the following before this PR:

>>> np.nanmax([np.nan, np.nan], initial=5.0)
<python-input-1>:1: RuntimeWarning: All-NaN axis encountered
np.float64(nan)

And after this PR:

>>> np.nanmax([np.nan, np.nan], initial=5.0)
np.float64(5.0)

IMO this is a correct, useful change, but it bears noting.

  • Please add a test for this case (a multi-element all-NaN argument with non-NaN initial) along with a release note, you're currently not testing the multi-element case of this pattern.

I would also add a test that a NaN initial is ignored for non-NaN input:

assert np.nanmax([1.0, 2.0, 3.0] initial=np.nan) == 3.0
assert np.nanmom([1.0, 2.0, 3.0] initial=np.nan) == 1.0

@ngoldbaum ngoldbaum left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thanks!

@seberg @charris are you two OK with this going in for 2.5.0? I don't think it should be backported.

Comment thread numpy/lib/_nanfunctions_impl.py Outdated
# the same way it ignores NaN in array elements.
initial_is_nan = (
initial is not np._NoValue
and np.ndim(initial) == 0

@seberg seberg May 20, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmmm, I don't love np.ndim (it converts to an array on the way just to check that this is a scalar). initial must be a scalar, and maybe it is OK if

There are a couple of subtle things, that I am a bit unsure about whether we should address them here:

  • np.isnan(...) will actually currently fail for things like decimal.Decimal(0) coming in, which isn't an unreasonable initial value.
  • That this path is taken at all for list inputs is a bit grotesque... The input is a list, which is converted to an array. This isn't some subclass that needs special handling.
    (The problem with this is, whether we can just reasonably switch to a array_converter/__array_wrap__ pattern to begin with?! This would side-step the .max() dispatching though...)
  • Existing issue: I am actually unhappy about the warning being issued. The warning is a bit dubious either way, but we shouldn't issue a warning at all (on either path) is passed whether NaN is returned or not.
    The reason is that we should issue a warning only when the normal reduction would fail after removing all NaNs, but np.fmin.reduce([], initial=np.nan) succeeds. (Maybe slightly debatable due to the NaN result, but I'd argue the user asked for it explicitly, so a warning makes no sense.)

In some sense these don't matter, but it does make this feel a bit like a band-aid and the decimal.Decimal(0) input failing seems like a problem.

@joshuaswanson

Copy link
Copy Markdown
Contributor Author

Switched to the initial != initial self-inequality check and added a test for that path.

@charris charris modified the milestones: 2.5.0 Release, 2.6.0 Release May 24, 2026
@ngoldbaum

Copy link
Copy Markdown
Member

@seberg would you mind giving this another look when you have a chance?

@seberg seberg left a comment •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is an OK fix now, thanks. We still have two annoying things that would be nice to clean up, but while to me they seem like bigger issues than this one, they are distinct.

  1. It isn't really forgivable that we call asanyarray() unnecessarily late and are hitting the slow path at all here for non-object input.
  2. The warning is wrong on both paths, it should only be given if initial is np._NoValue or initial is None.

@charris charris removed the 09 - Backport-Candidate PRs tagged should be backported label Jul 27, 2026

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

BUG: nanmax/nanmin, 'initial' argument misbehaves

4 participants