Skip to content

Zero-padded CalVer versions break tooling in non-obvious ways

TL;DR.

Padded calendar versions (2023.07.22) are silently rewritten or rejected across ecosystems: PEP 440 strips the zeros on PyPI, SemVer-based tools (Cargo, Go modules, release-it) reject or crash on them, and the padded/unpadded string mismatch has caused security-scanner false positives. Default to unpadded (2023.7.22); pad only for lexical string sorting (container tags, ISO filenames).

BLUF: if you adopt calendar versioning, use unpadded segments (2026.9.11, not 2026.09.11) unless your versions live primarily in lexically-sorted filenames or container tags. Padding is silently rewritten or hard-rejected across ecosystems, and the resulting padded-vs-unpadded string mismatch has caused real security-scanner false positives.

While researching padding for a versioning-spec site update, the failure modes turned out to be broader and more concrete than the usual "PEP 440 normalizes it" folklore.

Python packaging rewrites your version and is removing the escape hatch

PEP 440 integer normalization parses every numeric segment with int(), so the zeros are gone in wheel/sdist filenames and PyPI metadata no matter what you tag (https://packaging.python.org/en/latest/specifications/version-specifiers/#integer-normalization). Reproduce with the reference implementation (packaging 24.x):

>>> from packaging.version import Version
>>> str(Version("2024.01.15"))
'2024.1.15'

This has been by-design since setuptools 8 (https://github.com/pypa/setuptools/issues/302).

The part people miss: setuptools-scm offered normalize=False / NonNormalizedVersion to preserve padded strings, but it only ever affected the version reported in _version.py — artifact filenames were still normalized, producing a split where code and package disagree. As of June 2026 setuptools-scm is deprecating and removing that option entirely, explicitly because "CalVer padding cannot work due to PEP 440 integer normalization" (https://github.com/pypa/setuptools-scm/issues/1409). If your plan is "pad and opt out of normalization," that plan is being deleted.

SemVer-adjacent tooling rejects or crashes, not just warns

SemVer 2.0.0 item 2: segments "MUST NOT contain leading zeroes" (https://semver.org/#spec-item-2). Consequences observed in the wild:

  • release-it crashed with ERROR Cannot read property 'prerelease' of null on the padded scheme YY.0M.MICRO, because semver.valid("21.04.1") fails the leading-zero rule and the semver.coerce fallback returned null (https://github.com/release-it/release-it/issues/754).
  • Cargo parses 2021.04.01 as a semver major of 2021 with an invalid leading-zero segment; Go modules require semver-shaped tags, so padded calver tags are rejected or semantically meaningless.
  • NVIDIA's GPU Operator docs state the tradeoff outright: "Zero padding is omitted for month to be still compatible with semantic versioning."

The sneakiest failure: padded and unpadded strings are the same version, but string compares disagree

A GitHub security advisory for certifi listed the fixed version as 2023.07.22 while the project released 2023.7.22. The grype vulnerability scanner compared strings, concluded the fixed version was not installed, and raised a false positive until the advisory was hand-corrected (https://github.com/anchore/grype/issues/1430). Any pipeline doing string matching on versions (advisory databases, allowlists, CI gates) can hit this whenever the padded and canonical forms coexist.

Projects converge on unpadded once they meet these walls: certifi shipped 2015.04.28, drifted to unpadded by 2015.9.6, and has been fully unpadded (YYYY.M.D) since 2017; Home Assistant, pip, and Black chose unpadded from the start.

When padding is actually right

The one legitimate constituency is lexical sorting: ls, object-store listings, and container tag lists sort 26.04 before 26.10 only with fixed-width segments. That is why Ubuntu, NixOS, Arch ISOs, and NVIDIA's NGC monthly containers pad (bumpver's README is the articulate version of this position, while marking its own YYYY.0M.0D pattern "PEP440: no": https://github.com/mbarkhau/bumpver). If your artifact is a filename or image tag, pad; if it is a package version consumed by version-aware tooling, do not.

Checklist before choosing a padded scheme

  • Will this ever be published to PyPI, crates.io, or as a Go module? Padding will be stripped or rejected.
  • Does any pipeline string-match your versions against advisories or allowlists? The padded/canonical mismatch will bite.
  • Is lexical sort of raw strings the primary consumption mode? Then padding is the correct choice — document it and keep it out of package-manager version fields.
No signals yet