diff --git a/docs/configuration.md b/docs/configuration.md
index 299c1eb034d0..8eddba0411e0 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -64,6 +64,8 @@ The following settings determine how different types should be evaluated.
- **disableBytesTypePromotions** [boolean]: Disables legacy behavior where `bytearray` and `memoryview` are considered subtypes of `bytes`. [PEP 688](https://peps.python.org/pep-0688/#no-special-meaning-for-bytes) deprecates this behavior, but this switch is provided to restore the older behavior. The default value for this setting is `true`.
+- **noImplicitReexport** [boolean]: PEP 484 indicates that imported symbols in `py.typed` packages are not considered re-exported unless they appear in `__all__` or use the redundant alias form (`from X import Y as Y`). When this setting is `false`, plain `from X import Y` imports are treated as valid re-exports for public names (those without a leading underscore), mirroring mypy's `no_implicit_reexport = false` behavior. Names with a leading underscore are always treated as private regardless of this setting. The default value for this setting is `true`.
+
## Type Check Diagnostics Settings
The following settings control pyright’s diagnostic output (warnings or errors).
@@ -351,6 +353,7 @@ The following table lists the default severity levels for each diagnostic rule w
| :---------------------------------------- | :--------- | :--------- | :--------- | :--------- |
| analyzeUnannotatedFunctions | true | true | true | true |
| disableBytesTypePromotions | true | true | true | true |
+| noImplicitReexport | true | true | true | true |
| strictParameterNoneValue | true | true | true | true |
| enableTypeIgnoreComments | true | true | true | true |
| enableReachabilityAnalysis | false | true | true | true |
diff --git a/packages/pyright-internal/src/analyzer/typeEvaluator.ts b/packages/pyright-internal/src/analyzer/typeEvaluator.ts
index 6cd8a1b8d814..f6f83caf9d52 100644
--- a/packages/pyright-internal/src/analyzer/typeEvaluator.ts
+++ b/packages/pyright-internal/src/analyzer/typeEvaluator.ts
@@ -5985,14 +5985,19 @@ export function createTypeEvaluator(
}
if (symbol.isPrivatePyTypedImport()) {
- addDiagnostic(
- DiagnosticRule.reportPrivateImportUsage,
- LocMessage.privateImportFromPyTypedModule().format({
- name: memberName,
- module: baseType.priv.moduleName,
- }),
- node.d.member
- );
+ const fileInfo = AnalyzerNodeInfo.getFileInfo(node);
+ const isImplicitReexportAllowed =
+ !fileInfo.diagnosticRuleSet.noImplicitReexport && !memberName.startsWith('_');
+ if (!isImplicitReexportAllowed) {
+ addDiagnostic(
+ DiagnosticRule.reportPrivateImportUsage,
+ LocMessage.privateImportFromPyTypedModule().format({
+ name: memberName,
+ module: baseType.priv.moduleName,
+ }),
+ node.d.member
+ );
+ }
}
} else {
// Does the module export a top-level __getattr__ function?
@@ -20499,22 +20504,28 @@ export function createTypeEvaluator(
}
if (resolvedAliasInfo.privatePyTypedImporter) {
- const diag = new DiagnosticAddendum();
- if (resolvedAliasInfo.privatePyTypedImported) {
- diag.addMessage(
- LocAddendum.privateImportFromPyTypedSource().format({
- module: resolvedAliasInfo.privatePyTypedImported,
- })
+ const importedName = node.d.name.d.value;
+ const fileInfo = AnalyzerNodeInfo.getFileInfo(node);
+ const isImplicitReexportAllowed =
+ !fileInfo.diagnosticRuleSet.noImplicitReexport && !importedName.startsWith('_');
+ if (!isImplicitReexportAllowed) {
+ const diag = new DiagnosticAddendum();
+ if (resolvedAliasInfo.privatePyTypedImported) {
+ diag.addMessage(
+ LocAddendum.privateImportFromPyTypedSource().format({
+ module: resolvedAliasInfo.privatePyTypedImported,
+ })
+ );
+ }
+ addDiagnostic(
+ DiagnosticRule.reportPrivateImportUsage,
+ LocMessage.privateImportFromPyTypedModule().format({
+ name: importedName,
+ module: resolvedAliasInfo.privatePyTypedImporter,
+ }) + diag.getString(),
+ node.d.name
);
}
- addDiagnostic(
- DiagnosticRule.reportPrivateImportUsage,
- LocMessage.privateImportFromPyTypedModule().format({
- name: node.d.name.d.value,
- module: resolvedAliasInfo.privatePyTypedImporter,
- }) + diag.getString(),
- node.d.name
- );
}
}
diff --git a/packages/pyright-internal/src/common/configOptions.ts b/packages/pyright-internal/src/common/configOptions.ts
index 484469ee1399..471ba0d35749 100644
--- a/packages/pyright-internal/src/common/configOptions.ts
+++ b/packages/pyright-internal/src/common/configOptions.ts
@@ -141,6 +141,10 @@ export interface DiagnosticRuleSet {
// No longer treat bytearray and memoryview as subclasses of bytes?
disableBytesTypePromotions: boolean;
+ // Treat plain `from X import Y` imports in py.typed packages as re-exports
+ // for public names? Mirrors mypy's no_implicit_reexport setting.
+ noImplicitReexport: boolean;
+
// Report general type issues?
reportGeneralTypeIssues: DiagnosticLevel;
@@ -417,6 +421,7 @@ export function getBooleanDiagnosticRules(includeNonOverridable = false) {
DiagnosticRule.enableExperimentalFeatures,
DiagnosticRule.deprecateTypingAliases,
DiagnosticRule.disableBytesTypePromotions,
+ DiagnosticRule.noImplicitReexport,
];
if (includeNonOverridable) {
@@ -541,6 +546,7 @@ export function getOffDiagnosticRuleSet(): DiagnosticRuleSet {
enableReachabilityAnalysis: false,
deprecateTypingAliases: false,
disableBytesTypePromotions: true,
+ noImplicitReexport: true,
reportGeneralTypeIssues: 'none',
reportPropertyTypeMismatch: 'none',
reportFunctionMemberAccess: 'none',
@@ -644,6 +650,7 @@ export function getBasicDiagnosticRuleSet(): DiagnosticRuleSet {
enableReachabilityAnalysis: true,
deprecateTypingAliases: false,
disableBytesTypePromotions: true,
+ noImplicitReexport: true,
reportGeneralTypeIssues: 'error',
reportPropertyTypeMismatch: 'none',
reportFunctionMemberAccess: 'none',
@@ -747,6 +754,7 @@ export function getStandardDiagnosticRuleSet(): DiagnosticRuleSet {
enableReachabilityAnalysis: true,
deprecateTypingAliases: false,
disableBytesTypePromotions: true,
+ noImplicitReexport: true,
reportGeneralTypeIssues: 'error',
reportPropertyTypeMismatch: 'none',
reportFunctionMemberAccess: 'error',
@@ -850,6 +858,7 @@ export function getStrictDiagnosticRuleSet(): DiagnosticRuleSet {
enableReachabilityAnalysis: true, // Not overridden by strict mode
deprecateTypingAliases: false,
disableBytesTypePromotions: true,
+ noImplicitReexport: true,
reportGeneralTypeIssues: 'error',
reportPropertyTypeMismatch: 'none',
reportFunctionMemberAccess: 'error',
diff --git a/packages/pyright-internal/src/common/diagnosticRules.ts b/packages/pyright-internal/src/common/diagnosticRules.ts
index 45cb51f6c156..4d736b31362d 100644
--- a/packages/pyright-internal/src/common/diagnosticRules.ts
+++ b/packages/pyright-internal/src/common/diagnosticRules.ts
@@ -21,6 +21,7 @@ export enum DiagnosticRule {
enableReachabilityAnalysis = 'enableReachabilityAnalysis',
deprecateTypingAliases = 'deprecateTypingAliases',
disableBytesTypePromotions = 'disableBytesTypePromotions',
+ noImplicitReexport = 'noImplicitReexport',
reportGeneralTypeIssues = 'reportGeneralTypeIssues',
reportPropertyTypeMismatch = 'reportPropertyTypeMismatch',
diff --git a/packages/pyright-internal/src/tests/fourslash/import.pytyped.noImplicitReexport.fourslash.ts b/packages/pyright-internal/src/tests/fourslash/import.pytyped.noImplicitReexport.fourslash.ts
new file mode 100644
index 000000000000..ee23e37ab9aa
--- /dev/null
+++ b/packages/pyright-internal/src/tests/fourslash/import.pytyped.noImplicitReexport.fourslash.ts
@@ -0,0 +1,36 @@
+///
+
+// @filename: pyrightconfig.json
+//// {
+//// "typeCheckingMode": "basic",
+//// "noImplicitReexport": false
+//// }
+
+// @filename: testLib/py.typed
+// @library: true
+////
+
+// @filename: testLib/__init__.py
+// @library: true
+//// from .module1 import one as one, two, three
+
+// @filename: testLib/module1.py
+// @library: true
+//// one: int = 1
+//// two: int = 2
+//// three: int = 3
+
+// @filename: .src/test1.py
+//// # pyright: reportPrivateImportUsage=true
+//// from testLib import one # explicit re-export (as-alias) — always ok
+//// from testLib import two # plain import, public name — ok with noImplicitReexport=false
+//// from testLib import three # plain import, public name — ok with noImplicitReexport=false
+//// import testLib
+//// testLib.one
+//// testLib.two # ok with noImplicitReexport=false
+//// testLib.three # ok with noImplicitReexport=false
+
+// Verify that no reportPrivateImportUsage errors are raised for public names
+// when noImplicitReexport=false. Private-name behavior is tested in privateImportUsage.test.ts.
+// @ts-ignore
+await helper.verifyDiagnostics();
diff --git a/packages/pyright-internal/src/tests/privateImportUsage.test.ts b/packages/pyright-internal/src/tests/privateImportUsage.test.ts
index bb63f59e6e83..4f689db741d6 100644
--- a/packages/pyright-internal/src/tests/privateImportUsage.test.ts
+++ b/packages/pyright-internal/src/tests/privateImportUsage.test.ts
@@ -167,3 +167,105 @@ describe('reportPrivateImportUsage with tracked library files', () => {
sp.dispose();
});
});
+
+describe('noImplicitReexport config option', () => {
+ // pkg_a: py.typed library that re-exports PublicClass via plain import (no __all__, no `as` alias)
+ // pkg_b: consumer that imports PublicClass from pkg_a
+ const files = [
+ {
+ path: combinePaths(libraryRoot, 'pkg_a', '__init__.py'),
+ content: 'from ._impl import PublicClass\n_PrivateClass = object',
+ },
+ {
+ path: combinePaths(libraryRoot, 'pkg_a', 'py.typed'),
+ content: '',
+ },
+ {
+ path: combinePaths(libraryRoot, 'pkg_a', '_impl.py'),
+ content: 'class PublicClass: pass\nclass _PrivateClass: pass',
+ },
+ {
+ path: normalizeSlashes('/src/consumer.py'),
+ content: [
+ 'from pkg_a import PublicClass', // public name — should be allowed with noImplicitReexport=false
+ 'from pkg_a import _PrivateClass', // private name — should always error
+ ].join('\n'),
+ },
+ ];
+
+ test('public name implicit re-export should not error when noImplicitReexport=false', () => {
+ const sp = createServiceProviderFromFiles(files);
+ const configOptions = new ConfigOptions(UriEx.file('/'));
+ configOptions.diagnosticRuleSet.reportPrivateImportUsage = 'error';
+ configOptions.diagnosticRuleSet.noImplicitReexport = false;
+
+ const importResolver = new ImportResolver(
+ sp,
+ configOptions,
+ new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)])
+ );
+ const program = new Program(importResolver, configOptions, sp);
+ const consumerUri = UriEx.file('/src/consumer.py');
+ program.setTrackedFiles([consumerUri]);
+ while (program.analyze()) {
+ // keep analyzing until complete
+ }
+
+ const sourceFile = program.getSourceFile(consumerUri);
+ assert(sourceFile, 'Source file should exist');
+ const diagnostics = sourceFile.getDiagnostics(configOptions) || [];
+ const errors = diagnostics.filter((d) => d.category === DiagnosticCategory.Error);
+
+ // PublicClass should not error; _PrivateClass should still error (1 total)
+ assert.strictEqual(
+ errors.length,
+ 1,
+ `Expected 1 error (for _PrivateClass only), got ${errors.length}: ${errors
+ .map((e) => e.message)
+ .join(', ')}`
+ );
+ assert(
+ errors[0].message.includes('_PrivateClass'),
+ `The sole error should be about _PrivateClass, got: ${errors[0].message}`
+ );
+
+ program.dispose();
+ sp.dispose();
+ });
+
+ test('public name implicit re-export still errors when noImplicitReexport=true (default)', () => {
+ const sp = createServiceProviderFromFiles(files);
+ const configOptions = new ConfigOptions(UriEx.file('/'));
+ configOptions.diagnosticRuleSet.reportPrivateImportUsage = 'error';
+ configOptions.diagnosticRuleSet.noImplicitReexport = true;
+
+ const importResolver = new ImportResolver(
+ sp,
+ configOptions,
+ new TestAccessHost(sp.fs().getModulePath(), [UriEx.file(libraryRoot)])
+ );
+ const program = new Program(importResolver, configOptions, sp);
+ const consumerUri = UriEx.file('/src/consumer.py');
+ program.setTrackedFiles([consumerUri]);
+ while (program.analyze()) {
+ // keep analyzing until complete
+ }
+
+ const sourceFile = program.getSourceFile(consumerUri);
+ assert(sourceFile, 'Source file should exist');
+ const diagnostics = sourceFile.getDiagnostics(configOptions) || [];
+ const errors = diagnostics.filter((d) => d.category === DiagnosticCategory.Error);
+
+ // Both imports should error when noImplicitReexport=true
+ assert.strictEqual(
+ errors.length,
+ 2,
+ `Expected 2 errors (PublicClass and _PrivateClass), got ${errors.length}: ${errors
+ .map((e) => e.message)
+ .join(', ')}`
+ );
+
+ program.dispose();
+ sp.dispose();
+ });
+});
diff --git a/packages/vscode-pyright/package.json b/packages/vscode-pyright/package.json
index ad63927f9820..82979183bcec 100644
--- a/packages/vscode-pyright/package.json
+++ b/packages/vscode-pyright/package.json
@@ -1500,6 +1500,12 @@
"Trace"
]
},
+ "python.analysis.noImplicitReexport": {
+ "type": "boolean",
+ "default": true,
+ "description": "When true, imported symbols in py.typed packages require explicit re-export (via __all__ or 'as' alias). Set to false to allow plain imports as re-exports for public names.",
+ "scope": "resource"
+ },
"python.analysis.typeCheckingMode": {
"type": "string",
"default": "standard",
diff --git a/packages/vscode-pyright/schemas/pyrightconfig.schema.json b/packages/vscode-pyright/schemas/pyrightconfig.schema.json
index cb6c591eb426..9f6e35779f7f 100644
--- a/packages/vscode-pyright/schemas/pyrightconfig.schema.json
+++ b/packages/vscode-pyright/schemas/pyrightconfig.schema.json
@@ -99,6 +99,11 @@
"title": "Treat typing-specific aliases to standard types as deprecated",
"default": false
},
+ "noImplicitReexport": {
+ "type": "boolean",
+ "title": "When true, imported symbols in py.typed packages require explicit re-export (via __all__ or 'as' alias). Set to false to allow plain imports as re-exports for public names.",
+ "default": true
+ },
"reportGeneralTypeIssues": {
"$ref": "#/definitions/diagnostic",
"title": "Controls reporting of general type issues",
@@ -618,6 +623,9 @@
"deprecateTypingAliases": {
"$ref": "#/definitions/deprecateTypingAliases"
},
+ "noImplicitReexport": {
+ "$ref": "#/definitions/noImplicitReexport"
+ },
"reportGeneralTypeIssues": {
"$ref": "#/definitions/reportGeneralTypeIssues"
},
@@ -936,6 +944,9 @@
"deprecateTypingAliases": {
"$ref": "#/definitions/deprecateTypingAliases"
},
+ "noImplicitReexport": {
+ "$ref": "#/definitions/noImplicitReexport"
+ },
"reportGeneralTypeIssues": {
"$ref": "#/definitions/reportGeneralTypeIssues"
},