pypa/pip · error · InvalidVersion

epoch must be non-negative integer, got {epoch}

Error message

epoch must be non-negative integer, got {epoch}

What it means

Raised by `_validate_epoch(value)` when the `epoch` argument to `Version.from_parts(epoch=...)` or `Version.__replace__(epoch=...)` is not a non-negative `int` (a falsy value like `0`/`None` is allowed and normalized to `0`; any other type or negative int is rejected). PEP 440 epochs are non-negative integers (the `N!` syntax).

Source

Thrown at src/pip/_vendor/packaging/version.py:271

:meta hide-value:
"""


# Validation pattern for local version in replace()
_LOCAL_PATTERN = re.compile(r"[a-z0-9]+(?:[._-][a-z0-9]+)*", re.IGNORECASE | re.ASCII)

# Fast path: If a version has only digits and dots then we
# can skip the regex and parse it as a release segment
_SIMPLE_VERSION_INDICATORS = frozenset(".0123456789")


def _validate_epoch(value: object, /) -> int:
    epoch = value or 0
    if isinstance(epoch, int) and epoch >= 0:
        return epoch
    msg = f"epoch must be non-negative integer, got {epoch}"
    raise InvalidVersion(msg)


def _validate_release(value: object, /) -> tuple[int, ...]:
    release = (0,) if value is None else value
    if (
        isinstance(release, tuple)
        and len(release) > 0
        and all(isinstance(i, int) and i >= 0 for i in release)
    ):
        return release
    msg = f"release must be a non-empty tuple of non-negative integers, got {release}"
    raise InvalidVersion(msg)


def _validate_pre(value: object, /) -> tuple[Literal["a", "b", "rc"], int] | None:
    if value is None:
        return value
    if isinstance(value, tuple) and len(value) == 2:

View on GitHub (pinned to d7d0d0a394)

Solutions

  1. Pass a non-negative `int`: `epoch=2`.
  2. Coerce from string with `int(value)` and assert `>= 0` first.
  3. Omit `epoch` (defaults to `0`) unless you genuinely need a non-zero epoch.
  4. For string-based input, build the version via `Version('2!1.0')` instead of `from_parts`.

Example fix

// before
Version.from_parts(epoch='2', release=(1, 0))  # str -> raises

// after
Version.from_parts(epoch=int('2'), release=(1, 0))
# or simply
Version('2!1.0')
Defensive patterns

Strategy: type-guard

Validate before calling

def is_valid_epoch(value: object) -> bool:
    return value is None or (isinstance(value, int) and not isinstance(value, bool) and value >= 0)

Type guard

def is_non_negative_int(value: object) -> bool:
    return isinstance(value, int) and not isinstance(value, bool) and value >= 0

Try / catch

from pip._vendor.packaging.version import Version, InvalidVersion

try:
    v = Version.from_parts(epoch=epoch, release=release)
except InvalidVersion as e:
    if 'epoch' in str(e):
        epoch = 0  # or int(epoch) after validation
        v = Version.from_parts(epoch=epoch, release=release)
    raise

Prevention

When it happens

Trigger: Calling `Version.from_parts(epoch=-1, release=(1,0))`, `Version.from_parts(epoch=1.5, release=(1,0))`, or `Version.from_parts(epoch='1', release=(1,0))`. The validator does `isinstance(epoch, int) and epoch >= 0`.

Common situations: Passing a string version number from config without conversion; a negative epoch from a buggy computation; a float epoch from division.

Related errors


AI-assisted analysis of pypa/pip@d7d0d0a394 (2026-08-04). Data as JSON: /data/errors/335c6d1a2bb42f06.json. Report an issue: GitHub.