pypa/pip · error · InvalidVersion

epoch must be non-negative integer, got

Error message

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

What it means

Raised by _validate_epoch() (version.py:265-270) when the epoch value passed to Version.from_parts() or Version.__replace__() is not a non-negative integer. The epoch defaults to 0 if falsy, but must otherwise be an int >= 0. The exception class is InvalidVersion (a ValueError subclass).

Solutions

  1. Pass epoch as a non-negative int (0, 1, 2, ...).
  2. Convert string epochs with int(epoch) and validate >= 0 before calling from_parts().
  3. Omit the epoch argument entirely if you want the default of 0.

Example fix

// before
Version.from_parts(epoch='2', release=(1, 0))
// after
Version.from_parts(epoch=2, release=(1, 0))
Defensive patterns

Strategy: type-guard

Validate before calling

from packaging.version import Version, InvalidVersion

def safe_from_parts(*, release, epoch=0, **kwargs):
    if not isinstance(epoch, int) or epoch < 0:
        raise TypeError(f"epoch must be a non-negative int, got {epoch!r}")
    return Version.from_parts(epoch=epoch, release=release, **kwargs)

Type guard

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

Try / catch

from packaging.version import InvalidVersion

try:
    v = Version.from_parts(epoch=epoch, release=(1,))
except InvalidVersion as e:
    if "epoch" in str(e):
        logger.error("Invalid epoch value: %s", epoch)
    raise

Prevention

When it happens

Trigger: Calling Version.from_parts(epoch=-1, release=(1,)) or Version.from_parts(epoch=1.5, release=(1,)) or Version.from_parts(epoch='1', release=(1,)). A string, float, or negative int triggers this.

Common situations: Passing an epoch as a string from a config file, a negative epoch due to a computation error, or a float epoch from division.

Related errors


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

Appendix: source

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

: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 f399c37189)