🐍 PythonDevOps / CI·CD
Semver Validation in Python
Validate semantic version strings in Python — parse MAJOR.MINOR.PATCH components, detect pre-release and build metadata, enforce stable-only releases on the main branch, and validate dependency manifests before publishing.
Also available in Node.js
Contents
1. Semver anatomy
1 . 2 . 3 - beta.1 + build.42 │ │ │ │ └─ Build metadata (ignored in precedence) │ │ │ └─────────── Pre-release identifier │ │ └──────────────── PATCH — backwards-compatible bug fixes │ └────────────────────── MINOR — backwards-compatible new features └──────────────────────────── MAJOR — breaking changes
2. API response structure
Endpoint: GET /v0/semver?value=…
{ "valid": true, "version": "1.2.3-beta.1+build.42", "major": 1, "minor": 2, "patch": 3, "prerelease": "beta.1", "build": "build.42", "formatted": "1.2.3-beta.1+build.42", "isStable": false }
isStable is True when MAJOR ≥ 1 and prerelease is empty.
3. Valid and invalid examples
| Input | Valid? | Note |
|---|---|---|
| 1.0.0 | ✓ | Stable release |
| 1.2.3-alpha | ✓ | Pre-release |
| 1.2.3-beta.1+sha.abc | ✓ | Pre-release + build metadata |
| 0.1.0 | ✓ | Initial development (isStable: False) |
| v1.2.3 | ✗ (strip v first) | Leading v — not valid semver, strip it |
| 1.2 | ✗ | Missing PATCH component |
| 1.2.3.4 | ✗ | Extra component |
| 1.2.3- | ✗ | Empty pre-release identifier |
| ^1.2.3 | ✗ | npm range specifier — not semver |
| 1.2.x | ✗ | Wildcard — not semver |
4. CI/CD gate — stable-only releases
💡Use
isStable to prevent accidentally publishing a pre-release version from your main/production branch. Run this check in your GitHub Actions workflow before pip publish or docker push.# scripts/check_version.py — run in CI before publish import asyncio import os import sys from isvalid_sdk import IsValidConfig, create_client async def main(): version = os.environ.get("RELEASE_VERSION", "").lstrip("v") if not version: print("ERROR: RELEASE_VERSION not set", file=sys.stderr) sys.exit(1) iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"])) result = await iv.semver(version) if not result.valid: print(f"ERROR: Invalid semver: {version!r}", file=sys.stderr) sys.exit(1) if not result.is_stable: print( f"ERROR: Pre-release {result.version!r} cannot be published to PyPI stable. " f"Use --pre flag or publish to TestPyPI.", file=sys.stderr, ) sys.exit(1) print(f"✓ Version {result.version} is valid and stable — proceeding with publish") asyncio.run(main())
5. Dependency manifest validation
import tomllib async def validate_pyproject(path: str = "pyproject.toml") -> list[dict]: """Validate all pinned versions in pyproject.toml dependencies.""" with open(path, "rb") as f: data = tomllib.load(f) deps = data.get("project", {}).get("dependencies", []) # Parse "package==1.2.3" → extract version pinned = {} for dep in deps: if "==" in dep: name, ver = dep.split("==", 1) pinned[name.strip()] = ver.strip() return await validate_dependencies(pinned)
6. Edge cases
⚠️Strip leading
v before sending to the API. Git tags often use v1.2.3 but semver spec does not allow the prefix. Call version.lstrip("v") first.# Common edge cases to handle cases = [ ("v1.2.3", "strip v → valid"), ("1.0.0-0.3.7", "numeric pre-release — valid"), ("1.0.0-x.7.z.92", "alphanumeric pre-release — valid"), ("1.0.0+20130313144700", "build metadata only — isStable: True"), ("1.0.0-beta+exp.sha.5114f85", "pre-release + build — isStable: False"), ] for raw, note in cases: cleaned = raw.lstrip("v") result = await iv.semver(cleaned) print(f"{raw:35} valid={result.valid} stable={result.is_stable} # {note}")
7. Full example with asyncio
import asyncio import os from isvalid_sdk import IsValidConfig, create_client iv = create_client(IsValidConfig(api_key=os.environ["ISVALID_API_KEY"])) async def validate_version(version: str) -> dict: # Strip leading "v" if present — "v1.2.3" → "1.2.3" cleaned = version.lstrip("v") result = await iv.semver(cleaned) if not result.valid: raise ValueError(f"Invalid semver: {version!r}") return result async def gate_main_branch_release(version: str) -> None: """Raise if version is pre-release — only stable versions go to main.""" result = await validate_version(version) if not result.is_stable: raise ValueError( f"Pre-release version {result.version!r} cannot be released on main. " f"Pre-release: {result.prerelease}" ) print(f"✓ Stable release {result.version} approved for main branch") # Validate a batch of dependency versions async def validate_dependencies(deps: dict[str, str]) -> list[dict]: tasks = {name: iv.semver(ver.lstrip("v")) for name, ver in deps.items()} results = await asyncio.gather(*tasks.values(), return_exceptions=True) return [ { "package": name, "version": ver, "valid": not isinstance(r, Exception) and r.valid, "stable": not isinstance(r, Exception) and r.is_stable, "error": str(r) if isinstance(r, Exception) else None, } for (name, ver), r in zip(deps.items(), results) ] async def main(): await gate_main_branch_release("2.1.0") # OK # await gate_main_branch_release("2.1.0-beta.1") # raises deps = { "fastapi": "0.110.0", "pydantic": "2.6.4", "httpx": "0.27.0-beta.1", } report = await validate_dependencies(deps) for row in report: stable = "stable" if row["stable"] else "pre-release" print(f" {row['package']}@{row['version']} — {stable}") asyncio.run(main())
8. Summary checklist
✓Strip leading "v" before validation (v1.2.3 → 1.2.3)
✓Check isStable before publishing to PyPI stable
✓Block pre-release versions on main/production branches
✓Validate all pinned versions in pyproject.toml
✓Use asyncio.gather for batch dependency validation
✓Reject npm range specifiers (^, ~, x wildcards)
✓Run version gate in CI before package publish step
✓Return 422 with reason on invalid semver input