Skip to content

fix: raise on any nested container in approx(), not only same-type - #14934

Merged
Zac-HD merged 1 commit into
pytest-dev:mainfrom
onk3sh:fix/approx-mixed-nested
Aug 25, 2026
Merged

fix: raise on any nested container in approx(), not only same-type#14934
Zac-HD merged 1 commit into
pytest-dev:mainfrom
onk3sh:fix/approx-mixed-nested

Conversation

@onk3sh

@onk3sh onk3sh commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Closes #10210

What is wrong

approx() refuses to descend into a nested container, and says so clearly — but only when that container happens to be the same type as the one holding it. Both guards test against the parent's type:

  • src/_pytest/approx.py:263ApproxMapping.__init__: if isinstance(value, type(expected)):
  • src/_pytest/approx.py:361ApproxSequenceLike.__init__: if isinstance(x, type(expected)):

A dict inside a list, a list inside a dict, a tuple inside a list, a set inside a tuple — none of those match, so they slip past and are compared with == as leaf values. That comparison is exact, so the tolerance is silently ignored:

>>> [{"a": 0.1 + 1e-9}] == approx([{"a": 0.1}])   # inside default tolerance
False
>>> [{"a": 0.1}] == approx([{"a": 0.1}])          # only exact equality passes
True
>>> [[0.1 + 1e-9]] == approx([[0.1]])             # same shape, same-type nesting
TypeError: pytest.approx() does not support nested data structures: [0.1] at index 0

Passing rel= explicitly produces a third behaviour — an error from a lower layer that does not mention nesting at all:

>>> [{"a": 0.11}] == approx([{"a": 0.1}], rel=1e-1)
TypeError: expected value must support abs(...) when relative tolerance is used, got dict

Reported in #10210 in 2022 and still reproducing on main.

The change

Ask whether the value is a container at all, rather than whether it matches the parent's type:

def _is_nested_container(value: object) -> bool:
    return isinstance(value, Collection) and not isinstance(value, str | bytes | bytearray)

str, bytes and bytearray are Collections too, but approx treats them as leaves on purpose and compares them exactly — test_nonnumeric_okay_if_equal covers that, and this PR extends it with bytes cases so the exclusion is pinned by a test rather than by the implementation.

Both existing error messages are untouched, so a nested dict in a list now reports "does not support nested data structures" (the sequence wording, from the container that actually holds it) and a list in a dict reports "does not support nested dictionaries".

#10215 attempted this in 2022 and was closed as stale during the 2026 sprint, with @Zac-HD noting "we'd be delighted to accept a fresh version of this patch". That patch only touched ApproxSequenceLike, so approx({"a": [1.0]}) would still have been silently wrong; this one covers both sides.

Behaviour change worth flagging

A numpy array nested inside a list or dict now raises instead of returning a result. That case was already broken — [np.array([1.0])] == approx([np.array([1.0 + 1e-9])]) returned False before this PR — so this converts a silent wrong answer into a clear error, but it is a visible change for anyone relying on the False. A top-level array is unaffected: ApproxNumpy handles it and does its own nesting.

Tests

testing/python/approx.py:

  • extended test_expected_value_type_error with every cross-kind combination — list-of-tuple, list-of-set, list-of-dict, tuple-of-dict, dict-of-list, dict-of-tuple, dict-of-set
  • added test_mixed_nested_containers_raise_instead_of_comparing_unequal, which pins the actual bug: values well inside the default tolerance used to compare False instead of raising
  • extended test_nonnumeric_okay_if_equal with bytes leaves

Before the change 11 of these fail; after, testing/python/approx.py is 149 passed. Full suite: 4483 passed, 51 skipped, 13 xfailed, 7 xpassed. ruff 0.16.3 check and format clean, mypy 2.3.1 clean on both touched files.

Checklist

  • Include new tests or update existing tests when applicable.
  • Allow maintainers to push and squash when merging my commits.
  • closes #10210 in the PR description and the commit.
  • Changelog file changelog/10210.bugfix.rst.
  • Added myself to AUTHORS in alphabetical order.
  • AI assistance is credited in a Co-authored-by trailer.

approx() rejects a nested container only when it is the same type as its
parent, so approx([[1]]) and approx({"k": {"k": 1}}) raise a clear
TypeError while approx([{"k": 1}]) and approx({"k": [1]}) are accepted.
The inner container is then compared exactly, so the values are unequal
however close they are and the tolerance is silently ignored.

Replace both isinstance(value, type(expected)) guards with a check for
any Collection other than str/bytes/bytearray, which compare exactly and
are already handled as leaves.

Closes pytest-dev#10210

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@psf-chronographer psf-chronographer Bot added the bot:chronographer:provided (automation) changelog entry is part of PR label Aug 25, 2026

@Zac-HD Zac-HD 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.

Thanks, Onkesh!

@Zac-HD
Zac-HD merged commit b0de735 into pytest-dev:main Aug 25, 2026
36 checks passed
onk3sh pushed a commit to onk3sh/onk3sh.github.io that referenced this pull request Aug 25, 2026
Only contributions actually merged upstream are listed. Two qualify:
pytest-dev/pytest#14934, which fixes approx() silently ignoring its
tolerance for a container nested inside a different container type, and
mightymoose/fortymm--#341, which rewrites the Devise mailer templates
for an open-source table tennis league platform.

Contributions that are open, superseded, or closed pending a linked
issue are deliberately left off. Listing work that has not landed
invites the reader to check, and what they find is weaker than the
claim.

The entries live in projects.ts alongside everything else, under a new
`open-source` projectType, so they get a page under /post/ and appear
in the /post/ listing without any parallel data path. Project gains an
optional `link` field; [slug].astro uses it for the outbound link so
each page points at its own merged PR instead of the GitHub profile.
Existing entries have no `link` and fall back to the previous target,
so nothing else changes.

The homepage section sits below Writing and reuses the .grid-3 + .card
pattern. No new nav item.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
onk3sh added a commit to onk3sh/onk3sh.github.io that referenced this pull request Aug 26, 2026
Content and structure cleanup across the site.

- Moved a project page to a descriptive slug and left a noindex redirect
  stub at the old URL so existing links keep resolving.
- Dropped an outbound repository link that no longer pointed anywhere
  useful, along with the string interpolation that built it.
- Corrected two legacy pages under post/ that Astro no longer builds.
  A rebuild never overwrites them, so edits under src/ never reach them.
- Removed data/terminal-commands.json and src/data/now.ts. Neither was
  referenced by any source file or any built page, yet the JSON was
  still being published at /data/terminal-commands.json carrying a
  stale bio and a project that does not exist anywhere on the site.
- The terminal's `lab` command advertised two demos that are not in
  /lab/. It points at the real ones now.
- Standardized spelling: -ise/-isation to -ize/-ization. Two spellings
  of the same word had been in use for the same concept.
- The About page had no <h1>, starting at <h2> while every other page
  had exactly one. Promoted the headline and moved the style hook with
  it, so the rendering is unchanged.

Adds an Open Source section to the homepage, below Writing, reusing the
existing .grid-3 + .card pattern. No new nav item. It lists only
contributions that merged upstream: pytest-dev/pytest#14934, which
fixes approx() silently ignoring its tolerance for a container nested
inside a different container type, and mightymoose/fortymm--#341, which
rewrites the Devise mailer templates for an open-source table tennis
league platform.

The entries live in projects.ts under a new `open-source` projectType,
so each gets a page under /post/ and appears in the /post/ listing with
no parallel data path. Project gains an optional `link` field, and
[slug].astro uses it for the outbound link so each page points at its
own merged PR. Existing entries have no `link` and fall back to the
previous target.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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 does not raise in mixed data type nested structures

2 participants