一个 Python 项目文件放哪、目录怎么组织,看似小事,却直接影响包能不能装、测试能不能跑、新人能不能快速上手。不同类型的项目——一次性脚本、可安装包、内部库、Django/Flask Web 应用——各有成熟的布局套路。下面逐一拆解,并给出可直接套用的目录模板和配置示例。
一次性脚本:别小看单文件
数据分析、运维小工具、一次性迁移脚本,往往就一个 .py 文件。但"一个文件"不代表可以乱堆。
推荐布局:
my_script/
├── my_script.py
├── requirements.txt
├── README.md
└── .gitignore
关键点:
requirements.txt锁依赖,哪怕只有两行也别省。半年后重跑时你会感谢自己。README.md写清楚运行方式:python my_script.py --input data.csv。- 脚本内部用
argparse而不是硬编码路径,方便复用。
一个可复用的脚本骨架:
#!/usr/bin/env python3
"""一次性数据清洗脚本 —— 把原始 CSV 中的空行删掉并输出清洗后文件."""
import argparse
import csv
import sys
def clean_csv(input_path: str, output_path: str) -> None:
with open(input_path, newline="", encoding="utf-8") as fin:
reader = csv.reader(fin)
rows = [row for row in reader if any(cell.strip() for cell in row)]
with open(output_path, "w", newline="", encoding="utf-8") as fout:
writer = csv.writer(fout)
writer.writerows(rows)
print(f"清洗完成:{len(rows)} 行写入 {output_path}")
def main() -> None:
parser = argparse.ArgumentParser(description="清洗 CSV 空行")
parser.add_argument("--input", required=True, help="原始 CSV 路径")
parser.add_argument("--output", required=True, help="输出 CSV 路径")
args = parser.parse_args()
clean_csv(args.input, args.output)
if __name__ == "__main__":
main()
把业务逻辑放在函数里、入口放在 main(),以后想改成可安装包时只需加一个 setup.py,代码本身不用动。
可安装包:标准 src 布局
当你需要 pip install 自己的包、发布到 PyPI 或内部 artifact 仓库时,布局必须满足打包工具的约定。目前社区主流是 src layout——源码放在 src/ 子目录下,而不是项目根目录。
my_package/
├── src/
│ └── my_package/
│ ├── __init__.py
│ ├── core.py
│ └── utils.py
├── tests/
│ ├── test_core.py
│ ├── test_utils.py
├── pyproject.toml
├── README.md
├── LICENSE
└── .gitignore
为什么用 src layout?根目录下直接放包目录(flat layout)容易导致一个致命问题:本地开发时 import my_package 导入的是源码目录而非已安装的包,测试可能跑过了但安装后行为不同。src/ 强制你先安装再测试,杜绝这类隐患。
pyproject.toml 最小可运行示例:
[build-system]
requires = ["setuptools>=68.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "my_package"
version = "0.1.0"
description = "一个演示标准布局的示例包"
requires-python = ">=3.9"
dependencies = []
[project.optional-dependencies]
dev = ["pytest>=7.0", "ruff"]
[tool.setuptools.packages.find]
where = ["src"]
安装并测试的完整流程:
# 创建虚拟环境并安装包(可编辑模式)
python -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
# 运行测试 —— 此时 import 的是已安装的包
pytest tests/
内部库:公司私有包的布局
内部库不发布到 PyPI,只给公司其他项目引用。布局和可安装包基本一致,区别在于分发方式:通常用 Git 仓库直接引用,或推到私有 PyPI 服务器。
pyproject.toml 里加一行标记私有:
[project]
name = "company_auth_lib"
version = "0.3.2"
# 私有包不设 classifiers 里的 PyPI 发布标记
classifiers = ["Private :: Do Not Upload"]
其他项目引用时,requirements.txt 或 pyproject.toml 直接写 Git 地址:
[project]
dependencies = [
"company_auth_lib @ git+ssh://git@gitlab.internal.dev/company-auth-lib.git@v0.3.2",
]
目录结构和可安装包一致,只是不需要 LICENSE(公司内部协议替代)和 PyPI 发布配置。
Flask 应用:两层结构起步
Flask 应用从小项目到大项目跨度很大。起步时一个 app.py 就能跑,但到需要蓝图、模板、静态文件时,必须升级布局。
推荐的中等规模布局:
flask_app/
├── app/
│ ├── __init__.py # create_app() 工厂函数
│ ├── models.py
│ ├── auth/
│ │ ├── __init__.py
│ │ ├── routes.py
│ │ ├── forms.py
│ ├── blog/
│ │ ├── __init__.py
│ │ ├── routes.py
│ │ ├── templates/
│ │ │ └── blog/
│ │ │ ├── list.html
│ │ │ ├── detail.html
│ ├── templates/ # 全局模板
│ │ └── base.html
│ ├── static/
│ │ ├── css/
│ │ ├── js/
├── tests/
│ ├── conftest.py
│ ├── test_auth.py
│ ├── test_blog.py
├── run.py # 入口:from app import create_app; app = create_app(); app.run()
├── pyproject.toml
├── requirements.txt # 生产依赖锁定
└── .flaskenv # FLASK_APP=run.py 等环境变量
工厂函数示例——app/__init__.py:
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
db = SQLAlchemy()
def create_app(config_name: str = "default") -> Flask:
app = Flask(__name__)
app.config.from_object(f"app.config.{config_name}")
db.init_app(app)
from app.auth.routes import auth_bp
from app.blog.routes import blog_bp
app.register_blueprint(auth_bp, url_prefix="/auth")
app.register_blueprint(blog_bp, url_prefix="/blog")
return app
蓝图注册让每个功能模块自包含:自己的路由、模板、表单。新增模块只需加一个蓝图目录和一行 register_blueprint,不用改其他代码。
Django 应用:项目与应用的分层
Django 的约定更明确:project 是整体配置容器,app 是功能模块。两者目录层级不同。
django_project/
├── manage.py
├── mysite/ # project 包
│ ├── __init__.py
│ ├── settings/
│ │ ├── base.py
│ │ ├── dev.py
│ │ ├── prod.py
│ ├── urls.py
│ ├── wsgi.py
│ ├── asgi.py
├── blog/ # app
│ ├── __init__.py
│ ├── models.py
│ ├── views.py
│ ├── urls.py
│ ├── admin.py
│ ├── migrations/
│ ├── templates/blog/
│ │ ├── post_list.html
│ │ ├── post_detail.html
│ ├── tests/
│ │ ├── test_models.py
│ │ ├── test_views.py
├── requirements/
│ ├── base.txt
│ ├── dev.txt
│ ├── prod.txt
└── pyproject.toml
几个容易踩的坑:
- 不要把所有 models 堆在 project 目录下。每个 app 应有自己的
models.py,Django 的迁移系统按 app 管理迁移文件。 - settings 拆分比单文件
settings.py更实用。base.py放公共配置,dev.py和prod.py分别覆盖数据库、密钥等。启动时用DJANGO_SETTINGS_MODULE=mysite.settings.prod切换。 - app 的 templates 放在 app 目录内(
blog/templates/blog/),而不是全局templates/。这样模板命名空间天然隔离,避免list.html冲突。
创建新 app 并注册的命令:
# 在项目根目录执行
python manage.py startapp comments
# 然后在 settings/base.py 的 INSTALLED_APPS 里加上 "comments"
选布局的决策清单
拿到一个新项目时,按这个顺序判断:
| 项目类型 | 布局选择 | 核心标志 |
|---|---|---|
| 一次性脚本,不复用 | 单文件 + requirements.txt | if __name__ == "__main__" |
| 会迭代的数据工具 | 脚本骨架 + argparse,考虑升级为包 | 函数拆分、有 CLI 入口 |
| 给他人 pip install 的库 | src layout + pyproject.toml | src/ 子目录、pytest 跑已安装包 |
| 公司内部共享库 | 同 src layout,Git/私有 PyPI 分发 | Private :: Do Not Upload |
| Flask 中小应用 | 工厂函数 + 蓝图目录 | create_app()、register_blueprint() |
| Django 应用 | project/app 分层 + settings 拆分 | manage.py、多 app 目录 |
最后一条通用原则:不管哪种布局,测试目录永远独立于源码目录。tests/ 放在项目根目录,和 src/ 或 app/ 平级。这样测试可以独立运行,也不污染包的安装内容。
布局不是审美问题,是工程约束。选对了,打包、测试、部署各环节才能顺畅衔接;选错了,每个环节都会冒出意想不到的路径问题。按项目类型套用对应模板,再根据团队规模微调,比从零摸索省掉大量排错时间。