Should we adjust Django's versioning to use a form of CalVer?

Django’s version numbers roll on. Having spent forever on 1.x, we’re just about to roll over to 6, can you believe.

Pop quiz: without looking it up, if you see a project on Django 2.x, can you say when that was release? How out of date it is?

I hear two complaints about our current versioning. The one is that it gives no information. The other is that it looks like semantic versioning, so we release a x.0 and folks are duly bemused about the lack of major (breaking) features. 6.0 is not a major change from the 5.2 LTS — indeed that’s arguable our major selling point — but folks outside the Django community don’t grasp that (and why should they).

A proposal would be to move to a kind of CalVer. Keeping the focus on the LTS cycle would could name release for the year of the LTS.

Obviously we missed 5.x. A shift during 6.x or for 7.x (as was) would be doable:

Existing Proposed Release Date
5.0 25.0 Dec 23
5.1 25.1 Sep 24
5.2 LTS 25.2 LTS Apr 25
6.0 27.0 Dec 25
6.1 27.1 Sep 26
6.2 LTS 27.2 LTS Apr 27
7.0 29.0 Dec 27
7.1 29.1 Sep 28
7.2 LTS 29.2 LTS Apr 29

Clearly, we can continue as we are. I wonder if folks have thoughts on whether such a change would be beneficial, or have other suggestions that might be better?

4 Likes

I think it’s worth investigating. Anything that “looks” SemVer but isn’t is always a pain in the head to understand and memorize. I do have a few notes to your proposal, though.

Fair, but to be honest, looking at Django 29.0 I would struggle to tell you when it was released, too, especially given that it’s not 2029 :smiley: Let’s not give Django the iOS treatment :wink:

To be fair, though, I don’t know of a better way off the top of my head. The clash of .2 and .0 releases happening in the same year is hard to do without them being “grouped” together: 6.2 would be 27.0, and 7.0 would be 27.1?


Another problem I have with this are the two-digit numbers. With other projects getting older and getting big version numbers (e.g. cryptography), these versions start looking like SemVer, which does not help the confusion. Maybe we should consider doing full four-digit years? 2029.0 is unmistakingly CalVer.

1 Like

Python’s PEP 2026 was proposed in 2024 and suggested a similar year-based versioning scheme, but it was ultimately rejected by their Steering Council: PEP 2026: Calendar versioning for Python - #126 by barry - PEPs - Discussions on Python.org

Then again, Apple have moved to adopt year-based versioning.

I have sympathy for the counter arguments on Python: year-based numbers look nice but they don’t help users think about when support ends, and any change is disruptive.

Also, in the case of your proposal, I think it’s as confusing as the current system to skip years—where did Django 26 and 28 go? Does my code checking django.VERSION need to account for those “impossible” numbers”?

Furthermore, I think that while our current “major” version bumps don’t communicate massive changes, they are at least a signal that things continue to change in Django, and might help convince “churned”/potential users to take a look. Year-based versions would probably have less impact there.

4 Likes

Good points Adam.

Yes, the grouping around the LTS causes that. Not sure what other ideas there are, bar dropping the LTS cycle being privileged, and just going straight YYYY.MM, which might be a thing. (:hot_pepper:)

Can’t quite imagine issues around the VERSION checks… :thinking: It’s just < and > in the main right? (Just missing your point clearly)

As a programmer I prefer projects which uses SemVer on those which use CalVer for my dependencies.

This because SemVer let me guess instantly how I should care about reading the full changelog:

  • it’s a major → absolutely yes;
  • it’s a minor → maybe;
  • it’s a patch → if I have time or I am looking for a specific fix.

With CalVer everytime I need to read changelogs or commits to understand what is expected to break and what changed.

IMO CalVer is fine for OS or for end-user software. For frameworks I absolutely prefer SemVer.

5 Likes

@sevdog Right! That’s the precise thought that leads to complaints about Django’s SemVar-alike. We have the time based release schedule and the deprecation policy, and — OK — as part of that we don’t remove deprecated features in the .2, saving until the .0 release. But Django’s releases aren’t really SemVer.

To phrase it using your guide: should you care about reading the full change log for .1 and .2 releases of Django? Absolutely yes. They are just as significant as the .0.

One way of thinking about the niggle people have is that we don’t communicate that very well. A .0 release comes out and folks comment “the last .2 had more significant features, meh”, or to that effect.

(None of that means we have to change, of course :smiley: but it is a perennial gripe.)

Not a bad idea, to be honest. LTS releases come out in April, once every two years; this is basically what Ubuntu’s been doing for over 15 years, but on odd years.

It still has a “learning curve” to it: “Only odd years dot-four are LTS” is as informative as “Only dot-two are LTS”—but it’s a scheme many people already know and understand

1 Like

Thanks for this post @carltongibson . I do think our version naming could be improved and is worth discussing. As a book author, the biggest challenge is that the first number changes so often, so someone new to the community rightly assumes that 5.2 and 6.0 represent very different versions. It means that smarter authors than me update every 2ish years around the .0 release so 5.0, 6.0, when it would be better all things equal to do it around the LTSs.

I think Python’s current versioning is quite good for authors because 3.10 versus, say, 3.13, doesn’t feel like a big deal even though it’s 3 years apart. But then we release far more often.

So I don’t have a great answer here other than I’m very open to new a new versioning system that somehow communicates the right of updates to Django as well as the built-in stability it has managed :slight_smile:

1 Like

For me, that’s the point which is the least well understood in 3rd party packages:

  • Folks testing for “Django 5” with >=5.0,<6 and not understanding that their test suite should usually run against 5.0, 5.1 and 5.2 if they claim support for the wole range
  • Using the trove classifier Framework :: Django :: 5 when adding support for 5.0

The counter point people are making is that Django is stable, which is true, but things change -slowly but surely- and some things are removed in .1 and .2 releases. Claiming support for Django 5.0 doesn’t guarantee support in 5.1 or 5.2.

2 Likes

I’ve become quite fond of Ubuntu’s version scheme - yy.mm.v with an optional LTS suffix. So over the course of a couple of years you’ll see releases like 24.04.x LTS, 24.10.x, 25.04.x (note, not LTS), and 25.10.x.

In the general case, the .x updates are maintenance releases that don’t materially change anything. The .04 releases of even-numbered years are LTS releases, everything else isn’t.

The current LTS releases are 24.04.3, 22.04.5, and 20.04.6. The current interim release is 25.04.

4 Likes

We don’t align with SemVer, and our pseudo-semver sets folks up for reasonable but incorrect assumptions. Carlton’s point that the .1 and .2 are just as important as the .0 release I think is spot on.

The problem is that we really have two totally different sets of compatibility guarantees, that are both vying for top billing and attention. LTS is talking about more than just its support status, it’s talking about upgrade paths too. We’re attempting, somewhat half-heartedly, to make upgrading from LTS to LTS a one-shot easy upgrade, even though we also tell people that upgrades really need to be by the smaller releases.

I think that we’re going to be happier if we fully choose one or the other. Either the LTS is our major version or every version is our major version. Right now it’s neither and both at the same time.

If we just let the LTS’s be long-term supported but otherwise a regular major version, I think that would do a lot to help folks get an intuitive sense of the trade-off they make when they use an LTS version instead of staying on the latest major version.

3 Likes

I completely agree. I’m new here, but I’ve been using Django for many years.

The current scheme seems to create a false hierarchy where .1 and .2 releases look like “minor releases.” The “one obvious way to do it” seems to be treating all releases as major versions (with LTS marking for every third release).

Each 8-month Django release typically introduces significant features, deprecations, and breaking changes. These require the same level of migration planning regardless of whether it’s labeled .0, .1, or .2.

The main downside seems to be that version numbers would increase more quickly, but I don’t know whether that’s a real disadvantage?

The benefit of minor/patch releases should ideally be that you don’t have to put the same effort into migration. We do extensive testing before upgrading to a .1 or .2 release, but hardly any testing at all (except the automatic test suite) for patch releases. The current versioning doesn’t match this reality.

Sequential major versioning would better reflect the reality that every Django release deserves serious consideration and planning. So Django 6, Django 7, Django 8 (LTS), then Django 9 etc.

Sound great. But instead of e. g. 25.2 LTS I would suggest to use long format like 2025.2 LTS. Why? Because 25.2 is just as meaningless as 5.2 if you don’t know that 25 means 2025. Writing 2025 makes that clear to everybody.

1 Like

Whatever is done needs to conform to Version specifiers - Python Packaging User Guide. I think that rules out 2025.2 LTS (for example) as the version that goes in PyPI.

Perhaps the complaints could be addressed by spreading awareness of why the version schema was designed the way it is. The main references are:

Mainly:

Starting with Django 2.0, version numbers will use a loose form of semantic versioning such that each version following an LTS will bump to the next “dot zero” version. For example: 2.0, 2.1, 2.2 (LTS), 3.0, 3.1, 3.2 (LTS), etc.

SemVer makes it easier to see at a glance how compatible releases are with each other. It also helps to anticipate when compatibility shims will be removed. It’s not a pure form of SemVer as each feature release will continue to have a few documented backwards incompatibilities where a deprecation path isn’t possible or not worth the cost. Also, deprecations started in an LTS release (X.2) will be dropped in a non-dot-zero release (Y.1) to accommodate our policy of keeping deprecation shims for at least two feature releases.

In my opinion, one of the biggest benefits of the current versioning schema is it gives third-party apps guidance. Apps should generally be able to support Django X.0, X.1, and X.2 without much upgrade work. After Django Y.0 comes out, it’s time to address deprecations and drop compatibility for Django X.0 and X.1.

The release notes have emphasized this workflow. Quoting the Django 6.0 release notes:

Following the release of Django 6.0, we suggest that third-party app authors drop support for all versions of Django prior to 5.2. At that time, you should be able to run your package’s tests using python -Wd so that deprecation warnings appear. After making the deprecation warning fixes, your app should be compatible with Django 6.0.

As for the CalVer proposal, I don’t think that having the major version number loosely correspond to the year of the release adds much benefit, especially considering the confusion it creates by skipping version numbers.

9 Likes

+1 for not changing DEP 4

though, doing the changes when django have major changes such as: better django serialization with ‘best practices’, full async support, etc would be a great idea.

I don’t think that I terribly care whether we change the version numbers. The numbers as they exist give a clear sense of our macro processes. I sympathize a lot with the motivations behind Epoch Semantic Versioning, and I think that Python’s version specifiers are more conducive to implementing those as separate initial version numbers that NPM’s semver specifiers. The way we number our version aligns very well with that, and I like that it lines up with the steering council terms.

The CalVer numbers don’t resonate with me as much as our current numbers, for reasons that I can’t quantify. I know the rest of the world seems to be embracing them, I just don’t think they tend to really line up with what’s most valuable to communicate about a release. I suspect folks with a different opinion disagree with me on what’s most value to communicate about a release.

For our time-based releases, every version is a major version, and I think that’s good. I don’t know what to call our first number, since “Epoch” has another important meaning to Python versions, but I’d keep “major” to describe the second number. Minor and patch versions are basically the same thing because of our compatibility policy.

I think that generally abandoning our effort to avoid breaking changes between LTS releases however, could be a real boon to helping us more clearly communicate what the second digit of the version is about, and might help us be a little more nimble in our development processes as well. I don’t think we actually succeed at making a clean upgrade path from what I’ve observed (but to be fair, I don’t follow LTS release upgrade cycles), so recognizing that fact and changing the official policy to reflect that reality could avoid certain types of discussions and maintenance overhead.

I might like to consider another versioning scheme that meets people’s expectations a bit more, but I don’t think CalVer is it. I think CalVer works better for projects where knowing the year (or whatever) is meaningful. To show just how meaningful it often isn’t, this thread was how I learned Ubuntu’s versions were based on years. Then again, I’m not always the sharpest tool in the box.

If I wanted to do anything it would be probably be to increment the major version on every (current) X.Y release, as there are always backwards incompatible changes of some kind. To me, the only real significance the current scheme has is that the third number indicates a bugfix release. The difference between 6.0 and 6.1 feels like almost nothing to me. 6.2 only really indicates to me that it’s LTS. Maybe I’m missing something here. In this case we could even ditch the third number. So we’d just have, if we changed it right now:

6.0 - initial 6.0 release, no changes
6.1 - first patch release

7.0 (what we would now call 6.1)

8.0 LTS
etc.

But I realise this probably sounds quite outrageous, I just like the simplicity, and it’s fun to watch the number go up faster :slight_smile:

I don’t hate it @tom :sweat_smile:

The thought I do have is that iterating the numbers more quickly doesn’t lean into (i.e. communicate) the stability that’s probably our biggest selling point. e.g. Just now:

@wsvincent’s point about keeping the lead number would give a sense of that stability.

I think it’s absolutely right that an X.0 could just have well been the X-1.3 without any difference at all.

Modulo @timgraham’s points about the LTS cycle. (Thanks for that post @timgraham — really good!)

@ryanhiebert I not an LTS user myself either. I used to rail against it slightly, but it’s still very popular with (estimate) about a third of the user base, so I’ve softened over the years.

My Current Status here: :person_shrugging: — likely status quo unless a clear better option appears, but I’ve enjoyed this discussion. Thanks all so far!

1 Like

I’m one of those who work* in an environment where upgrades every 8 months just wouldn’t be considered acceptable. The two-year cycle for LTS releases “fit” well enough.

In my ideal world (waving the “magic wand”), any code written for X.0 would still work for X.2. This would mean that no deprecated features would be removed between X.0 and X+1.0. In my opinion, this would be the intended spirit behind the SemVer numbering.

(I recognize that this is not the common opinion and potentially carries the “sense of stability” too far for this environment.)

*(used to :slight_smile: )

2 Likes