A Python package in this repo has its version written in three different files. Each one serves a different consumer, and they must agree perfectly or the build breaks in a confusing way. This is why the release script touches all three.
The three homes
# 1. pyproject.toml, the build metadata [project] version = "0.12.0" # 2. src/pyrpc_core/__init__.py: the runtime value __version__ = "0.12.0" # 3. uv.lock: the workspace resolution name = "pyrpc-core" version = "0.12.0"
Three copies of the same truth, three different consumers.
pyproject.toml: the build identity
python -m build reads version from [project] to name the wheel and sdist. PyPI keys its releases on this string. If it is wrong, you publish the wrong version, or, worse, collide with an existing one.
__init__.py: the runtime truth
__version__ is the value a running Python process sees. The CLI prints it in pyrpc version, and the test for that command asserts the output contains "pyRPC version", deliberately not a specific number, because the test would otherwise break on every release. The two copies of the version are expected to stay equal but are consumed by entirely different systems.
The lockfile: the resolved truth
uv.lock records every workspace member's version. When pyproject.toml changes but the lockfile does not, uv sync reports the project as out of date, not an error, but a perpetually dirty working tree and confusing CI diffs. So the release process runs uv lock right after the bump, folding the version into the lockfile.
Could there be one source?
Modern tooling offers a single-source option: read the version dynamically from __init__.py via a dynamic = ["version"] PEP 621 declaration. pyRPC does not do that, the release script's job is to make the three copies agree, and it prefers the plainest, most buildable shape. The tradeoff is accepted consciously: a script guarantees the invariant instead of a build-time indirection that can surprise packaging tools.
The invariant
The rule the release process enforces: after a bump, pyproject.toml, __init__.py, and uv.lock all read the same version. The release script handles the first two; the lockfile sync handles the third. Three files, one truth, zero drift, that is the whole discipline.

pyRPC