
设想如下场景:同事离职,走之前把他负责的项目“交接”给了我。
说是交接,其实就是在工位上指着屏幕跟我讲了40分钟:“这个config是配置,那个utils是工具函数,scripts下面有几个脚本但其中两个没用了别管它,tests……嗯测试我后来没怎么写……”
我当时就想,如果项目结构本身就能说话,何必费这口舌?
后来我接手第二个、第三个项目,发现每个项目目录长得都不一样。有的把所有代码扔一个文件夹,有的层层嵌套七八层,有的README写了一堆但关键信息一个没有。每次接手新项目,都得靠前任口头“翻译”。
痛定思痛,我整理了一套Python项目结构模板,自己用了大半年,团队里推了一阵子。不敢说是最佳实践,但至少交接的时候不用再口头解释了——打开项目,目录结构就是文档。
往期阅读>>>
Python 自动化管理Jenkins的15个实用脚本,提升效率
App2Docker:如何无需编写Dockerfile也可以创建容器镜像
Python 自动化识别Nginx配置并导出为excel文件,提升Nginx管理效率
先看全貌
my-project/├── README.md # 项目入口,必读├── pyproject.toml # 依赖管理 + 构建配置├── Makefile # 常用命令快捷入口├── .env.example # 环境变量模板├── .gitignore├── .github/│ └── workflows/│ └── ci.yml # CI配置├── docs/│ └── architecture.md # 架构设计文档├── scripts/│ ├── setup.sh # 一键初始化│ └── seed_data.py # 测试数据填充├── tests/│ ├── conftest.py # 公共fixture│ ├── unit/│ │ └── test_service.py│ └── integration/│ └── test_api.py├── src/│ └── my_project/│ ├── __init__.py│ ├── __main__.py # python -m my_project 入口│ ├── config.py # 配置加载│ ├── models/ # 数据模型│ ├── services/ # 业务逻辑│ ├── api/ # 对外接口│ └── utils/ # 工具函数└── docker/ ├── Dockerfile └── docker-compose.yml
别急着说“太复杂了”。我一个个讲,你看看这些文件是不是都有必要。
为什么用 src/ 布局?
这个可能是争议最大的点。很多人习惯了这样:
my-project/├── my_project/│ ├── __init__.py│ └── ...├── tests/└── pyproject.toml
项目根目录直接放包,看起来简单。但有个坑:你本地开发的时候,Python会从项目根目录找模块,这意味着你跑的是源码而不是安装后的包。本地跑得好好的,一打包发布就挂——因为安装路径、依赖关系可能都不一样。
把包放在 src/ 下面:
src/└── my_project/ ├── __init__.py └── ...
必须先 pip install -e . 安装才能用,本地开发环境和真实安装环境保持一致。这个做法Python官方文档和PyPA都推荐,叫 src layout。
一开始我也觉得多一层目录麻烦,但踩过一次“本地能跑装不上”的坑之后,就再也没回过头。
pyproject.toml 而不是 requirements.txt
以前项目里总有这些东西:
requirements.txtrequirements-dev.txtsetup.pysetup.cfgMANIFEST.in
哪个是干啥的?新来的人一脸懵。
pyproject.toml 一个文件搞定,PEP 518/621标准,Python社区已经定了:
[project]name = "my-project"version = "0.1.0"description = "项目描述"requires-python = ">=3.10"dependencies = ["fastapi>=0.100.0","sqlalchemy>=2.0","pydantic>=2.0",][project.optional-dependencies]dev = ["pytest>=7.0","pytest-cov","ruff","mypy",][project.scripts]my-project = "my_project.__main__:main"
依赖、元数据、入口点,全在这一个文件里。不需要再维护 setup.py 和 requirements.txt 两套东西。
开发环境安装:
pip install -e".[dev]"一行搞定,开发和运行依赖都装上。
Makefile — 团队新人的救命稻草
新同事入职第一天, clone完代码问你:“怎么跑?怎么测?怎么格式化?”
以前我得口头说:先装依赖,然后跑pytest,flake8检查代码风格……现在我只说一句:make。
.PHONY: install test lint format run cleaninstall:pip install -e ".[dev]"test:pytest tests/ -v --cov=my_projectlint:ruff check src/ tests/mypy src/format:ruff format src/ tests/run:python -m my_projectclean:rm -rf build/ dist/ *.egg-info .pytest_cache .mypy_cachefind . -type d -name __pycache__ -exec rm -rf {} +
make test 跑测试,make lint 检查代码,make run 启动项目。不用记命令,不用翻文档,make 一下就能看到所有可用命令。
Makefile的本质很简单:把团队约定变成可执行文件,而不是口头约定。
config.py — 别再把配置散落在各处
你有没有见过这种代码?
# service.pyDB_HOST = "192.168.1.100"# 硬编码API_KEY = "sk-xxxxx"# 直接写源码里TIMEOUT = int(os.environ.get("TIMEOUT", "30")) # 到处都是环境变量读取
配置散落在十几个文件里,改一个数据库地址要翻遍整个项目。更别提密钥直接写在代码里这种操作——我见过不止一次推到Git上去了。
用一个统一的配置模块:
# src/my_project/config.pyfrompydantic_settingsimportBaseSettingsfromfunctoolsimportlru_cacheclassSettings(BaseSettings):# 应用配置app_name: str = "my-project"debug: bool = False# 数据库db_host: str = "localhost"db_port: int = 5432db_name: str = "mydb"db_user: str = ""db_password: str = ""# 外部服务api_key: str = ""api_timeout: int = 30model_config = {"env_file": ".env"}@lru_cachedefget_settings() ->Settings:returnSettings()
所有配置集中管理,类型有校验,默认值有文档,环境变量自动读取。哪里需要配置就 from my_project.config import get_settings,不用到处 os.environ.get。
.env.example 是给新人看的模板:
# .env.example — 复制为 .env 后填写实际值DB_HOST=localhostDB_PORT=5432DB_NAME=mydbDB_USER=your_userDB_PASSWORD=your_passwordAPI_KEY=your_api_keyAPI_TIMEOUT=30
新人clone完项目,复制 .env.example 为 .env,填上自己的配置,就能跑起来。不用再问你“数据库地址是什么”“API密钥找谁要”。
models/、services/、api/ — 分层不是装腔作势
很多人写Python喜欢把所有逻辑塞一个文件,甚至一个函数从头写到尾。几百行的函数,数据库查询、业务判断、响应格式化全搅在一起。改一处怕牵连其他,最后谁都不敢动。
拆成三层:
src/my_project/├── models/ # 数据长什么样│ ├── __init__.py│ ├── user.py # 用户模型│ └── order.py # 订单模型├── services/ # 业务怎么干│ ├── __init__.py│ ├── user_service.py│ └── order_service.py├── api/ # 外部怎么调│ ├── __init__.py│ └── routes.py
各层干的事不一样:
models/:纯数据定义,不管业务逻辑。用SQLAlchemy定义ORM,用Pydantic定义Schema,别混着来。
services/:核心业务逻辑。查数据库、调外部接口、做计算,都在这里。这是项目最核心的部分,也应该是最好测试的部分——因为不依赖HTTP框架,mock起来方便。
api/:HTTP接口层。只负责接请求、调service、返响应。业务逻辑一行都别写。
举个具体的例子:
# models/user.py — 数据模型fromsqlalchemyimportColumn, String, Booleanfrommy_project.models.baseimportBaseclassUser(Base):__tablename__ = "users"id = Column(String, primary_key=True)name = Column(String)email = Column(String, unique=True)is_active = Column(Boolean, default=True)
# services/user_service.py — 业务逻辑frommy_project.models.userimportUserfrommy_project.configimportget_settingsclassUserService:def__init__(self, db_session):self.db = db_sessiondefget_user(self, user_id: str) ->User|None:returnself.db.query(User).filter_by(id=user_id).first()defdeactivate_user(self, user_id: str) ->bool:user = self.get_user(user_id)ifnotuser:returnFalseuser.is_active = Falseself.db.commit()returnTrue
# api/routes.py — 接口层fromfastapiimportAPIRouter, Dependsfrommy_project.services.user_serviceimportUserServicerouter = APIRouter()@router.get("/users/{user_id}")defget_user(user_id: str, service: UserService = Depends()):user = service.get_user(user_id)ifnotuser:return {"status": "not_found"}return {"id": user.id, "name": user.name}
这样分层的好处:改接口不影响业务逻辑,换数据库不影响接口,写单元测试可以绕过HTTP直接测service。每一层只关心自己的事,改动的范围才可控。
__main__.py — 统一入口
之前见过各种启动方式:有人 python app.py,有人 python run.py,有人 python -m some.module.that.is.deep.nested。每个项目不一样,每次都得问。
用 __main__.py:
# src/my_project/__main__.pyfrommy_project.configimportget_settingsfrommy_project.apiimportcreate_appdefmain():settings = get_settings()app = create_app(settings)app.run(host="0.0.0.0", port=8000)if__name__ == "__main__":main()
启动方式统一:
python -m my_project不用记入口文件叫啥,不用翻README找启动命令。python -m 包名,就这一种。
tests/ — 分清单元测试和集成测试
很多项目的 tests/ 目录就是一个大杂烩,单元测试和集成测试混在一起。跑起来有的快有的慢,只想跑快的还得分着挑。
分开来:
tests/├── conftest.py # 公共fixture├── unit/ # 不依赖外部,秒级完成│ └── test_user_service.py└── integration/ # 需要数据库/外部服务 └── test_api.py
# tests/conftest.pyimportpytestfrommy_project.services.user_serviceimportUserService@pytest.fixturedefuser_service():"""单元测试用的mock service"""returnUserService(db_session=MockSession())
日常开发跑单元测试,速度快:
pytest tests/unit/ -vCI或者上线前跑全量:
pytest tests/ -v这比一锅炖的体验好太多了。
scripts/ — 那些只跑一次的脚本也得有个家
每个项目总有几个“偶尔用一下”的脚本:初始化数据库、填充测试数据、迁移旧数据、清理临时文件。以前这些脚本要么散落在项目根目录,要么在某个人的电脑上,要么压根找不到。
给它们一个固定位置:
scripts/├── setup.sh # 一键初始化开发环境└── seed_data.py # 填充测试数据
setup.sh 是新人入职的第一条命令:
#!/bin/bashset-eecho">>> 创建虚拟环境"python -m venv .venvsource .venv/bin/activateecho">>> 安装依赖"pip install -e".[dev]"echo">>> 配置环境变量"cp .env.example .envecho">>> 初始化数据库"python -m my_project.db initecho">>> 完成!运行 make run 启动项目"
这样不管谁来,clone完跑一个 bash scripts/setup.sh 就能开始干活。
docs/ — 不是让你写长篇大论
很多项目要么没文档,要么文档是过时的。问题出在“文档”这个词太吓人了,好像得写个几十页的规范似的。
我 docs/ 里就放一个文件:architecture.md。
内容不超过一页,写三件事就够了:
这个项目是干啥的(一两句话)
目录结构每个部分是干啥的(就是上面那些解释)
关键决策和原因(比如为什么选FastAPI不选Flask,为什么用src布局)
# 项目架构## 简介内部订单管理系统,提供REST API给前端调用。## 目录说明- `src/my_project/models/` — 数据模型(ORM + Schema)- `src/my_project/services/` — 业务逻辑- `src/my_project/api/` — HTTP接口- `tests/unit/` — 单元测试(不需要数据库)- `tests/integration/` — 集成测试(需要数据库)## 关键决策- 使用FastAPI:需要异步支持 + 自动生成API文档- src布局:避免本地开发和安装环境不一致- Pydantic Settings:统一配置管理,类型安全
够用了。以后有人问“这个项目为什么这样组织”,指这个文件就行。
最后说个事
其实项目结构这件事,最怕的不是“不知道怎么组织”,而是“每个人都按自己的习惯来”。
模板的意义从来不是“这个结构最牛”,而是“大家用同一套”。当所有项目长得差不多,你打开任何一个都能在10秒内找到想要的东西,这才是效率。
代码能自己说话,就不用人替它解释。
https://ima.qq.com/wiki/?shareId=f2628818f0874da17b71ffa0e5e8408114e7dbad46f1745bbd1cc1365277631c

夜雨聆风