Skip to content

Reject sequences nested in a sequence of a different type in approx (closes #11945) - #14860

Open
Kayvan-Zahiri wants to merge 1 commit into
pytest-dev:mainfrom
Kayvan-Zahiri:fix/approx-mixed-nested-sequences
Open

Reject sequences nested in a sequence of a different type in approx (closes #11945)#14860
Kayvan-Zahiri wants to merge 1 commit into
pytest-dev:mainfrom
Kayvan-Zahiri:fix/approx-mixed-nested-sequences

Conversation

@Kayvan-Zahiri

Copy link
Copy Markdown

Closes #11945.

The bug

ApproxSequenceLike.__init__ rejects nesting with:

if isinstance(x, type(expected)):

That only catches an element of the same type as its container. A tuple inside a
list, or a list inside a tuple, passes the guard, reaches ApproxScalar, and is
compared with ==. So the comparison is exact, not approximate, and no error is
raised:

>>> [[1.20000000000001]] == approx([[1.2]])          # same type
TypeError: pytest.approx() does not support nested data structures
>>> (1.20000000000001,) == approx((1.2,))            # flat
True
>>> [(1.20000000000001,)] == approx([(1.2,)])        # tuple in list
False
>>> ([1.20000000000001],) == approx(([1.2],))        # list in tuple
False

The identical-value case is the quieter half of it, because it looks like it works:

>>> [(1.2,)] == approx([(1.2,)])
True
>>> ([1.2],) == approx(([1.2],))
True

Those return True from an exact comparison, so the tolerance is silently ignored.
A test written that way passes for the wrong reason and starts failing the moment a
value drifts by one ulp.

@crazymerlyn localised this in the issue.

The fix

Use _is_sequence_like, the predicate approx() itself uses to decide whether to
dispatch to ApproxSequenceLike. The guard and the dispatcher then agree on what a
sequence is.

Mappings are excluded deliberately. _is_sequence_like is true for dict, since a
dict has __getitem__ and __len__, so using it unqualified would newly reject
[{"x": 1.0}] == approx([{"x": 1.0}]), which works today. approx() avoids that by
checking Mapping before _is_sequence_like; the guard needs the same exclusion.
A dict nested in a sequence still gets the old exact comparison, and I have left
that alone since it is outside this issue. Happy to address it here or separately
if you would like it changed.

Behaviour change worth flagging

[np.array([1.0, 2.0])] == approx([np.array([1.0, 2.0])]) raised ValueError
("truth value of an array is ambiguous") on main and now raises TypeError with
the nested-structure message. Both are errors, and the new one names the actual
problem, but it is a different exception type.

Everything else I checked is unchanged: flat lists and tuples, ["string"],
[{"x": 1.0}], a top-level numpy array, and same-type nesting.

Tests

Two cases added to the existing test_expected_value_type_error parametrisation
(tuple-in-list, list-in-tuple), plus test_nested_mixed_sequence_not_silently_compared
which pins that these raise rather than returning a bool.

All four fail on main; the two pre-existing parametrisations pass either way.
testing/python/approx.py: 140 passed.

ApproxSequenceLike guarded against nesting with isinstance(x, type(expected)),
which only caught an element of the same type as the container. A tuple inside a
list, or a list inside a tuple, passed the guard and was handed to ApproxScalar,
where it was compared exactly instead of approximately. That returned a wrong
bool rather than raising, so [(1.20000000000001,)] == approx([(1.2,)]) was False
while the flat (1.20000000000001,) == approx((1.2,)) was True.

Use _is_sequence_like, the same predicate approx() uses to dispatch to
ApproxSequenceLike, so nesting is rejected consistently. Mappings are excluded
so that a dict nested in a sequence keeps its current behaviour.

Closes pytest-dev#11945
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Aug 11, 2026
@Kayvan-Zahiri

Copy link
Copy Markdown
Author

One thing I should have put in the description: I used AI tooling while working
on this, so flagging it against your AI/LLM policy.

How it was used: to help navigate python_api.py and draft the fix and tests.
What it was not: I found and characterised the bug myself, decided that
isinstance(x, type(expected)) is the wrong check rather than patching the
symptom, and verified every case in the description by running it. I can answer
questions about any line of it.

I have left the Co-authored-by trailer off deliberately rather than by
oversight, since the work was directed and reviewed by me throughout rather
than produced by an unattended agent. Happy to add it if you would rather the
checklist item be satisfied literally.

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

Labels

bot:chronographer:provided (automation) changelog entry is part of PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

pytest.approx() incorrectly flags nested sequences as unequal

1 participant