
写这篇东西的起因是我又双叒叕在群里看到有人发ModuleNotFoundError: No module named xxx的截图了。说实话这种问题对于刚接触 Python 和 PyCharm 的朋友来说基本是必经之路。绝大多数情况下报错信息里带着“No module named”字样而且代码逻辑本身没毛病那就是解释器找不到你的模块跟代码写没写错关系不大。尤其是当你把项目拆成多个包、多个目录来管理代码时模块不在同一个包下导致的导包失败会变得非常频繁而且 Pycharm 有时候还会出现“明明是同一个项目换个目录就导不了包”的诡异现象。这篇东西我就围绕“模块不在同一个包中导致导包报错”这个场景把原因、底层机制、解决方案和 Pycharm 里的坑一次性讲清楚。不管你是在校学生做课设还是刚入职用 PyCharm 写业务代码只要你被ImportError折磨过这篇内容应该能帮你省下不少时间。1. 先搞清楚 Python 导包的底层逻辑1.1 “模块不在同一个包”到底是什么意思很多人一看到导包报错第一反应是去检查 import 语句有没有写错或者文件名是不是拼错了。但实际上当你确认代码本身没问题之后问题往往出在“Python 解释器根本不知道去哪里找这个模块”。这里需要先理清几个概念。模块就是一个.py文件比如utils.py。包就是一个包含__init__.py文件的目录从 Python 3.3 开始即便没有__init__.py目录也可以作为命名空间包被导入但为了规范我还是建议你保留这个文件。当我们写from utils import helper时Python 解释器会按照sys.path里记录的路径列表一个一个去查找名为utils.py的文件。所以出现ModuleNotFoundError本质上就是sys.path列表里没有包含目标模块所在的目录或者包含的目录不对。所谓“模块不在同一个包中”只是一个比较笼统的说法实际场景可以细分成三种你要导入的模块在项目的另一个目录层级里比如src/module_a.py要导入src/core/module_b.py。你要导入的模块在项目根目录之外比如引入了外部的公共库或者另一个独立项目。你要导入的目录本身被 PyCharm 错误地识别成了“普通目录”而不是“源根目录”Sources Root导致搜索路径没有自动加上。我自己最常遇到的是第一种和第三种叠加出现的情况明明 py 文件就在项目里打开终端运行却能跑通在 PyCharm 里一点运行就报错。这种灵异现象十有八九就是 PyCharm 的路径标记和终端下的路径不一致导致的。1.2 sys.path 的搜索顺序你用对了吗先做一个简单实验。你在 PyCharm 里新建一个项目随便写一个脚本然后执行import sys for p in sys.path: print(p)你会看到类似这样的一组路径/Users/你的用户名/项目目录 /usr/local/lib/python3.9/site-packages /Library/Developer/CommandLineTools/Library/Frameworks/Python.framework/Versions/3.9/lib/python3.9 ...第一条路径通常就是项目根目录或者你当前脚本所在的目录PyCharm 会自动帮你加进去。但注意这个“自动加进去”是有条件的取决于 PyCharm 对目录的标记。sys.path的搜索顺序大体是当前执行脚本所在的目录。PYTHONPATH环境变量里记录的路径。Python 标准库目录。site-packages 中安装的第三方库路径。.pth文件里记录的路径。平时你写import numpy就是靠第 4 条路径找到的。而你想import config如果你自己的config.py不在上述任何一条路径里解释器就只能干瞪眼直接给你抛一个ModuleNotFoundError。那么“模块不在同一个包中”这个场景通常就是第一条和第二条路径没覆盖到位。换句话说要么你运行脚本的目录层级不对要么项目根目录没有被正确标记为源根要么PYTHONPATH没有配置。2. 拆解三类最常见的 PyCharm 导包报错场景2.1 场景一同一项目不同包之间的相互导入这是我在处理问答时遇到最多的场景。假设项目结构如下project/ ├── main.py ├── utils/ │ ├── __init__.py │ └── helper.py └── services/ ├── __init__.py └── user_service.py现在user_service.py里想导入utils/helper.py的函数from utils.helper import format_name直觉告诉我这个写法是对的。但你运行user_service.py时可能会遇到两种结果如果直接右键运行user_service.pyPyCharm 会把services目录加入sys.path同时项目根目录project也被标记为 Sources Root所以from utils.helper能正常找到。但是如果utils目录没有被正确标记或者 PyCharm 没有把项目根目录加入路径就会报错。这里的关键在于PyCharm 在运行脚本时会把脚本所在目录和标记为 Sources Root 的目录加入sys.path。所以你观察一下 PyCharm 的目录颜色正常的源根目录应该是蓝色。如果你看到utils或project显示为普通灰色目录说明没有被标记那么解释器就不会把它加入搜索路径。我自己的建议是在 PyCharm 里看到任何报ModuleNotFoundError且你自己确认 import 写法没问题的情况先按住 Ctrl 键Mac 上用 Cmd点击 import 后面的模块名看看能否跳转过去。如果跳转不了就说明路径没被解释器识别这时候再去检查目录标记。2.2 场景二跨项目或外部目录导入还有一种场景比较折腾就是你要导入的模块根本不在当前项目目录下。比如你在做接口自动化测试公共的封装库放在另一个项目里或者从同事那里拉下来的公共代码放在硬盘的某个公共目录里。这种情况下即便你把目录标记成 Sources Root可能也没法解决问题因为 PyCharm 的 Sources Root 是基于当前项目视图内的目录来标记的。如果你要把项目外部的目录加进来需要另想办法这个后面会详细说。2.3 场景三运行时终端与 PyCharm 运行结果不一致这个问题特别迷惑人。很多人在 PyCharm 里运行报错但是打开 macOS 的 Terminal 或者 Windows 的 CMD手动进到项目目录再运行同一个文件发现一切正常。原因很简单在终端里运行python main.py时当前工作目录就是projectPython 解释器默认把当前工作目录加入sys.path所以能搜到同级的utils。而在 PyCharm 里默认的工作目录和sys.path的注入方式跟终端不完全一致它依赖项目设置里的根目录标记。如果你某些目录没标记对就会出现“终端能跑IDE 里跑不了”的尴尬局面。理解这个原理之后就明白解决方案的核心思路想办法让 PyCharm 运行脚本时把目标模块的根目录注入sys.path。3. 彻底解决从包结构梳理到 PyCharm 配置3.1 按照包的标准结构组织你的项目如果项目是刚起步还没有历史包袱强烈建议一开始就按标准的包结构来组织目录。这样能从根本上避免大部分导包问题。一个合格的包结构通常长这样project/ # 项目根目录 ├── README.md ├── requirements.txt ├── setup.py # 可选如果要打包发布 ├── my_package/ # 主包目录 │ ├── __init__.py │ ├── core/ │ │ ├── __init__.py │ │ └── engine.py │ ├── utils/ │ │ ├── __init__.py │ │ └── logger.py │ └── models/ │ ├── __init__.py │ └── user.py ├── tests/ # 测试目录 │ ├── __init__.py │ └── test_user.py └── scripts/ # 可执行脚本目录 ├── __init__.py └── run_demo.py整个项目的根目录是project所有源码都放在一个主包my_package下面。这样只要project被加入sys.path你在scripts/run_demo.py里就可以用绝对导入from my_package.utils.logger import setup_logger from my_package.core.engine import Engine所有导入都从主包一层一层找下去不会出现需要sys.path.append或者各种“跳目录导入”的别扭写法。这是我目前最推荐的项目组织方式简单、清晰、可维护。不过现实是很多老项目或者临时脚本并不会按照这种结构组织。没关系下面这些方案同样能解决。3.2 把项目根目录标记为 Sources Root这是一个最直接的 PyCharm 操作也是很多人踩坑最多的点。在 PyCharm 左侧的项目文件树中找到你想要作为导入根目录的文件夹通常是项目根目录、src目录或者某个公共代码目录右键点击它选择Mark Directory as-Sources Root。标记成功后这个目录会变成蓝色PyCharm 在运行时就会把这个目录加入sys.path。比如在刚才的场景中project下面有utils和services你想在services里通过from utils.helper import xxx导入那么你应该把project标记为 Sources Root。之后运行services下的任何文件project目录都会被纳入搜索路径Python 就能沿着project/utils/helper.py找到目标模块。注意这一步跟当前运行文件在哪里没有关系。有些同学会遇到一种情况把services目录设置成 Sources Root 了然后运行services里的文件结果仍然找不到utils。这是因为如果你只把services标记为 Sources Root解释器搜索的是services目录以及它下面的子模块没有把上一级的project目录加进去。所以操作时先想明白你 import 语句里的那个“顶级包”或者“顶级模块”它所在的目录是哪一个就把那个目录设成 Sources Root。3.3 不要让包目录“隐形”聊到这个顺便说一个容易忽略的点。当你在某个目录下创建了一个__init__.py文件PyCharm 会把这个目录视为程序包。这本身没问题但是有些情况下程序包里的模块导入外部模块会产生“相对导入还是绝对导入”的困惑。假设目录结构是project/ ├── main.py └── pkg_a/ ├── __init__.py └── module_a.py如果main.py的内容是from pkg_a import module_a module_a.say_hello()然后module_a.py里又想导入同包下的另一个模块module_b那么正确写法是from pkg_a.module_b import another_func或者用相对导入from .module_b import another_func如果module_a里写了from module_b import another_func解释器会先在sys.path里找有没有module_b.py结果没有因为module_b不在sys.path记录的任何一个目录下它是躺在pkg_a这个子目录里的。于是报错。这种“在同包内导入同包模块却用了不带包前缀的写法”的情况也极其常见。究其原因是写代码的人混淆了“脚本”和“模块”的概念。脚本是你直接用 Python 运行的入口文件它所在的目录会加入sys.path。模块是被 import 加载的文件它的定位依赖包路径不会因为“你俩在同一个文件夹里”就被自动识别为可导入的对象。如果你希望一个.py文件能被其他包导入请把它当作模块、放在包结构里来管理而不是指望解释器把每个文件目录都加进搜索路径。3.4 临时方案修改 sys.path如果你的项目结构已经一团糟或者你要快速验证某个功能不想折腾 PyCharm 的目录标记那可以在代码里临时手动添加路径。import sys import os # 获取当前文件的绝对路径的上一级目录 current_dir os.path.dirname(os.path.abspath(__file__)) parent_dir os.path.dirname(current_dir) sys.path.append(parent_dir) from utils.helper import format_name这段代码的意思很清楚先把当前文件所在的上一级目录加入sys.path然后再导入utils下面的模块。不少人会把这段代码写在文件最顶部。但这里我必须泼一盆冷水这个方案只能临时救急千万别把它当成常规做法。原因至少有两点如果这种sys.path.append到处出现代码的可维护性会变得极差。每个文件都要写一行路径拼接逻辑以后项目挪个位置、或者别人接手代码根本不知道你为什么要往sys.path里塞这个目录。它跟你使用的 IDE 或者运行方式强相关。今天你在 PyCharm 里改好了明天换 VS Code 跑同样的代码路径又可能变得不对劲。所以能用前两种方案规范项目结构、标记 Sources Root解决的问题就不要用sys.path.append这种打补丁的方式。4. 实战案例从报错到跑通的全过程4.1 一个典型的复现场景为了说得更明白我构造一个实际会报错的场景然后一步一步把它修好。你从网上下了一个开源项目结构大概是这样awesome_project/ ├── examples/ │ └── demo.py ├── awesome/ │ ├── __init__.py │ ├── core.py │ └── utils.py └── README.md你在 PyCharm 中直接右键运行examples/demo.py文件里的代码是from awesome import core from awesome.utils import load_config core.run()结果 PyCharm 控制台给你来一句ModuleNotFoundError: No module named awesome奇怪了awesome目录明明存在。为什么找不到原因就在于PyCharm 在运行examples/demo.py时会自动把examples目录作为脚本目录加入sys.path。同时如果项目根目录awesome_project没有被标记为 Sources Root解释器就不会把awesome_project加入搜索路径。于是awesome这个目录虽然在项目里存在但 Python 解释器“看不见”它因为它不在搜索路径上。4.2 一步步排查和修复这里我按实际排查的顺序走一遍第一步先检查目录是不是没有__init__.py。如果awesome目录下没有任何__init__.py那么 Python 3 的命名空间包机制可能还能勉强工作但在 PyCharm 中配合某些虚拟环境时会偶发出问题。稳妥起见确认awesome目录里有__init__.pyawesome的子目录如果有也要有。第二步检查目录标记。在 PyCharm 项目树中看awesome_project目录颜色如果不是蓝色右键Mark Directory as | Sources Root。第三步再看运行配置。点击顶部运行按钮旁边的下拉框选择Edit Configurations确认 Python 解释器选对工作目录Working directory是否为awesome_project根目录。第四步如果以上都没问题还报错就在sys.path里把实际路径打印出来看看有没有包含awesome_projectimport sys for p in sys.path: print(p)如果确实没有并且你不想依赖目录标记可以在运行配置里设置环境变量PYTHONPATH指定为你的项目根目录。![注意] 提醒一点设置PYTHONPATH是全局影响比较小的方案但 PyCharm 的 Run Configuration 默认不会自动加载PYTHONPATH环境变量你需要手动在环境变量字段里添加PYTHONPATH项目根目录。把上面几步做完当我再次右键运行demo.py控制台输出正常不再有ModuleNotFoundError。4.3 多种解决办法对照为了方便以后查阅我把最常用的几种解决方案放在一起做个对比方案适用场景操作难度推荐程度规范项目结构统一用绝对导入新项目、刚起步前期需要设计强烈推荐Mark Directory as Sources RootPyCharm 内运行最简单右键即可非常推荐设置PYTHONPATH环境变量跨环境运行、命令行也要跑中等推荐代码中sys.path.append临时调试、来不及改结构一行代码不推荐用相对导入from . import xxx同一个包内部互相引用简单但要小心入口可用但需理解关于相对导入我再多说一句。很多新手在 PyCharm 里写from . import xxx或者from ..something import xxx然后直接右键运行当前文件然后报ImportError: attempted relative import with no known parent package。这个问题并不是你的包结构错了而是因为直接运行一个包内的模块时Python 不知道这个模块的父包是谁。相对导入只对于“作为模块被加载”的场景生效对于“作为脚本直接运行”的场景不生效。如果你想用相对导入那么你应该把包外的入口文件作为启动脚本。比如project/ ├── run.py └── my_package/ ├── __init__.py ├── models/ │ ├── __init__.py │ └── user.py └── services/ ├── __init__.py └── auth.pyauth.py里写了from ..models.user import User那么你不要直接右键运行auth.py而应该在project根目录下创建run.py并在里面写from my_package.services.auth import ...再运行run.py。只有这时候auth.py才有了父包my_package相对导入才能正确解析。5. PyCharm 配置层面容易忽视的细节与坑5.1 Run Configuration 的 Working Directory 带来的假象PyCharm 的每个运行配置Run/Debug Configuration里都有 Working Directory 这个选项它会决定你运行脚本时的当前工作目录。很多人以为修改这个目录就能解决导包问题其实它只解决了文件读写的问题比如打开一个相对路径的配置文件对于模块导入不一定有帮助。因为导入依托的是sys.path逻辑而不是当前工作目录。除非你运行的脚本正好在 Working Directory 下Python 才把脚本所在目录加入sys.path。在查一个导包问题时我建议先看一眼 PyCharm 的 Run ConfigurationScript path是不是指向了正确文件Python interpreter是不是当前虚拟环境Working directory是不是项目根目录有一个实际案例某同学在test目录下放测试脚本想导入项目根目录下的src包但 Run Configuration 里的 Working Directory 指向了test目录导致找不到src。把 Working Directory 改回项目根目录之后问题就消失了。有时候 PyCharm 的自动检测并不总是符合预期养成检查这些配置的习惯很重要。5.2 Python 解释器与虚拟环境不一致另一个容易被忽略的点是Python 解释器环境可能不是同一个。比如你在终端里执行python --version发现是 3.10但在 PyCharm 里右下角显示的却是另一个路径的 3.9 解释器这可能导致你安装的第三方包在 PyCharm 里全部找不到。导包报错如果涉及第三方库比如ModuleNotFoundError: No module named requests那就不是因为包结构问题而是解释器环境不对。先到 PyCharm 的Settings | Project | Python Interpreter里确认解释器路径然后在下方列表里看是否安装了对应的包。如果没有点击加号安装即可。pycharm 提供的方案本质是把虚拟环境所需的依赖统一管理。我在搞爬虫、数据分析项目时常会为每个项目单独建一个虚拟环境因为不同项目的包依赖版本经常冲突比如 A 项目需要pandas 1.xB 项目可能已经用到pandas 2.x。统一装到全局环境里很容易把环境搞乱某个包被升级之后另一个项目的代码就瘫了。5.3 缓存与索引问题如果你已经正确设置了 Sources Root并且 import 语句也很标准运行仍然报错那有可能是 PyCharm 的缓存没跟上。特别是当你最近大量移动过文件、改过目录名称、从版本控制工具里拉过代码分支PyCharm 的索引可能还是旧状态。这时候去菜单栏执行File | Invalidate Caches...然后选择Invalidate and Restart。PyCharm 会清空本地缓存并重启重新索引项目。很多莫名其妙的跳转失败、导入识别错误在清理缓存之后会有明显好转。还有一种情况是你项目里的__init__.py文件可能被误删导致 Python 不把这个目录当作包来对待。检查一下你的包目录下是否有__init__.py如果没有就直接新建一个空文件放进去。注意虽然 Python 3 支持命名空间包但在 PyCharm 的某些检查环节和旧代码里保留__init__.py仍然是最兼容的做法。6. 从命令行运行到 PyCharm 运行如何保持一致性6.1 使用终端时路径为什么没问题回到我前提到的那个迷思为什么代码在终端里运行正常在 PyCharm 里就报错因为终端里当你执行cd ~/awesome_project python examples/demo.pyPython 解释器会默认把执行脚本的目录examples加入sys.path注意不是当前目录awesome_project。这里有两种情况往往能跑通如果脚本里有from awesome import ...那么搜索路径里需要有awesome_project目录。在旧版 Python比如 3.9 之前如果你直接运行python examples/demo.pyPython 会自动把examples而非父目录加入sys.path所以awesome_project不一定在路径里。某些 Python 版本和运行方式下通过python -m examples.demo运行会把当前目录加入sys.path这样awesome_project就能被搜到。所以别以为“终端里能跑就是代码没问题”实际上可能是运气好Python 版本差异或者入口方式刚好让你碰上了。6.2 用 -m 参数运行模块很多问题能自然消失在包结构的前提下推荐从项目根目录使用-m参数来运行模块而不是直接指定脚本路径。比如cd ~/awesome_project python -m examples.demo这样 Python 会把当前工作目录作为sys.path的第一项examples和awesome都能被正确识别为顶级包。如果examples目录下有__init__.py并且demo.py里面用了相对导入-m方式同样能保证相对导入可用。在 PyCharm 的 Run Configuration 里也可以通过修改运行方式来达成类似效果把运行目标从script path改成module name然后填examples.demo再把 Working Directory 设置为awesome_project根目录。这样 PyCharm 的运行行为就跟上面命令行一致了。6.3 统一根目录别让入口文件散落太深开发新项目时尽量保持主入口文件在项目根目录层级不要太深。如果入口在五六层目录下面每次运行都要保证那一连串的目录都被正确标记而其中任何一个环节出错报错信息就会很莫名其妙。如果入口必须放在深层目录比如scripts/deploy/run_deploy.py那就把scripts的父级根目录标记为 Sources Root然后在代码里统一使用“从项目根开始的绝对导入”而不是一层层..往上找。举个例子如果你的项目结构是project/ ├── src/ │ ├── __init__.py │ └── logic.py └── scripts/deploy/ └── run_deploy.pyrun_deploy.py里你要导入logic.py我建议你在 Run Configuration 里把 Working Directory 和 Sources Root 都指到project然后代码写from src.logic import process_data不要写from ...src.logic import process_data后者虽然可能在某些相对路径场景下能跑但一旦运行入口换了位置就翻车。保持绝对导入能让你减少很多不必要的折腾。7. 高频报错速查对照这个表就能定位问题整理了一个速查表把我在实际开发和帮人看代码时遇到的高频导包问题归类放在一起。遇到报错时可以先对照这个表排查省得漫无目的地乱试。报错信息可能原因排查方向ModuleNotFoundError: No module named xxx模块不在sys.path的任何目录下检查是否标记 Sources Root、确认模块目录结构ModuleNotFoundError: No module named requests第三方库未安装或解释器环境不对检查当前解释器、在Settings里安装包ImportError: cannot import name yyy from xxx模块里没有你要导入的属性或函数检查拼写、是否循环导入、是否导入的是旧版本缓存ImportError: attempted relative import with no known parent package直接运行包内模块且使用相对导入改用脚本入口或用-m方式运行ModuleNotFoundError: No module named __main__.xxx在包内模块里错误地引用了入口模块名检查循环导入和相对导入的使用方式终端能跑但 PyCharm 报错PyCharm 的目录标记或运行配置不一致检查 Source Root、Run Configuration 的 Working DirectoryPyCharm 能跑但命令行报错命令行下没有PYTHONPATH或当前目录不对设置PYTHONPATH或改用python -m运行这个表被我贴过很多次基本覆盖了 80% 的导包问题。每次看到报错先别急按照表格定位到原因类别再对症处理成功率很高。关于“循环导入”这里也想多说一句它的报错形式通常是ImportError: cannot import name xxx from partially initialized module。出现的原因是两个模块互相 import比如 A 模块 import B 模块B 模块又 import A 模块。Python 在加载 A 的过程中发现需要加载 BB 又回头加载还没完全加载完的 A于是找不到 A 中尚未定义的变量。这种问题的解决办法通常是把公共函数或常量提取到一个新的模块里两个模块都去 import 它。把某个 import 语句放到函数内部延迟到真正需要时再导入。很多导包问题跟“不在同一个包”没直接关系但同样会让人误以为是路径问题。如果你在排查时方向错了就会浪费很多时间。8. 自己踩过的坑和总结坦白讲从刚学 Python 到现在我在导包这件事上交过的学费不在少数。印象最深的是一次爬虫项目代码写到一半发现爬虫模块和解析模块放在两个包下当时图省事到处写sys.path.append结果代码跑得歪歪扭扭后来项目要部署到服务器那些硬编码的本地路径瞬间全部失效。最后花了大半天把项目结构整个重构了一遍统一成标准包结构删掉所有sys.path.append问题才彻底根治。从那以后我再创建 PyCharm 项目时都会先花几分钟规划目录结构顺手把目录标记和解释器配置好而不急着写代码。代码写到一半再回头调整结构改起来比一开始就做对要麻烦得多。还有一个小技巧是如果项目里存在多个包需要互相同引用建议在项目根目录放一个__init__.py。哪怕这个文件是空的也能让 PyCharm 把所有包纳入同一个顶层命名空间下检查代码时的识别率会提高不少。关于 PyCharm 的目录标记我再补充一个细节。PyCharm 的 Mark Directory as 不止有 Sources Root还有 Test Sources Root、Resources Root 等。如果你的测试文件要导入项目源码里的模块把测试目录标记为Test Sources Root、把源码目录标记为Sources Root即可。这样跑 pytest 或者 unittest 时源码目录可以被搜索到测试目录也不需要重复加入sys.path。补充一句关于多人协作的建议如果项目是团队共同维护的尽量把导入路径规范写进 README或者提供一个统一的环境配置文件让每个人打开项目后先做一次目录标记。否则你本地跑得很欢同事拉下来代码跑不通就开始浪费时间。更聪明的做法是用刚才说的标准包结构加python -m运行方式这样无论谁拿到代码只要在根目录执行命令就能正常工作不依赖 IDE 的目录标记状态。最后再分享一个检查思路。导包报错之后不要只盯着报错信息本身关键是把下面几件事一次性排查清楚当前 PyCharm 用的 Python 解释器是哪个是不是项目虚拟环境项目要导入的目录有没有被正确设置为 Sources Root代码里有没有出现裸的相对路径或者硬编码的sys.path.appendPyCharm 的缓存是不是该清理了目录结构里的__init__.py有没有缺失把这些问题挨个过一遍绝大多数导包问题都能在十分钟内解决。剩下那些特别离奇的多半是代码中循环导入或者命名冲突这时候就得静下心来看实际报错的堆栈信息了。但这是另一个话题等下次有空再展开聊。