Skip to content

brain/dataclasses: add inference tip for dataclasses.replace()#3061

Open
ego-ipse wants to merge 5 commits into
pylint-dev:mainfrom
ego-ipse:infer-dataclasses-replace-caller-type
Open

brain/dataclasses: add inference tip for dataclasses.replace()#3061
ego-ipse wants to merge 5 commits into
pylint-dev:mainfrom
ego-ipse:infer-dataclasses-replace-caller-type

Conversation

@ego-ipse
Copy link
Copy Markdown

@ego-ipse ego-ipse commented May 19, 2026

Summary

dataclasses.replace(obj, **changes) was inferred by following the stdlib
body of replace() / _replace(). That body resolves obj.__class__ via
the metaclass chain, which trips when obj is an instance of a class whose
bases include a subscripted generic (e.g. IrCollection(Base[T])) — the
Subscript node causes helpers.object_type to return Uninferable,
propagating up to the entire replace() call.

The same bug manifests differently for old-style Generic[T] vs PEP 695
class Foo[T] syntax: old-style returns Uninferable (masked error), PEP 695
returns the wrong class (visible false positives in pylint E1101).

Fix

Add a dedicated inference tip for dataclasses.replace in
brain_dataclasses.py that short-circuits the stdlib body entirely: it infers
the first argument, determines its type, and yields a fresh instance of that
class. This is the same pattern used by other brain plugins (isinstance,
type(), etc.) and correctly propagates the concrete caller type through
replace() — including across Self-annotated method chains.

The tip handles both call forms:

  • dataclasses.replace(obj, ...) (attribute access)
  • from dataclasses import replace; replace(obj, ...) (bare name)

Tests

Nine new tests in tests/brain/test_dataclasses.py:

Test What it covers
test_replace_returns_instance_of_caller_type Basic case: result is the passed type
test_replace_returns_subclass_instance old-style Generic[T] base — concrete subclass propagated
test_replace_pep695_generic_base PEP 695 class Base[T] — same (skipped on <3.12)
test_replace_bare_name_form from dataclasses import replace form
test_replace_frozen_dataclass frozen=True is orthogonal, still works
test_replace_uninferable_first_arg First arg can't be inferred → yields Uninferable
test_replace_classdef_first_arg First arg is a class itself → yields instance of it
test_replace_non_instance_non_class_falls_back First arg infers to a module → falls back to default inference
test_replace_no_args_does_not_crash Edge case: no args, no uncaught exception

@Pierre-Sassoulas Pierre-Sassoulas added Enhancement ✨ Improvement to a component Brain 🧠 Needs a brain tip labels May 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Brain 🧠 Needs a brain tip Enhancement ✨ Improvement to a component

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants