Skip to content

Fix class checker crashes on x.__class__ targets outside plain assignments - #11268

Open
codeAnqiang-ma wants to merge 2 commits into
pylint-dev:mainfrom
codeAnqiang-ma:fix/class-checker-assignattr-crash
Open

Fix class checker crashes on x.__class__ targets outside plain assignments#11268
codeAnqiang-ma wants to merge 2 commits into
pylint-dev:mainfrom
codeAnqiang-ma:fix/class-checker-assignattr-crash

Conversation

@codeAnqiang-ma

Copy link
Copy Markdown

Type of Changes

Type
🐛 Bug fix

Description

Closes #11267

visit_assignattr fires for every AssignAttr, but the two __class__ code paths assumed the target's parent is a plain assignment carrying a literal value, so legal constructs like for foo.__class__ in [Foo]: aborted the whole module with a fatal astroid-error (F0002):

  • _check_invalid_class_object read node.parent.value, which does not exist for a for-loop, with, or comprehension target and is None for a bare annotation; its tuple branch indexed .value.elts, which crashes when the right-hand side is a call, a nested tuple, or a too-short literal (AttributeError / IndexError).
  • _check_in_slots read node.parent.value the same way for slotted classes, and _has_same_layout_slots called next(assigned_value.infer()) without handling astroid.InferenceError, so an unresolvable value such as an undefined name also crashed.

The fix skips the checks when there is no single assigned value to inspect (the same approach as #11173's non-name loop target fix), and gives an unresolvable value in _has_same_layout_slots the same answer as any other non-class value, matching the existing behavior for Uninferable (assigning-non-slot is still emitted — covered by the existing ClassReassingingInvalidLayoutClass expectation and a new test).

Existing behavior is preserved: the #7467 tuple-unpacking cases keep their messages, including starred unpacking like self.__class__, *rest = ... (the bound check only skips genuinely unbalanced unpacking).

Test evidence

Both extended functional tests fail before the fix and pass after it:

$ python -m pytest tests/test_functional.py -k "invalid_class_object or assigning_non_slot" -q
# before the fix: 2 failed (AttributeError: 'For' object has no attribute 'value', ...), 1 passed
# after the fix:  3 passed

Full functional suite (python -m pytest tests/test_functional.py -q -n auto): 874 passed, 29 skipped, 14 failed — the failing set is byte-for-byte identical on current main in the same environment (macOS, Python 3.13.3; e.g. wrong_import_order, used_before_assignment_py310/311), i.e. pre-existing and unrelated. mypy and a self-run of pylint on the changed file are clean.

This PR was prepared with AI assistance as part of a code correctness review; every crash variant was reproduced locally and the fix and tests were reviewed and run by the author.

…ments

visit_assignattr fires for every AssignAttr, but the two __class__ code
paths assumed the target's parent is a plain assignment carrying a
literal value:

- _check_invalid_class_object read node.parent.value, which does not
  exist for a for-loop, ``with``, or comprehension target, is None for a
  bare annotation, and its tuple branch indexed .value.elts, which
  crashes when the right-hand side is a call, a nested tuple, or a
  too-short literal.
- _check_in_slots read node.parent.value the same way for slotted
  classes, and _has_same_layout_slots called next(assigned_value.infer())
  without handling astroid.InferenceError, so an unresolvable value such
  as an undefined name aborted the whole module.

Skip the checks when there is no single assigned value to inspect, and
give an unresolvable value the same answer as any other non-class value,
matching the existing behavior for Uninferable.

Closes pylint-dev#11267

Co-authored-by: Cursor <cursoragent@cursor.com>
@Pierre-Sassoulas Pierre-Sassoulas added the Crash 💥 A bug that makes pylint crash label Aug 15, 2026
@codecov

codecov Bot commented Aug 15, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 96.40%. Comparing base (4be9585) to head (ffba5c1).
⚠️ Report is 2 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main   #11268      +/-   ##
==========================================
+ Coverage   96.36%   96.40%   +0.04%     
==========================================
  Files         178      178              
  Lines       19953    20042      +89     
==========================================
+ Hits        19228    19322      +94     
+ Misses        725      720       -5     
Files with missing lines Coverage Δ
pylint/checkers/classes/class_checker.py 94.80% <100.00%> (+0.72%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@github-actions

This comment has been minimized.

Three shapes from the same crash family were left out:

- ``nodes.Starred`` is the one non-assignment parent that does carry a
  usable value (astroid resolves it through the target itself), so
  skipping it silently dropped ``invalid-class-object`` and
  ``assigning-non-slot`` on ``head, *foo.__class__ = ...``, which used to
  be diagnosed correctly.
- A ``nodes.List`` assignment target crashed like the tuple one, and was
  only made silent; it now goes through the same unpacking logic.
- ``ClassDef.slots()`` returns None when a class in the mro has no
  ``__slots__``, so ``zip_longest(slots, None)`` still aborted the whole
  module on the plainest shape of all, ``instance.__class__ =
  SlotlessClass``. CPython rejects that assignment too, so
  ``assigning-non-slot`` is the right answer.

The parent-shape guard is now a single ``_assigned_value`` helper, so the
two call sites cannot drift apart.
@Pierre-Sassoulas
Pierre-Sassoulas force-pushed the fix/class-checker-assignattr-crash branch from b2d65b3 to ffba5c1 Compare August 15, 2026 13:30
@github-actions

This comment has been minimized.

Comment thread doc/whatsnew/fragments/11267.bugfix Outdated
Comment on lines +1 to +9
Fix a crash in the class checker when ``x.__class__`` is an assignment target
outside a plain assignment, such as a for-loop, ``with``, or comprehension
target, a bare annotation, a list target, or a tuple target unpacked from a
non-literal or too-short right-hand side. This affected both
``invalid-class-object`` and ``assigning-non-slot``, which also crashed when the
assigned value could not be resolved by astroid, or when ``__class__`` was
assigned a class that does not define ``__slots__``.

Closes #11267

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.

Suggested change
Fix a crash in the class checker when ``x.__class__`` is an assignment target
outside a plain assignment, such as a for-loop, ``with``, or comprehension
target, a bare annotation, a list target, or a tuple target unpacked from a
non-literal or too-short right-hand side. This affected both
``invalid-class-object`` and ``assigning-non-slot``, which also crashed when the
assigned value could not be resolved by astroid, or when ``__class__`` was
assigned a class that does not define ``__slots__``.
Closes #11267
Fix a crash in :ref:`invalid-class-object` and :ref:`assigning-non-slot` when ``__class__`` is assigned outside a simple assignment (e.g. ``for obj.__class__ in classes:``).
Closes #11267

@github-actions

Copy link
Copy Markdown
Contributor

🤖 According to the primer, this change has no effect on the checked open source code. 🤖🎉

This comment was generated for commit ffba5c1

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

Labels

Crash 💥 A bug that makes pylint crash

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Crash AttributeError: 'For' object has no attribute 'value' when x.__class__ is a for-loop, with, or unpacking target

2 participants