diff --git a/packages/pyright-internal/src/analyzer/dataClasses.ts b/packages/pyright-internal/src/analyzer/dataClasses.ts index 7367def7af1c..80bc2ac55ac4 100644 --- a/packages/pyright-internal/src/analyzer/dataClasses.ts +++ b/packages/pyright-internal/src/analyzer/dataClasses.ts @@ -332,6 +332,7 @@ export function synthesizeDataClassMethods( let variableTypeEvaluator: EntryTypeEvaluator | undefined; let hasDefault = false; let isDefaultFactory = false; + let isFieldSpecifierWithoutDefault = false; let isKeywordOnly = ClassType.isDataClassKeywordOnly(classType) || sawKeywordOnlySeparator; let defaultExpr: ExpressionNode | undefined; let includeInInit = true; @@ -437,6 +438,8 @@ export function synthesizeDataClassMethods( defaultExpr = defaultFactoryArg.d.valueExpr; } + isFieldSpecifierWithoutDefault = !hasDefault; + const aliasArg = statement.d.rightExpr.d.args.find((arg) => arg.d.name?.d.value === 'alias'); if (aliasArg) { const valueType = evaluator.getTypeOfExpression(aliasArg.d.valueExpr).type; @@ -576,9 +579,15 @@ export function synthesizeDataClassMethods( if (insertIndex >= 0) { const oldEntry = fullDataClassEntries[insertIndex]; - // 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) { + // A bare annotation (`x: int`) inherits a parent field's default at + // runtime. An explicit `field()` with no default or default_factory + // does not, so the synthesized __init__ parameter is required. + if ( + !dataClassEntry.hasDefault && + oldEntry.hasDefault && + oldEntry.includeInInit && + !isFieldSpecifierWithoutDefault + ) { dataClassEntry.hasDefault = true; dataClassEntry.defaultExpr = oldEntry.defaultExpr; hasDefault = true; diff --git a/packages/pyright-internal/src/tests/samples/dataclass19.py b/packages/pyright-internal/src/tests/samples/dataclass19.py new file mode 100644 index 000000000000..0b957d4e1f25 --- /dev/null +++ b/packages/pyright-internal/src/tests/samples/dataclass19.py @@ -0,0 +1,38 @@ +# This sample tests that overriding a dataclass field with field() +# and no default does not inherit the parent default. + +from dataclasses import dataclass, field + + +@dataclass +class Base: + x: int = 1 + + +@dataclass +class Foo(Base): + # This should not generate an error. field() with no default + # removes the inherited default at runtime. + x: int = field() + + +# This should generate an error because x is required. +Foo() + +foo = Foo(2) +reveal_type(foo.x, expected_text="int") + + +@dataclass +class Base2: + a: int = 0 + + +@dataclass +class BareOverride(Base2): + # This should generate an error because a bare annotation still + # inherits the parent default at runtime. + a: int + + # This should generate an error because a still has a default. + b: str diff --git a/packages/pyright-internal/src/tests/typeEvaluator4.test.ts b/packages/pyright-internal/src/tests/typeEvaluator4.test.ts index 47eed9ce1b23..797a73da565e 100644 --- a/packages/pyright-internal/src/tests/typeEvaluator4.test.ts +++ b/packages/pyright-internal/src/tests/typeEvaluator4.test.ts @@ -413,6 +413,12 @@ test('DataClass18', () => { TestUtils.validateResults(analysisResults, 0); }); +test('DataClass19', () => { + const analysisResults = TestUtils.typeAnalyzeSampleFiles(['dataclass19.py']); + + TestUtils.validateResults(analysisResults, 3); +}); + test('DataClassReplace1', () => { const configOptions = new ConfigOptions(Uri.empty());