Skip to content

Fix ErrorTree.__getitem__ mutating the tree on error-free access - #1551

Closed
agu2347 wants to merge 1 commit into
python-jsonschema:mainfrom
agu2347:fix-errortree-getitem-mutation
Closed

Fix ErrorTree.__getitem__ mutating the tree on error-free access#1551
agu2347 wants to merge 1 commit into
python-jsonschema:mainfrom
agu2347:fix-errortree-getitem-mutation

Conversation

@agu2347

@agu2347 agu2347 commented Aug 16, 2026

Copy link
Copy Markdown

Summary

ErrorTree.__contains__/__iter__ change their results after a read-only ErrorTree.__getitem__ access on an index that had no errors, contradicting the documented contract ("Check whether instance[index] has any errors").

from jsonschema import Draft202012Validator
from jsonschema.exceptions import ErrorTree

schema = {
    "type" : "array",
    "items" : {"type" : "number", "enum" : [1, 2, 3]},
    "minItems" : 3,
}
instance = ["spam", 2]

v = Draft202012Validator(schema)
tree = ErrorTree(v.iter_errors(instance))

list(tree)      # [0]      -- correct
1 in tree        # False    -- correct

tree[1]           # accessing the error-free index 1

list(tree)       # [0, 1]   -- wrong, 1 has no errors
1 in tree        # True     -- wrong

Root cause

ErrorTree._contents is a defaultdict(ErrorTree). __getitem__ did return self._contents[index] directly -- and on a defaultdict, merely reading a missing key inserts it. So a read-only lookup for an error-free (but otherwise valid) index permanently adds that index to _contents, which is exactly what __contains__/__iter__/total_errors check.

Fix

__init__ (tree construction) is the only place that should auto-vivify entries, since it's recording real error paths -- so it now populates _contents directly (container._contents[element]) instead of going through the public __getitem__.

__getitem__ no longer touches _contents for a miss: it validates the index against self._instance (still propagating LookupError for genuinely invalid indices, per the existing docstring/tests) and returns a fresh, unstored ErrorTree() for an index with no recorded errors, so _contents -- and therefore __contains__/__iter__/total_errors/__len__/__repr__, which all read _contents -- stay accurate.

Testing

  • Added test_getitem_of_an_error_free_index_does_not_mutate_the_tree to jsonschema/tests/test_exceptions.py, reproducing the exact scenario from the issue and asserting list(tree)/in/total_errors are unchanged before and after accessing the error-free index.
  • Full suite: jsonschema/tests/: 8281 passed, 232 skipped (unrelated/environment-gated), 0 failed.
  • Reverting only jsonschema/exceptions.py (keeping the new test) reproduces the exact reported bug: AssertionError: Lists differ: [0, 1] != [0].
  • ruff check clean on both changed files.

Fixes #1328

`ErrorTree._contents` is a `defaultdict(ErrorTree)`. `__getitem__`
accessed `self._contents[index]` directly, and merely *reading* a
missing key on a defaultdict inserts it. So calling `tree[index]` for
an index that had no validation errors of its own silently added that
index to `_contents`, which is exactly what `__contains__` and
`__iter__` check -- causing `index in tree` and `list(tree)` to change
after a read-only lookup, contradicting the documented "Check whether
instance[index] has any errors" contract.

Populate `_contents` directly during tree construction in `__init__`
(where auto-vivification is genuinely wanted, since we're recording
real error paths), and make `__getitem__` return a fresh, unstored
`ErrorTree()` for indices with no recorded errors instead of touching
`_contents`. `total_errors`/`__len__`/`__repr__`, which also iterate
`_contents`, are fixed as a consequence.

Fixes python-jsonschema#1328
@read-the-docs-community

Copy link
Copy Markdown

Documentation build overview

📚 python-jsonschema | 🛠️ Build #34089591 | 📁 Comparing 5fb60c1 against latest (eda9779)

  🔍 Preview build  

1 file changed
± _modules/jsonschema/exceptions/index.html

@Julian Julian closed this Aug 16, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The return of __iter__() and __contains__() change after accessing of an index with no error

2 participants