这事儿我在数据采集脚本里见过不止一次。
代码能跑。
from google.cloud import storage
client = storage.Client()
运行没报错,项目也确实连上了对象存储。结果切回 VS Code,输入:
from google.cloud import st
Pylance 不提示 storage。
有时候连已经存在的类都不自动补全。更尴尬的是,程序继续运行,像什么都没发生过。
这到底是 Python 包坏了,还是 Pylance 拉胯?
先别急着改配置。这个现象通常不是一个问题,而是四个机制叠在了一起。
运行时成功,不代表静态分析器看得见
先把最容易混在一起的两件事分开:
- Python 运行时要把模块加载出来;
- Pylance 要在不执行代码的情况下推断出模块里有哪些名字。
这不是同一件事。
Python 执行:
from google.cloud import storage
时,会按照当前解释器的 sys.path 去找模块。只要它最终找到了 google.cloud.storage,导入就能成功。
Pylance 的自动导入则不一样。它通常不会为了获取提示而真的执行整个包。它需要扫描目录、解析 __init__.py、读取类型存根、判断包的导出关系,然后建立自己的索引。
一个是“现在能不能加载”。
一个是“我能不能提前判断你可能要加载什么”。
这能完全一样吗?
不能。
第一个坑:你运行代码的解释器,可能不是 Pylance 使用的解释器
这个问题最普通,也最容易被复杂现象掩盖。
比如你在终端里执行:
pip install google-cloud-storage
包被装进了 A 环境。
但 VS Code 当前选中的解释器是 B 环境。
于是出现一种很典型的错觉:
- 终端里的 Python 能导入;
- VS Code 里的 Pylance 找不到;
- 你以为是 Pylance 索引坏了;
- 实际上两个 Python 根本不是同一个。
我现在遇到这类问题,第一步不会看 settings.json,而是先看解释器路径。
python -c "import sys; print(sys.executable)"
python -m pip show google-cloud-storage
python -c "from google.cloud import storage; print(storage.__file__)"
这三条命令要在 VS Code 当前使用的环境里执行。
如果项目用了虚拟环境,还要确认 VS Code 左下角显示的解释器,和终端里的 sys.executable 对得上。
不对就先切解释器。
这一步没对上,后面研究 __init__.py 都是白费劲。

第二个坑:google.cloud 这类包,不一定是传统的普通包
很多人看到一个目录没有 __init__.py,第一反应是:包结构坏了。
不一定。
Python 支持命名空间包。像 google、google.cloud 这种路径,可能由多个独立安装的发行包共同组成。google-cloud-storage 只是其中的一部分,它不一定需要在某个统一的 google/cloud/__init__.py 里把所有子包写死。
运行时只要搜索路径里存在对应目录,Python 就可能把它拼起来。
这对运行时很方便。
对静态分析器就没那么省心了。
因为 Pylance 需要判断:
- 这个目录是不是命名空间包;
- 子目录来自哪个安装位置;
- 当前环境里到底装了哪些同名分发包;
storage是一个模块、一个子包,还是某个文件里的变量;- 当前索引深度有没有扫到它。
所以,__init__.py 缺失,不一定意味着导入错误。
但它确实会影响工具理解包结构的方式。
__init__.py 到底做了什么?
我们自己写一个简单包:
my_pkg/
├── __init__.py
└── client.py
client.py 里有:
class Client:
pass
如果 __init__.py 是空的,那么更明确的导入方式是:
from my_pkg.client import Client
这时 Client 属于 my_pkg.client 模块,并没有自动变成 my_pkg 的公开成员。
如果我们希望用户这样写:
from my_pkg import Client
就需要在 __init__.py 里显式导出:
from .client import Client
__all__ = ["Client"]
这两种写法对 Python 来说都合法。
区别在于,第一种公开的是子模块路径,第二种公开的是包的顶层 API。
Pylance 更容易理解第二种,因为导出关系写在了静态代码里。
注意,是“更容易理解”,不是“加了 __all__ 就一定自动提示”。
__all__ 主要解决的是公开名称和通配符导入语义。它不能凭空把一个运行时才生成的对象变成静态分析器已经知道的符号。
第三个坑:动态导出,运行时很灵活,静态分析器很难猜
有些包为了减少启动时间、兼容不同版本,喜欢做动态导出。
例如:
# package/__init__.py
def __getattr__(name):
if name == "Client":
from .client import Client
return Client
raise AttributeError(name)
这样写以后:
from package import Client
在运行时可能完全正常。
问题是,Pylance不一定会执行 __getattr__。它也不会随便运行第三方包,因为执行包代码可能有副作用,甚至会修改环境、发网络请求、读取配置。
那它怎么知道 Client 存在?
如果包同时提供了类型存根,事情会好很多:
package/
├── __init__.py
├── __init__.pyi
└── py.typed
__init__.pyi 可以把静态 API 写出来:
from .client import Client as Client
对于类型检查器来说,.pyi 是“这个模块对外承诺有哪些名字”的说明书。
而 py.typed 通常表示这个包携带的类型信息可以被类型检查器使用。
所以这里有一个经常被忽略的关系:
- 运行时实现决定代码能不能执行;
.pyi和py.typed影响类型工具能不能理解;__init__.py和显式导出影响工具能不能找到顶层 API;- 动态
__getattr__会让运行时能力超过静态可见范围。
这几件事不是一回事。
第四个坑:Pylance 不是无限深度扫描整个 site-packages
就算解释器正确,包结构也没问题,自动导入仍然可能不完整。
原因是索引有成本。
第三方包很多,目录层级很深,文件数量也不少。如果 Pylance 每次启动都把所有依赖完整展开,机器风扇先起飞,编辑器再说话。
所以 Pylance 会对部分包做索引控制。
常见相关设置包括:
{
"python.analysis.autoImportCompletions": true,
"python.analysis.indexing": true
}
对于层级较深、自动导入不完整的包,也可以配置包索引深度:
{
"python.analysis.packageIndexDepths": [
{
"name": "google.cloud",
"depth": 3,
"includeAllSymbols": true
}
]
}
这里要注意三点。
第一,autoImportCompletions 只控制自动导入建议,不负责修复解释器路径。
第二,索引深度主要影响“能不能被搜到”,不等于运行时 import 规则发生了变化。
第三,includeAllSymbols 不是越多越好。索引范围开得很大,可能让分析变慢,也可能把内部实现名称一起暴露出来。
为了一个提示,把整个依赖树都扫一遍,划算吗?
通常不划算。
真实排查时,我会按这个顺序走

先确认当前解释器
import sys
print(sys.executable)
再确认包实际装在哪里:
python -m pip show google-cloud-storage
不要只看系统里的 pip 输出。
再确认完整导入路径
分别测试:
import google
import google.cloud
from google.cloud import storage
from google.cloud.storage import Client
哪一层失败,问题范围就缩小了。
如果:
from google.cloud import storage
成功,但:
from google.cloud import StorageClient
失败,那就不是“整个包找不到”,而是顶层没有导出这个名字,或者这个名字只在子模块里存在。
再看包的目录结构
可以查看包路径:
import google.cloud.storage as storage
print(storage.__file__)
然后检查:
- 目标模块是不是真的存在;
- 顶层
__init__.py有没有显式导入; - 是否存在
.pyi类型存根; - 是否有
py.typed; - 包是不是通过动态属性暴露 API。
这里我通常不建议一上来就改包源码。
先确认它到底提供了哪一种 API。
最后再处理 Pylance 索引
如果前面都没问题,再看:
- VS Code 是否启用了正确解释器;
python.analysis.indexing是否关闭;python.analysis.autoImportCompletions是否关闭;- 包是否需要增加索引深度;
- 重载窗口后是否重新建立索引。
有时候重新选择一次解释器,再执行“Developer: Reload Window”,提示就回来了。
有时候不会。
因为工具能力也有边界。
为什么我不建议把“自动导入提示”当成最终判断?
自动导入很方便,但它只是编辑器根据静态信息给出的建议。
它不是 Python 运行时的权威答案。
一个包可能存在以下情况:
- 运行时通过
__getattr__动态生成对象; - 顶层 API 是由多个子包拼出来的;
- 类型存根没有及时更新;
- 包的实现依赖版本判断;
- 模块由 C 扩展提供;
- 真实导出路径和文档推荐路径不同;
- Pylance索引还没有覆盖到对应层级。
所以“Pylance没有提示”至少可能对应三种完全不同的结论:
- 当前解释器里根本没有这个包;
- 包存在,但你写的导入路径不是它公开的路径;
- 导入路径是对的,只是 Pylance 没有足够的静态信息。
这三种情况,修法完全不同。
第一种修解释器。
第二种查包结构和公开 API。
第三种才考虑存根、索引和 IDE 能力。
对包作者来说,什么样的结构更容易被工具理解?
如果是自己维护的包,建议把公开 API 写得明确一点:
my_pkg/
├── __init__.py
├── client.py
├── client.pyi
└── py.typed
顶层导出不要只靠动态逻辑:
from .client import Client
__all__ = ["Client"]
如果实现必须动态加载,就补充对应的类型存根。
如果包是给团队内部使用的,文档里也最好明确:
# 推荐
from my_pkg import Client
# 不建议依赖内部实现
from my_pkg._internal.client_impl import Client
否则用户今天跟着自动提示导入,明天包一升级,内部目录一改,代码就崩。
真正稳定的是公开 API,不是某个编辑器刚好提示出来的路径。
最后,别把问题简化成一行配置
Python 包能运行,Pylance却不能自动导入,通常不是一个简单的“插件坏了”。
它更像是四层信息没有对齐:
- 解释器层:Pylance和终端是不是用的同一个 Python;
- 包结构层:这是普通包、命名空间包,还是多个分发包拼出来的路径;
- 类型信息层:有没有显式导出、
.pyi和py.typed; - 索引层:Pylance有没有扫描到对应目录和符号。

我做跨境采集这几年,工具链里最怕的就是“代码能跑,所以环境一定没问题”这种判断。
运行时成功,只能证明当前这一次执行找到了目标。
它不能证明:
- 编辑器理解了这套包结构;
- 类型检查器拿到了完整 API;
- 自动导入索引覆盖了所有子模块;
- 你使用的是包作者真正承诺的公开路径。
所以遇到这类问题,先别急着搜一行神配置。
先问自己一句:
到底是 Python 找不到,还是 Pylance看不懂?
答案不一样,排查路线也不一样。
这事儿,不能混着修。