Calendar Versioning (CalVer) for APIs — Date-Based Release Strategy
In this tutorial, you'll learn about Calver. We cover key concepts, practical examples, and best practices to help you understand and apply this topic effectively.
Calendar Versioning (CalVer) uses release dates as version numbers, communicating release cadence clearly while requiring additional compatibility documentation.
What You'll Learn
By the end of this lesson, you will implement CalVer version formats, compare CalVer with SemVer, communicate compatibility alongside date-based versions, and choose when CalVer is appropriate.
Why It Matters
CalVer is ideal for APIs with regular release cadences where consumers track releases by date rather than compatibility semantics.
CalVer Format Comparison
from datetime import datetime
from typing import Dict, Optional, List
import re
class CalVerParser:
FORMATS = {
"YYYY": r"(\d{4})",
"YYYY.MM": r"(\d{4})\.(\d{2})",
"YYYY.MM.DD": r"(\d{4})\.(\d{2})\.(\d{2})",
"YY.MM": r"(\d{2})\.(\d{2})",
}
def __init__(self, format_str: str = "YYYY.MM"):
self.format_str = format_str
self.pattern = self.FORMATS.get(format_str)
def parse(self, version: str) -> Optional[Dict]:
match = re.match(self.pattern, version)
if not match:
return None
groups = match.groups()
parts = self.format_str.split(".")
result = {}
for i, part in enumerate(parts):
if i < len(groups):
result[part.lower()] = int(groups[i])
return result
def generate(self, dt: datetime = None) -> str:
dt = dt or datetime.utcnow()
parts = self.format_str.split(".")
values = []
for p in parts:
if p == "YYYY": values.append(str(dt.year))
elif p == "YY": values.append(str(dt.year % 100).zfill(2))
elif p == "MM": values.append(str(dt.month).zfill(2))
elif p == "DD": values.append(str(dt.day).zfill(2))
return ".".join(values)
def compare(self, v1: str, v2: str) -> int:
p1 = self.parse(v1)
p2 = self.parse(v2)
if not p1 or not p2:
return 0
for key in ["year", "month", "day"]:
if key in p1 and key in p2:
if p1[key] != p2[key]:
return -1 if p1[key] < p2[key] else 1
return 0
calver = CalVerParser("YYYY.MM")
v = calver.generate(datetime(2026, 6, 28))
print(f"Generated: {v}")
parsed = calver.parse("2026.06")
print(f"Parsed: {parsed}")
cmp = calver.compare("2026.06", "2026.12")
print(f"Compare: {cmp}")
CalVer vs SemVer Decision
class VersionSchemeDecider:
def __init__(self):
self.factors = {}
def add_factor(self, name: str,
calver_score: int,
semver_score: int):
self.factors[name] = {
"calver": calver_score,
"semver": semver_score,
}
def recommend(self) -> str:
calver_total = sum(f["calver"] for f in self.factors.values())
semver_total = sum(f["semver"] for f in self.factors.values())
return "calver" if calver_total > semver_total else "semver"
def get_recommendation_reason(self) -> str:
rec = self.recommend()
reasons = []
for name, scores in self.factors.items():
pref = "calver" if scores["calver"] > scores["semver"] else "semver"
if pref == rec:
reasons.append(name)
return f"Recommended: {rec}. Key factors: {', '.join(reasons)}"
decider = VersionSchemeDecider()
decider.add_factor("Release cadence", 5, 2)
decider.add_factor("Compatibility communication", 2, 5)
decider.add_factor("Consumer type", 3, 4)
print(decider.get_recommendation_reason())
CalVer API Router
class CalVerRouter:
def __init__(self):
self.releases: Dict[str, Dict] = {}
self.latest: Optional[str] = None
def add_release(self, date_str: str,
handler: callable,
breaking: bool = False):
self.releases[date_str] = {
"handler": handler,
"breaking": breaking,
}
if not self.latest or date_str > self.latest:
self.latest = date_str
def route(self, version: str,
request: Dict) -> Dict:
release = self.releases.get(version)
if not release:
return {"status": 404,
"body": {"error": f"Version {version} not found"}}
return release["handler"](request)
def get_breaking_changes_since(self,
version: str) -> List[str]:
changes = []
for date_str, release in sorted(self.releases.items()):
if date_str > version and release["breaking"]:
changes.append(date_str)
return changes
router = CalVerRouter()
router.add_release("2026.01", lambda r: {"version": "2026.01"})
router.add_release("2026.06", lambda r: {"version": "2026.06"},
breaking=True)
breaking = router.get_breaking_changes_since("2026.01")
print(f"Breaking changes since 2026.01: {breaking}")
Common Mistakes
Mistake 1: No Compatibility Information
CalVer does not communicate compatibility. Always document breaking changes alongside the version.
Mistake 2: Overly Precise Versions
YYYY.MM.DD is too granular for most APIs. YYYY.MM or YYYY.MM.MINOR provides a better balance.
Mistake 3: Non-Standard Date Formats
Use ISO 8601-based formats (YYYY.MM) for consistency and correct alphabetical sorting.
Mistake 4: Assuming Chronological Order
Versions like 2026.1 (January) and 2026.12 (December) sort correctly as strings. Test version sorting.
Mistake 5: Mixing CalVer and SemVer
Do not use both schemes simultaneously. Choose one and communicate it clearly.
Practice Questions
- What is the CalVer format and when is it appropriate?
- How does CalVer communicate breaking changes?
- What is the difference between YYYY.MM and YYYY.MM.DD CalVer?
- How do you compare two CalVer versions?
- When should you choose CalVer over SemVer?
Challenge
Build a CalVer-based API Versioning system that generates versions in YYYY.MM format, routes requests to the correct handler, tracks breaking changes per release, and provides a compatibility report between any two versions.
FAQ
Mini Project
Build a CalVer-based versioning system that generates YYYY.MM version strings, supports routing per release, tracks breaking changes with dates, provides compatibility reports between releases, and compares CalVer and SemVer for the same API.
What's Next
Learn about Version Formats for choosing the right version scheme, or explore Semantic Versioning for compatibility-based versioning.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro