From 63e6acb0cbed763f2997ce41aba287a9dbf5b48b Mon Sep 17 00:00:00 2001 From: nileshpatil6 Date: Thu, 20 Aug 2026 23:03:13 +0530 Subject: [PATCH 1/2] Drop inherited dataclass default when an override assigns a field specifier When a dataclass field overrides an inherited field that has a default, pyright always re-inherits that default. That matches the runtime only for a bare annotation. If the override assigns a field specifier that supplies no default, the runtime replaces the entry outright and the parameter becomes required: @dataclass class Base: x: int = 1 @dataclass class Foo(Base): x: int = field() inspect.signature(Foo.__init__) # (self, x: int) -> None Foo() # TypeError: missing argument 'x' pyright synthesized "(self: Foo, a: int = 0) -> None" instead, so the call above was not reported, and the override itself was flagged with "overrides a field of the same name but is missing a default value" even though nothing is inherited in that case. Track whether the entry had an assigned value and only inherit the base default for bare annotations. The bare form is unchanged: it still inherits and still reports the diagnostic. Verified against CPython for all three forms, including that the default is dropped for "x: int = field()", kept for "x: int", and that the parameter is removed for "x: int = field(init=False)". Fixes #11660 --- .../src/analyzer/dataClasses.ts | 13 +++++++- .../src/tests/samples/dataclass4.py | 33 +++++++++++++++++++ .../src/tests/typeEvaluator4.test.ts | 2 +- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/packages/pyright-internal/src/analyzer/dataClasses.ts b/packages/pyright-internal/src/analyzer/dataClasses.ts index 7367def7af1c..2555de3a5f91 100644 --- a/packages/pyright-internal/src/analyzer/dataClasses.ts +++ b/packages/pyright-internal/src/analyzer/dataClasses.ts @@ -331,6 +331,7 @@ export function synthesizeDataClassMethods( let aliasName: string | undefined; let variableTypeEvaluator: EntryTypeEvaluator | undefined; let hasDefault = false; + let hasAssignedValue = false; let isDefaultFactory = false; let isKeywordOnly = ClassType.isDataClassKeywordOnly(classType) || sawKeywordOnlySeparator; let defaultExpr: ExpressionNode | undefined; @@ -362,6 +363,7 @@ export function synthesizeDataClassMethods( } hasDefault = true; + hasAssignedValue = true; defaultExpr = statement.d.rightExpr; // If the RHS of the assignment is assigning a field instance where the @@ -578,7 +580,16 @@ export function synthesizeDataClassMethods( // While this isn't documented behavior, it appears that the dataclass implementation // causes overridden variables to "inherit" default values from parent classes. - if (!dataClassEntry.hasDefault && oldEntry.hasDefault && oldEntry.includeInInit) { + // This applies only when the override is a bare annotation (`x: int`). If the + // override assigns a field specifier that supplies no default (`x: int = field()`), + // the runtime replaces the entry outright, so the inherited default is dropped and + // the parameter becomes required. + if ( + !dataClassEntry.hasDefault && + !hasAssignedValue && + oldEntry.hasDefault && + oldEntry.includeInInit + ) { dataClassEntry.hasDefault = true; dataClassEntry.defaultExpr = oldEntry.defaultExpr; hasDefault = true; diff --git a/packages/pyright-internal/src/tests/samples/dataclass4.py b/packages/pyright-internal/src/tests/samples/dataclass4.py index 31640b1ff8a0..7b13d6fce2f7 100644 --- a/packages/pyright-internal/src/tests/samples/dataclass4.py +++ b/packages/pyright-internal/src/tests/samples/dataclass4.py @@ -108,3 +108,36 @@ class DC10: class DC11(DC10): a: str = field() b: bool = field() + + +@dataclass +class DC12: + a: int = 0 + + +@dataclass +class DC13(DC12): + # Unlike a bare annotation, an assigned field specifier that supplies no + # default replaces the inherited entry rather than inheriting its default, + # so "a" becomes a required parameter. + a: int = field() + + +reveal_type(DC13.__init__, expected_text="(self: DC13, a: int) -> None") + +# This should generate an error because "a" no longer has an +# inherited default value, so it must be provided. +DC13() + + +@dataclass +class DC14: + a: int = 0 + b: int = 1 + + +@dataclass +class DC15(DC14): + # This should generate an error because "b" drops its inherited default + # and would then follow "a", which still has one. + b: int = field() diff --git a/packages/pyright-internal/src/tests/typeEvaluator4.test.ts b/packages/pyright-internal/src/tests/typeEvaluator4.test.ts index 47eed9ce1b23..382b4026aee9 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator4.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator4.test.ts @@ -326,7 +326,7 @@ test('DataClass3', () => { test('DataClass4', () => { const analysisResults = TestUtils.typeAnalyzeSampleFiles(['dataclass4.py']); - TestUtils.validateResults(analysisResults, 6); + TestUtils.validateResults(analysisResults, 8); }); test('DataClass5', () => { From 2ce62337eb7c3500a5046289586052cea2b0e3cd Mon Sep 17 00:00:00 2001 From: nileshpatil6 Date: Tue, 25 Aug 2026 14:45:16 +0530 Subject: [PATCH 2/2] Cover the field(init=False) override branch An assigned field specifier with init=False removes the parameter from __init__ rather than dropping the inherited default, so the attribute keeps the base class value. Pin both the constructor signature and the attribute type. --- .../pyright-internal/src/tests/samples/dataclass4.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/packages/pyright-internal/src/tests/samples/dataclass4.py b/packages/pyright-internal/src/tests/samples/dataclass4.py index 7b13d6fce2f7..cd6f39dc7f93 100644 --- a/packages/pyright-internal/src/tests/samples/dataclass4.py +++ b/packages/pyright-internal/src/tests/samples/dataclass4.py @@ -141,3 +141,15 @@ class DC15(DC14): # This should generate an error because "b" drops its inherited default # and would then follow "a", which still has one. b: int = field() + + +@dataclass +class DC16(DC12): + # An assigned field specifier with init=False removes the parameter from + # __init__ entirely rather than dropping the inherited default, so the + # attribute keeps the base class value. + a: int = field(init=False) + + +reveal_type(DC16.__init__, expected_text="(self: DC16) -> None") +reveal_type(DC16().a, expected_text="int")