Python 环境变量配置:严格解析布尔值、空值和整数
定时任务里设置了 TASK_DRY_RUN=false,Python 却仍进入预览分支;批量大小留空后又在任务运行一半时才报错。环境变量传进来的是字符串,必须在任务入口明确转换并校验。
本文面向初学者到中级读者,用标准库写一个配置检查工具,区分未设置、空字符串和非法值,并加入命令行覆盖。工具输出最终配置,方便在真正的批处理开始前检查;它本身不执行批处理或修改数据文件。
1. 方法:先定义环境变量的规则
Python 的 os.environ 文档说明,环境变量的键和值都是字符串。bool("false") 和 bool("0") 都是 True,因为非空字符串为真;这遵循真值测试规则,并不会解析单词含义。
本文约定下面的规则。这些取值是工具自己的配置协议:
| 参数 | 未设置时 | 接受的已设置值 |
|---|---|---|
TASK_DRY_RUN |
True |
true、false、1、0,忽略大小写及首尾空白 |
TASK_BATCH_SIZE |
100 |
去掉首尾空白后,必须为 1–4 位 ASCII 数字,数值为 1–1000 |
- 未设置才采用默认值;空字符串和纯空白都报错,避免部署时漏填被悄悄忽略。
- 先验证这两个环境变量,再应用命令行覆盖。即使传入覆盖参数,已有的非法环境值仍会导致失败。
- 有效配置的优先级为:默认值 < 环境变量 < 显式命令行参数。
例如 0001 接受为整数 1,但 +1、1_000、小数和全角数字不接受。工具只读取表中的两个变量,不会自动加载 .env 文件,也不会检查其他变量的拼写。
2. 准备环境
需要已有的 Python 3.9 或更新版本,以及 Linux 下的 bash 或 zsh;无需安装第三方包。本文在 Python 3.11.4 上验证。
- 新建演示目录:
python3 --version
mkdir env-settings-demo
cd env-settings-demo
- 将下一节完整代码保存为
env_settings.py。如果只在定时任务里失败,先对照Python 定时任务环境排查确认启动入口和解释器。
3. 实现:一次读取、严格转换
"""Validate task environment settings and print JSON. Python 3.9+."""
import argparse
import json
import os
import re
def parse_bool(value):
token = value.casefold()
if token in ("true", "1"):
return True
if token in ("false", "0"):
return False
raise ValueError("expected true, false, 1, or 0")
def parse_batch(value):
message = "expected 1..1000 using 1..4 ASCII digits"
if not re.fullmatch(r"[0-9]{1,4}", value):
raise ValueError(message)
number = int(value)
if not 1 <= number <= 1000:
raise ValueError(message)
return number
def env_value(name, default, convert):
raw = os.environ.get(name)
if raw is None:
return default
value = raw.strip()
if not value:
raise ValueError(f"{name}: empty value; unset it to use the default")
try:
return convert(value)
except ValueError as exc:
raise ValueError(f"{name}: {exc}") from exc
def main():
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
parser.add_argument(
"--dry-run", action=argparse.BooleanOptionalAction, default=None,
help="override TASK_DRY_RUN; defaults to true when unset",
)
parser.add_argument("--batch-size", help="override TASK_BATCH_SIZE (1..1000)")
args = parser.parse_args()
try:
settings = {
"dry_run": env_value("TASK_DRY_RUN", True, parse_bool),
"batch_size": env_value("TASK_BATCH_SIZE", 100, parse_batch),
}
if args.dry_run is not None:
settings["dry_run"] = args.dry_run
if args.batch_size is not None:
settings["batch_size"] = parse_batch(args.batch_size.strip())
except ValueError as exc:
parser.error(str(exc))
print(json.dumps(settings, sort_keys=True))
if __name__ == "__main__":
main()
raw is None 区分未设置和空值,不能写成 os.environ.get(name) or default,否则空字符串会被当作默认值。整数转换先限制字符和长度,再检查范围,因此很长的数字也会得到固定的错误提示。
命令行使用 BooleanOptionalAction,生成 --dry-run 与 --no-dry-run。默认值设为 None,才能识别用户是否明确传入开关,不能用 if args.dry_run 判断是否覆盖。不要把布尔参数声明成 type=bool。
工具只输出这两个经过转换的字段,不打印整个进程环境。接入自己的批处理时,把任务调用放在配置验证成功之后,并在业务代码中实际处理 dry_run;参数名称本身不会阻止写操作。
4. 使用:默认值、环境变量和覆盖
- 查看帮助,再用不含这两个变量的子进程验证默认值:
python3 env_settings.py --help
env -u TASK_DRY_RUN -u TASK_BATCH_SIZE python3 env_settings.py
默认输出:
{"batch_size": 100, "dry_run": true}
env -u 只从本次子进程环境中移除指定变量,不改变当前终端。这样不会被先前导出的变量干扰。
- 显式设置两个环境变量;下面的赋值只用于这一次命令:
TASK_DRY_RUN=false TASK_BATCH_SIZE=25 python3 env_settings.py
输出:
{"batch_size": 25, "dry_run": false}
- 用命令行覆盖有效环境值:
TASK_DRY_RUN=false TASK_BATCH_SIZE=25 \
python3 env_settings.py --dry-run --batch-size 7
输出:
{"batch_size": 7, "dry_run": true}
- 检查空值与越界。每次运行后立即查看退出码;这两条命令都应该失败:
TASK_DRY_RUN='' TASK_BATCH_SIZE=25 python3 env_settings.py
printf 'exit=%s\n' "$?"
TASK_DRY_RUN=true TASK_BATCH_SIZE=1001 python3 env_settings.py
printf 'exit=%s\n' "$?"
两次退出码均为 2,标准错误分别包含 TASK_DRY_RUN: empty value 和 TASK_BATCH_SIZE: expected 1..1000,标准输出没有配置 JSON。成功时退出码为 0。不要用 || true 掩盖启动配置错误。
5. 验证:在独立子进程中检查边界
把下面保存为同目录的 check_env_settings.py,执行 python3 check_env_settings.py。它从子进程环境中移除已有的两个变量,因此不受终端配置影响,也不会修改当前进程之外的环境。
import json
import os
from pathlib import Path
import subprocess
import sys
script = Path(__file__).with_name("env_settings.py")
base = dict(os.environ)
for name in ("TASK_DRY_RUN", "TASK_BATCH_SIZE"):
base.pop(name, None)
def run(values, args=()):
return subprocess.run(
[sys.executable, str(script), *args],
env={**base, **values}, capture_output=True, text=True, timeout=5,
)
valid = [
({}, (), {"batch_size": 100, "dry_run": True}),
({"TASK_DRY_RUN": "false", "TASK_BATCH_SIZE": "25"}, (),
{"batch_size": 25, "dry_run": False}),
({"TASK_DRY_RUN": " TrUe ", "TASK_BATCH_SIZE": " 0001 "}, (),
{"batch_size": 1, "dry_run": True}),
({"TASK_DRY_RUN": "0", "TASK_BATCH_SIZE": "1000"}, ("--dry-run",),
{"batch_size": 1000, "dry_run": True}),
({"TASK_DRY_RUN": "1"}, ("--no-dry-run", "--batch-size", "7"),
{"batch_size": 7, "dry_run": False}),
]
for values, args, expected in valid:
result = run(values, args)
assert result.returncode == 0, result.stderr
payload = json.loads(result.stdout)
assert payload == expected
assert type(payload["dry_run"]) is bool
assert type(payload["batch_size"]) is int
assert result.stderr == ""
invalid = [
("TASK_DRY_RUN", ""), ("TASK_DRY_RUN", " "),
("TASK_DRY_RUN", "flase"), ("TASK_DRY_RUN", "yes"),
("TASK_BATCH_SIZE", ""), ("TASK_BATCH_SIZE", "0"),
("TASK_BATCH_SIZE", "1001"), ("TASK_BATCH_SIZE", "-1"),
("TASK_BATCH_SIZE", "2.5"), ("TASK_BATCH_SIZE", "1_000"),
("TASK_BATCH_SIZE", "12"), ("TASK_BATCH_SIZE", "9" * 5000),
]
for name, value in invalid:
result = run({name: value})
assert result.returncode == 2
assert result.stdout == ""
assert name in result.stderr
result = run({"TASK_DRY_RUN": "flase"}, ("--dry-run",))
assert result.returncode == 2 and result.stdout == ""
assert "TASK_DRY_RUN" in result.stderr
result = run({}, ("--batch-size", "0"))
assert result.returncode == 2 and result.stdout == ""
result = run({"TASK_DRY_RUN": "flase"}, ("--help",))
assert result.returncode == 0 and "--no-dry-run" in result.stdout
print("OK: defaults, booleans, boundaries, overrides, and errors")
预期输出:
OK: defaults, booleans, boundaries, overrides, and errors
测试同时核对返回码、JSON 的实际布尔类型、标准输出和标准错误,覆盖空值、拼写错误、数字边界、超长数字、显式关闭开关,以及非法环境值不能被覆盖隐藏的策略。--help 在环境值非法时也应正常工作,便于排查。
6. 常见错误与修复
| 现象 | 原因与处理 |
|---|---|
false 仍被当作真 |
删除 bool(raw) 或 type=bool,改用明确的允许值转换。 |
empty value |
变量已存在但没有内容。填入有效值;需要默认值时,用 unset TASK_DRY_RUN 或 unset TASK_BATCH_SIZE 移除对应变量。 |
expected true, false, 1, or 0 |
检查启动命令中的拼写;本文不接受 yes、off 等其他写法。 |
expected 1..1000 |
去掉符号、分隔符和小数,填写范围内的 ASCII 十进制整数。 |
| 改了终端变量,已运行的任务没有变化 | 在任务启动入口设置变量并重新启动进程;另一个终端的修改不会更新已有进程环境。 |
.env 文件写好了却没生效 |
本工具没有 .env 加载器。应由启动入口明确传入这两个环境变量。 |
参数增多、需要嵌套结构时,可以转用TOML 任务配置教程;不要在环境变量里自行设计复杂的分隔格式。若配置校验后仍遇到中文日志编码错误,继续检查标准输出编码。
7. 总结
在任务启动时区分未设置、空值和非法字符串,再统一转换布尔值与整数,可以让配置问题尽早暴露。为环境变量和命令行写清优先级,并用独立子进程检查退出码与实际输出。把通过校验的设置交给业务代码后,再开始真正的批处理。
- 原文作者:春江暮客
- 原文链接:https://www.bobobk.com/python-environment-variable-validation.html
- 版权声明:本作品采用 知识共享署名-非商业性使用-禁止演绎 4.0 国际许可协议 进行许可,非商业转载请注明出处(作者,原文链接),商业转载请联系作者获得授权。