根本原因不是密码错误,而是twine未正确读取或覆盖了凭据配置;需确保.pypirc路径准确(如~/.pypirc)、权限严格(chmod 600)、section名与-r参数完全一致、username必须为__token__、repository URL为上传专用地址且格式无误。

根本原因不是密码错了,而是凭据没被 twine 真正读到,或者读到了但被覆盖/忽略。
twine upload 时找不到 .pypirc 或 PYPIRC_PATH 指向错误
twine 默认只认两个位置的凭据配置:~/.pypirc(Linux/macOS)或 %USERPROFILE%\pip\pip.ini(Windows),且文件必须叫这个名字、权限不能太宽松(比如 macOS 上 chmod 600 ~/.pypirc 是必须的)。如果你把配置放在其他路径(如 ./pypirc),又没用 --config-file 显式指定,twine 就直接跳过认证环节,报 403 Client Error: Forbidden。
- 检查是否真有
~/.pypirc,而不是~/.pypirc.bak或pypirc.example - 运行
twine upload --help查看 “Config file” 行,确认它实际加载的是哪个路径 - 用
twine upload --config-file ./my-pypirc dist/*强制指定,避免路径猜测 - Azure Pipelines 场景下,
TwineAuthenticate@0会设置PYPIRC_PATH环境变量 —— 但注意:它只影响该任务之后的步骤,且优先级高于~/.pypirc;如果之前已有同名环境变量,会被追加而非覆盖
凭据写在 [pypi] 而不是 [testpypi],却上传到了 TestPyPI
PyPI 和 TestPyPI 是两个完全独立的服务,各自需要独立的用户名/密码或 API token。很多人在 ~/.pypirc 里只配了 [pypi],然后执行 twine upload -r testpypi dist/*,结果 twine 找不到 [testpypi] 区块,就退回到匿名模式,触发 403。
-
~/.pypirc必须包含对应仓库名的 section,例如:
[distutils] index-servers = pypi testpypi [pypi] username = __token__ password = pypi-xxxxx... [testpypi] username = __token__ password = tkv1-xxxxx...
-r 参数值和配置中 section 名称是否**完全一致**(区分大小写,不允许多余空格)用了 API token 但没设 username = __token__
PyPI 要求使用 token 时,username 字段**必须严格写成 __token__**(两个下划线开头+结尾),哪怕你账户名是 alice。写成 alice 或留空,都会导致 403。
立即学习“Python免费学习笔记(深入)”;
- 错误示范:
username = alice→ 鉴权失败 - 正确写法:
username = __token__,password = pypi-axxxx... - token 本身不能带空格或换行;复制时容易多粘一个回车,建议用
echo "xxx" | tr -d '\n'检查 - 如果用 CI(如 Azure Pipelines),
TwineAuthenticate@0任务内部已自动填好__token__,但你要确保它输出的凭据没被后续脚本意外覆盖
仓库 URL 写错或协议不匹配
有些私有源(如 Azure Artifacts)要求 repository URL 以 https:// 开头,且末尾不能多斜杠、不能少路径段。twine 对 URL 格式敏感,稍有偏差就会 fallback 到默认 PyPI,再用你的私有凭据去撞官方 PyPI,必然 403。
- 检查
.pypirc中repository =的值是否与你在源管理界面看到的 “Upload URL” 完全一致 - 常见错误:
https://pkgs.dev.azure.com/org/_packaging/feed/pypi/simple/(这是安装用的 simple index)≠https://pkgs.dev.azure.com/org/_packaging/feed/pypi/upload/(这才是上传 endpoint) - URL 中不能含中文、空格或未转义的特殊字符;如有,需用
%20替换空格等
最容易被忽略的一点:twine 不会告诉你“凭据格式不对”,只会统一报 403。与其反复试密码,不如先用 twine check dist/*.whl 确保包本身合法,再用 twine upload --verbose -r xxx dist/* 看它到底连了哪个 URL、读了哪个配置文件——日志里那行 Using configuration from ... 才是真相入口。


















