第 09 章

密钥、代码风格与依赖:.env、Ruff 与 uv

把 API key 从代码里移到环境变量和 .env 文件,用 python-dotenv 自动加载;用 Ruff 一键格式化、查错并整理 import;再用 uv 完成建项目、装包、同步依赖的整套流程。

Secrets, Style and Dependencies: .env, Ruff and uv

Move API keys out of code into environment variables and a .env file loaded by python-dotenv, let Ruff format, lint and sort imports, and use uv to create projects, add packages and rebuild environments.

首发于 2025.10.21中 / EN
系统课程pythonenvironment-variablesruffuvworkflow
语言
字号
目录
篇内目录

这一章在讲什么

最后一章不讲语言本身,而讲「真正开始做项目时需要的那套工具」。三件事:怎样安全地存放 API key,怎样让代码风格保持一致,以及怎样用一个命令管理项目、依赖和虚拟环境。

课程把这三件事放在一起,是因为它们共同决定了「别人拿到你的项目能不能跑起来」。这一章对应长视频第 9 章,时间范围 4:44:33–5:15:15。

时间轴

时间内容
4:44:33为什么密钥不能写进代码
4:46:00环境变量与 os.environ.get
4:47:58export 的局限:只在当前终端会话有效
4:49:57.env 文件与 python-dotenv
4:52:56load_dotenv 的查找路径问题
4:55:45Ruff:格式化、检查、import 排序
5:01:23uv:现代的项目与依赖管理
5:03:38uv init、uv add、uv remove、uv sync
5:08:28完整工作流与收尾

为什么不能把密钥写进代码

课程展示了一段很常见的代码:

API_KEY = "sk-proj-xxxxxxxxxxxxxxxx"

一旦这段代码进入 Git,任何能访问仓库的人——同事、客户、公开仓库的浏览者——都能拿走这个 key。对很多服务来说,一个 key 就足以读取全部客户数据、替客户发消息、产生真实账单。

正确做法是:代码里只写「去哪里取」,密钥本身放在代码之外。

环境变量

环境变量存在操作系统里,不在 Python 文件里。程序通过标准库读取:

import os

api_key = os.environ.get("API_KEY")
database = os.environ.get("DATABASE_URL", "sqlite:///local.db")

print(api_key)
print(database)

没有设置时 os.environ.get 返回 None;第二个参数提供默认值,于是 database 会拿到默认字符串。

设置方式是在终端里 export:

export API_KEY="sk-proj-xxxxxxxx"

但课程马上指出了它的局限:export 只对当前终端会话有效。关掉终端、开一个新窗口,变量就没了,程序又只能读到 None。而且每次开新终端都要重新 export,非常繁琐。

(Windows PowerShell 的写法是 $env:API_KEY = "sk-...",作用范围同样只限当前会话。)

.env 文件

更实用的做法是把变量写进项目根目录的 .env:

API_KEY=sk-proj-xxxxxxxx
DATABASE_URL=postgresql://localhost:5432/app
APP_ENV=development

再用 python-dotenv 加载:

pip install python-dotenv
from dotenv import load_dotenv
import os

loaded = load_dotenv()      # 返回 True 表示找到了文件
print("loaded:", loaded)

api_key = os.environ.get("API_KEY")
print(api_key)

load_dotenv() 会在当前目录(有时也向上查找)寻找 .env,把它里面的键值对写进 os.environ。之后所有读取方式都和真正的环境变量一样。

如果 .env 不在当前目录,可以显式指定路径:

load_dotenv(".env")            # 当前目录
load_dotenv("config/.env")     # 子目录
load_dotenv("../.env")         # 上一层

找不到文件时 load_dotenv() 返回 False,不会抛异常。这是最容易出错的地方:程序能跑,但读到的 key 是 None,过一会儿才在调用 API 时失败。养成先打印返回值的习惯。

别忘了 .gitignore

.env 必须写进 .gitignore:

.env
.venv/
__pycache__/

否则 git add . 会把密钥一起提交。可以放一个 .env.example 进仓库,里面只写变量名和占位值,告诉别人需要配置哪些项,但不泄露真实值。

Ruff:格式化、检查与 import 排序

Python 的语法只有一种写法,但风格有很多种。PEP 8 定义了官方风格,而 Ruff 负责把代码自动整理成符合风格的样子。

安装很简单:在 VS Code 扩展市场搜索 Ruff,选 Astral 发布的那个,点安装。

然后在设置里开启保存时格式化:

  1. 打开设置(Ctrl+,),搜索 format on save,勾选;
  2. 搜索 default formatter,选择 Ruff。

之后保存文件时,Ruff 会自动修正空格、空行、缩进、行宽,以及列表与字典的换行方式。课程的原话是:「第一次看到的时候觉得太爽了」。

Ruff 同时提供三项能力:

  • 格式化:把代码排成统一风格;
  • 检查(lint):找出未使用的变量与 import、可疑写法等,对应编辑器里的黄色波浪线;
  • import 排序:把 import 按标准库、第三方库、本地模块分组排序。

自动修复通过命令面板完成:Ctrl+Shift+P → 输入 Ruff → 选择 Ruff: Fix all auto-fixable problems。未使用的 import os 会被直接删掉。

课程强调:这不会改变程序逻辑,只是让所有文件看起来像同一个人写的。团队协作时,代码评审就不用再讨论空格问题。

uv:一个工具管到底

传统流程要记三样东西:pip install 装包、requirements.txt 记录依赖、venv 管理虚拟环境。pip freeze 还会把依赖的依赖一起写进去,文件很快变得又长又乱。

uv 用一个工具替代全部:

uv --version

新建项目

uv init my_new_project
cd my_new_project
code .

生成的结构大致是:

my_new_project/
├─ .gitignore
├─ .python-version
├─ README.md
├─ main.py
└─ pyproject.toml

pyproject.toml 是现代 Python 项目的配置文件,记录项目名、Python 版本和依赖列表。uv 还会创建一个虚拟环境,只是把它藏在 .venv 里,不打扰你。

添加与删除依赖

uv add pandas numpy

这一条命令同时完成三件事:安装到虚拟环境、写入 pyproject.toml 的依赖列表、更新锁文件。速度比 pip 快很多。

uv remove pandas

删除依赖并同步更新配置。

重建环境

别人克隆了你的仓库,没有虚拟环境,只需要:

uv sync

uv 会读取 pyproject.toml 与锁文件,把环境完整还原。这正是「换台电脑也能跑」的实现方式。

交互式窗口

在 VS Code 里使用 Jupyter 交互窗口时,需要额外安装内核:

uv add ipykernel

然后选择项目对应的虚拟环境解释器,否则交互窗口会提示找不到内核。

课程给出的日常结论很干脆:装包用 uv add,删包用 uv remove,重建环境用 uv sync,其余交给 uv。可以完全不碰 pip、requirements.txt 和手动创建虚拟环境。

完整工作流

课程最后把所有工具串成一张清单,建议亲手走一遍:

  1. 用 uv init 创建项目,进入目录并在 VS Code 中打开;
  2. 用 uv add 安装需要的包;
  3. 写代码,保存时由 Ruff 自动格式化;
  4. 用 git init 初始化仓库,确认 .gitignore 包含 .venv/ 与 .env;
  5. 把密钥写进 .env,用 load_dotenv() 读取;
  6. git add、git commit、git push 推送到 GitHub;
  7. 别人用 git clone 加 uv sync 就能还原环境;
  8. 之后每天只需要:改代码 → 保存 → 提交 → 推送。

课程的说法是:这套流程正是他们工作室给客户做项目时的起点,很多教程省略它,是因为教程只做玩具项目。

本章结论

  • 密钥不能硬编码;用环境变量或 .env,代码里只留读取逻辑。
  • export 只在当前终端会话有效,.env 才能长期保存。
  • python-dotenv 的 load_dotenv() 返回布尔值,找不到文件时不会报错,需要主动检查。
  • .env 必须进 .gitignore;用 .env.example 说明需要哪些变量。
  • Ruff 一次解决格式化、lint 与 import 排序,配合「保存时格式化」几乎无需手动干预。
  • uv 用 init / add / remove / sync 覆盖项目创建、依赖管理与环境还原。
  • 完整工作流把代码、密钥、版本、依赖四件事固定下来,之后只剩日常的提交与推送。

理解检验

1. 为什么 export API_KEY=... 之后新开一个终端就读不到这个变量了?

参考答案:export 设置的是当前 shell 进程及其子进程的环境变量,生命周期与这个终端会话绑定。关闭终端后进程结束,变量随之消失;新开的终端是一个新进程,继承的是系统级或用户级的初始环境,不包含上一个会话里临时 export 的值。要跨会话保留,应当写进 .env 由程序加载,或写进 shell 的启动配置文件(如 ~/.zshrc、~/.bashrc),但后者仍不适合存放项目密钥。

2. load_dotenv() 返回 False 时程序会怎样?为什么这是一个危险的状态?

参考答案:返回 False 只说明没找到 .env 文件,函数本身不抛异常,程序继续运行。危险之处在于后续 os.environ.get("API_KEY") 会返回 None,而这个 None 可能一路传到调用 API 的地方才报错,错误信息往往与「文件没找到」无关,排查成本高。稳妥做法是检查返回值,或在取到 None 时立即抛出明确错误:

if not load_dotenv():
    raise RuntimeError(".env not found")

3. .env 已经写进 .gitignore,为什么还要提供一个 .env.example?

参考答案:.gitignore 只保证密钥不被提交,但别人克隆仓库后看不到项目需要哪些环境变量,程序会因为缺配置而失败。.env.example 列出所有变量名与占位值(如 API_KEY=your-key-here),不含真实密钥,既说明了配置清单,又不会泄露。新成员复制成 .env 再填自己的值即可。

4. Ruff 的「格式化」和「lint」有什么区别?各自解决什么问题?

参考答案:格式化只调整排版——空格、空行、缩进、行宽、换行方式——不改变代码含义,目的是让所有文件风格一致、易读。lint 则分析代码找出潜在问题,例如未使用的 import、未使用的变量、可疑的比较写法;它可能提示真正的 bug,也可能只是风格建议。Ruff 同时提供两者,还附带 import 排序:把标准库、第三方库与本地模块分组并排序。

5. uv add pandas 与 pip install pandas 相比,多做/少做了哪些事?

参考答案:pip install 只把包装进当前环境,不记录到项目配置里;要留下记录必须另外执行 pip freeze > requirements.txt,而且 freeze 会把所有间接依赖一并写死。uv add pandas 会同时:把包安装到项目虚拟环境、把直接依赖写入 pyproject.toml、更新锁文件(记录精确版本),并且速度快得多。结果是依赖声明干净(只列你主动要的),环境可由 uv sync 精确重建。

6. 同事克隆了你的项目,运行时报 ModuleNotFoundError: No module named 'pandas'。他应该执行什么?为什么这样能解决问题?

参考答案:在项目根目录执行 uv sync。它会读取 pyproject.toml 和锁文件,创建或更新 .venv,安装全部声明的依赖及其精确版本。ModuleNotFoundError 的原因是克隆下来只有源码,虚拟环境不在仓库里(被 .gitignore 排除),所以本机没有安装这些包;uv sync 正是用来从配置重建环境的命令。执行后还需要在编辑器中选中该项目的虚拟环境解释器,否则运行器可能仍在使用别的环境。

与前后章的关系

第 6 章把脚本整理成项目,第 7 章用类组织代码,第 8 章给代码加上版本与远程备份,这一章补齐密钥、风格与依赖三块拼图。四章合起来是一个可以直接交付的项目骨架:结构清楚、可复用、有历史、密钥安全、换机器能一键还原。

下一门课会在这个骨架上叠加 AI 能力——调用模型 API、构建 agent、接入数据源。那时你需要的不是新的语法,而是这里已经建立的工程习惯。

What this chapter covers

The final chapter is not about the language but about the tooling a real project needs. Three things: how to store API keys safely, how to keep code style consistent, and how to manage a project, its dependencies and its virtual environment with one command.

They belong together because they jointly decide whether someone else can run your project. This is chapter 9 of the long course, covering 4:44:33–5:15:15.

Timeline

TimeTopic
4:44:33Why keys must never live in code
4:46:00Environment variables and os.environ.get
4:47:58The limit of export: only the current terminal session
4:49:57The .env file and python-dotenv
4:52:56How load_dotenv searches for the file
4:55:45Ruff: formatting, linting, import sorting
5:01:23uv: modern project and dependency management
5:03:38uv init, uv add, uv remove, uv sync
5:08:28The complete workflow and wrap-up

Why keys must not live in code

The lecture shows a very common pattern:

API_KEY = "sk-proj-xxxxxxxxxxxxxxxx"

As soon as this file enters Git, anyone with access to the repository — colleagues, clients, visitors to a public repo — can take the key. For many services a single key is enough to read all customer data, send messages on the customer's behalf, or run up a real bill.

The rule: code says where to fetch the secret; the secret itself lives outside the code.

Environment variables

Environment variables live in the operating system, not in the Python file. The standard library reads them:

import os

api_key = os.environ.get("API_KEY")
database = os.environ.get("DATABASE_URL", "sqlite:///local.db")

print(api_key)
print(database)

If the variable is unset, os.environ.get returns None; the second argument supplies a default, so database falls back to the given string.

Setting one happens in the terminal:

export API_KEY="sk-proj-xxxxxxxx"

The lecture immediately points out the limitation: export only affects the current terminal session. Close the terminal or open a new window and the variable is gone, so the program reads None again. Re-exporting every time is tedious.

(In Windows PowerShell the equivalent is $env:API_KEY = "sk-...", which is also limited to the current session.)

The .env file

A more practical approach is to put the variables in .env at the project root:

API_KEY=sk-proj-xxxxxxxx
DATABASE_URL=postgresql://localhost:5432/app
APP_ENV=development

and load them with python-dotenv:

pip install python-dotenv
from dotenv import load_dotenv
import os

loaded = load_dotenv()      # True when the file was found
print("loaded:", loaded)

api_key = os.environ.get("API_KEY")
print(api_key)

load_dotenv() looks for .env in the current directory (sometimes also above it) and copies the key-value pairs into os.environ. Everything downstream reads them exactly like real environment variables.

If the file lives elsewhere, pass the path explicitly:

load_dotenv(".env")            # current directory
load_dotenv("config/.env")     # subdirectory
load_dotenv("../.env")         # one level up

When the file is missing, load_dotenv() returns False instead of raising. That is the most common trap here: the program runs, the key is None, and the failure only appears later at the API call. Check the return value.

Do not forget .gitignore

.env must be listed in .gitignore:

.env
.venv/
__pycache__/

Otherwise git add . commits the key. A .env.example can go in the repository with variable names and placeholder values only, telling others what to configure without exposing anything.

Ruff: formatting, linting, import sorting

Python has one syntax but many styles. PEP 8 defines the official conventions, and Ruff enforces them automatically.

Install the extension from the VS Code marketplace: search for Ruff and pick the one published by Astral.

Then enable format on save:

  1. Open settings (Ctrl+,), search for format on save, tick it.
  2. Search for default formatter and choose Ruff.

From then on, saving fixes spacing, blank lines, indentation, line length and the wrapping of lists and dictionaries. In the lecture's words, the first time you see it is delightful.

Ruff provides three capabilities:

  • Formatting: uniform layout across the codebase.
  • Linting: unused variables and imports, suspicious patterns — the yellow squiggles in the editor.
  • Import sorting: groups and orders imports by standard library, third-party and local modules.

Auto-fixes run from the command palette: Ctrl+Shift+P → type Ruff → Ruff: Fix all auto-fixable problems. An unused import os disappears immediately.

The lecture stresses that this changes no behaviour; it only makes every file look like it was written by the same person. In teams, code review stops arguing about whitespace.

uv: one tool for the whole job

The traditional flow requires three things: pip install for packages, requirements.txt for recording them, and venv for the environment. pip freeze also writes every transitive dependency, so the file grows long and messy.

uv replaces all of it:

uv --version

Creating a project

uv init my_new_project
cd my_new_project
code .

The generated layout is roughly:

my_new_project/
├─ .gitignore
├─ .python-version
├─ README.md
├─ main.py
└─ pyproject.toml

pyproject.toml is the modern project file, recording the name, Python version and dependencies. uv also creates a virtual environment, hidden in .venv so it stays out of the way.

Adding and removing dependencies

uv add pandas numpy

One command does three things: installs into the virtual environment, records the dependencies in pyproject.toml, and updates the lock file. It is far faster than pip.

uv remove pandas

Removes the dependency and updates the configuration.

Rebuilding the environment

When someone clones your repository without a virtual environment, they need only:

uv sync

uv reads pyproject.toml and the lock file and restores the environment exactly. That is what makes "it runs on another machine" true.

The interactive window

To use VS Code's Jupyter interactive window, add the kernel:

uv add ipykernel

Then select the project's virtual environment as the interpreter, or the window reports that no kernel is available.

The lecture's daily rule is blunt: uv add to install, uv remove to uninstall, uv sync to rebuild. pip, requirements.txt and manual virtual environments can be forgotten entirely.

The complete workflow

The lecture closes with a checklist worth walking through once:

  1. uv init the project, open the folder in VS Code.
  2. uv add the packages you need.
  3. Write code; Ruff formats it on save.
  4. git init, confirm .gitignore covers .venv/ and .env.
  5. Put secrets in .env and read them with load_dotenv().
  6. git add, git commit, git push to GitHub.
  7. Anyone else runs git clone plus uv sync to restore the project.
  8. Every day after that: edit, save, commit, push.

The lecture notes this is exactly the starting point their agency uses for client work; tutorials skip it because they build toy projects.

Takeaways

  • Never hardcode secrets; use environment variables or .env, and keep only the reading logic in code.
  • export lasts only for the current terminal session; .env persists.
  • load_dotenv() returns a boolean and does not raise when the file is missing — check it.
  • .env must be in .gitignore; ship a .env.example to document the required variables.
  • Ruff covers formatting, linting and import sorting; with format on save it becomes invisible.
  • uv's init / add / remove / sync handle project creation, dependencies and environment restoration.
  • The complete workflow fixes the four pillars — code, secrets, version, dependencies — leaving only everyday commits and pushes.

Comprehension check

1. Why does a variable set with export API_KEY=... disappear in a new terminal?

Reference answer: export sets an environment variable for the current shell process and its children, so its lifetime is tied to that session. Closing the terminal ends the process and the variable with it; a new terminal is a new process that inherits only the system or user environment, not values exported in the previous session. To persist across sessions, use .env and load it from the program, or put a line in the shell startup file (~/.zshrc, ~/.bashrc) — though that still is not the right place for project secrets.

2. What happens when load_dotenv() returns False, and why is that dangerous?

Reference answer: False only means the .env file was not found; the function raises nothing and execution continues. The danger is that later os.environ.get("API_KEY") returns None, and that None may travel all the way to the API call before failing, with an error message unrelated to the missing file. Check the return value, or fail fast:

if not load_dotenv():
    raise RuntimeError(".env not found")

3. .env is already in .gitignore. Why also ship a .env.example?

Reference answer: .gitignore only keeps the secret out of the repository; it does not tell a person who clones it which environment variables the project expects, and the program will fail on missing configuration. .env.example lists every variable name with a placeholder (for example API_KEY=your-key-here), documenting the required setup without exposing a real key. A new teammate copies it to .env and fills in their own values.

4. What is the difference between Ruff's "formatting" and "linting"?

Reference answer: formatting only changes layout — spaces, blank lines, indentation, line length, wrapping — without changing meaning, so every file looks consistent. Linting analyses the code for potential problems such as unused imports, unused variables or suspicious comparisons; it can point at real bugs as well as style issues. Ruff provides both, plus import sorting, which groups standard-library, third-party and local imports and orders them.

5. Compared with pip install pandas, what does uv add pandas do more or less?

Reference answer: pip install only installs into the current environment and records nothing in the project; keeping a record needs a separate pip freeze > requirements.txt, and freeze writes every transitive dependency as well. uv add pandas installs into the project environment, records the direct dependency in pyproject.toml, and updates the lock file with exact versions — much faster too. The result is a clean declaration of what you asked for, and an environment that uv sync can rebuild exactly.

6. A colleague cloned your project and got ModuleNotFoundError: No module named 'pandas'. What should they run, and why does it fix it?

Reference answer: run uv sync in the project root. It reads pyproject.toml and the lock file, creates or updates .venv, and installs every declared dependency at the recorded version. The error appears because cloning brings only source files — the virtual environment is excluded by .gitignore — so nothing is installed locally. After syncing, they should also select the project's virtual environment as the interpreter in the editor, otherwise the runner may still use a different environment.

How it connects

Chapter 6 turned scripts into a project, chapter 7 organized code with classes, chapter 8 added version history and a remote backup, and this chapter completes the set with secrets, style and dependencies. Together they form a project skeleton ready for delivery: clear structure, reusable code, history, safe secrets, and a one-command rebuild on another machine.

The next course layers AI on top of this skeleton — calling model APIs, building agents, connecting data sources. What you need then is not new syntax but the engineering habits established here.

来源