Easy: Introduce breaking new version, but freeze and continue to support old versions (e.g. a python3 install should include the ability to fallback to python2). A real world example of this philosophy are the D3D APIs. Each new major D3D version had a completely new API, but old D3D versions are supported forever.
Python source files then somehow need to announce what python runtime version they expect.
Having to support old versions forever also should make the maintainers think twice before breaking compatibility again.
I think two aspects have played a significant part in the migration pain:
1. The amount of breaking changes. If it was possible to do a little at a time, over a series of releases, it's a bit more manageable.
2. Being an interpreted language makes it difficult/painful to break compatibility over a series of releases, as all applications and libraries have to be kept source compatible. Source compatibility also makes it a pain for library maintainers, as you can't just cross-compile the library to a older ABI.
Maybe support for consuming libraries based on an intermediate, compiled representation, would make it easier for the library eco-system to follow breaking changes over a series of releases, and in turn make it more feasible for applications to handle a small amount of breaking changes over a longer release timeline.
They also changed the C interface at the same time -- and many Python libraries had non-trivial amounts of C code in them because Python was and is so slow.
It was also for a time impossible (or close to impossible) to run the same Python code in 2.x and 3.x. It helped when later 2.x versions acquired more 'from __future__ import'.
They really aimed for making it maximally hard to support v2 and v3 at the same time which created a nasty coordination problem: why upgrade if the libraries aren't ready and why upgrade the libraries if there are no users.
Feature flags or at worse, transpile. Perl did the first (in form of just writing what version features you want to use in header) and they also went thru "UTF8ication" of the language.
Even if you don't get all old code to work, having say 80% of it working means to start using Py3 you'd need to only initally rewrite 20% of the codebase, run tests and off you go, then migrate rest slowly over time.
Angular2 is a model for breaking changes IMO. The naming convention is a travesty, but the upgrade process was really elegant. The biggest bits:
- Added some stuff to AngularJS to make it more Angular2 compatible so that you could get closer to the target without taking in the breaking changes
- A thing called ngUpgrade let you dual-boot AngularJS and Angular2 together, so that you could transition parts of your program over time without needing to redo the whole thing
- Well documented migration guides for each step, outlining what you need to do in order to move from X version to Y version.
I've never used any version of Angular, but from what I've seen people complain about it was also a huge breaking change - so much of peoples' apps had to be rewritten they ended up just switching to React or Vue or something.
Fundamentally yes it was a huge shift. Practically, no, you dont need to rewrite a bunch _if_ you followed the upgrade path laid out as I described in my original post. They backported a lot of stuff so that you could do the rewrite incrementally.