Hello everyone,
(apologies in advance for the long post, but there are lots of aspects to dig here, it's not for the pleasure of writing but a necessary to progress on the relevance of a DEP) *"As you can see, there's more than "blinding self-absorption" and "harmful psychological bias" here. Your freecodecamp article makes valid points. It also misses completely what's hard in maintaining backwards compatibility. I wish you hadn't found it useful to insult the work put into managing backwards-compatibility over the years and the people who did it."* I'd rather address that immediately: as you read in my article, "blinding self-absorption", "harmful psychological bias" and other strong words were all aimed at something very specific, the "feeling of return to purity" that a person could have when destroying compatibility shims. I persist and sign regarding this one: ruining the day of thousands of anonymous plugin/website maintainers all over the world should *always* be made under compulsion of heavy constraints, and with regrets; never lightheartedly. Now, maybe my attempt at explaining the roots of nowadays' mass breakages by a cultural problem are all wrong (my bad), and there are other reasons behind the *apparently* reckless compatibility destructions that I witness left and right. But what are they? Since it's not technical inability, since it's not laziness, since it's not ideology, what makes that (as far as I'm concerned) the Django ecosystem fails on that aspect, contrary to loads of others "big" frameworks, runtimes, libraries etc? If it's "we don't have enough resources", it's all understandable, but at least let's make it clear. The weirdest, in the whole story, is the global idea that everything is fine, that compatibility is already a "major concern" and treated so, that maybe it can be improved a little bit but no need to push it. It's not that way I see it. And a problem can't be addressed if it's not first recognized as so. Now, I know that it requires a good amount of diplomacy to criticize a behaviour, or here a whole policy, without concerned people feeling attacked. I don't have this kind of talent (and probably won't ever), so all I can do is be honest with my view on the situation, and bring attempts at solutions. It's precisely because Django is a great piece of software (with its omnipresent lazy-objects, it's automatic admin and model forms, its migration system, its focus on security headers...) that having it scarifying itself is unbearable. So I apologize in advance for all the criticisms that expand below, but without them there'd be no need for any DEP or change in the first place. I also want to make this clear: I didn't mean that breaking changes were never justified, just that they had to be the last option, and thoroughly explained. When the AngularJS team, after the years of hype, admitted that the whole designed was flawed, reset the whole framework as Angular version 2, and made great efforts to ease part-by-part migration towards the new behaviour, I felt unlucky but not angry. When trivial changes ruined tons of pluggable Django apps that did one thing and did it well, it was a different matter. Before answering remarks, I think it's worth summarizing what the "problem" is. This is not lightly that I used the words "dependency hell". My experience with Django, purely regarding compatibility, has been abysmal compared to lots of other ecosystems (in C/C++, PHP, Jquery...) that I have crossed, and which didn't have the malleability of Python; I see several cumulated reasons explaining that : - The pace of breakages: an arm's long list of new breaking changes introduced every 8 months, dropping at the same time all shims older than 2-3 years, is imo a too fast breakage pace, even by the Web's standard. When changes are mainly aesthetical, compatibility shims had better remain for very long periods of time, else the benefit/harm balance is just indefensible. - More importantly, the *scope* of breakages: as long as it's only about fixing corner cases of the framework, and only a tiny portion of users is impacted (those who abused counter-intuitive and unpythonic behaviours), one can understand, and just hope not to be shot by the next upgrade; but here, we're dealing with changes that deliberately break about every existing application, even those which scrupulously followed official tutorials: ForeignKey's "on_delete" argument killed most migration files when becoming mandatory, moving url-related primitives left and right was as destructive as expected... - The growing "less batteries included" philosophy. Valueable contrib modules were outsourced from the codebase, and other pluggable applications (likes CMS apps) follow this trend, putting all valuable features into external plugins. This "minimal core" philosophy would work fine if main applications counter-balanced it with a more delicate approach to breaking changes; but it is the contrary that happens, surprisingly. So in such fast-breaking environment, only monolithic applications can avoid turning into nightmares. - The apparent needlessness of lots of changes, aggravated by the severe lack of explaining in release notes. As Linus said "And those reasons really need to be very good, and spelled out and explained". For sure I'm thankful that these release notes exist at first. I really am. Especially because without them, Django would be doomer than doomed. But even after digging the commits regarding almost 30 changes, and tracking some of them in tickets, most of them continue to look injustifiable to me. I have probably missed some incredibly deep pondering and reasonings backing them, but this miss is a problem in itself. When a perfectly suitable utility gets dropped in favor of a castrated stdlib one without a word of justification, when URL patterns (the very starting points of most pluggable apps) get broken 10 times in 5 years, it feels just like being spat on, "deal with it" style. To illustrate, I'd like to redraw the history of said URL resolving and reversal system, as I've lived it (temporal order of changes not guaranteed). - A few years years ago, you had to use the patterns() function, with tuple arguments including dotted strings, and an optional string prefix. - Then you had to provide individual url() objects instead of tuples. - Then the "url defaults", that everyone "star-imported" from as per official tutorials, changed their location. - Then patterns() were replaced by a list of urls, or by a 2-tuple (!!) when including the app_name with it. - Then url reversal by dotted strings was made impossible, one had to use named urls - Then providing urls as dotted strings became impossible, views had to be imported and inserted directly. - Then include() didn't accept a 3-tuple anymore, for some reason. - As a side effect, include(admin.site.urls) was dropped, one had to directly use admin.site.urls in patterns - Then url() got replaced by re_path(), when introducing simpler path matchers, and basic classes like RegexUrlPattern got removed. - And recently lots of utilities have been continuously transferred from django.core.urls and django.core.urlresolvers to django.urls, breaking imports on their way, for a reason that most end users ignore. My honest opinion is that, for a framework which is so reknowned and massively used, and for a set of features that are so central to every pluggable app, this avalanches of breaking changes is rather hard to sustain; especially considered the short lifespan granted to compatibility shims. Django is not just a website engine, but also a toolkit for building bigger blocks, more specialized frameworks, which themselves ought to get their own plugins, themes, bridges to interesting utilities... but it's hard to build big castles on too shaky grounds. The worse is, the forward-evolutivity of the whole system has LOWERED in the process. URL Patterns() was a powerful factory design-pattern, which could return any object it wanted; this object could fake being a list, could customize its __add__() behaviour, could accept more arguments... And what do we get in the end ? A *tuple* ([patterns], app_name), maybe the most inevolutive, semantic-less container type in Python. How can I explain all this to a Django newbie who would want to upgrade his site after 5 years of stagnation? I fear that as breaking changes become more and more normal, less and less care will be taken to pick the most evolutive data types and designs along the road. Whereas long term compatibility also requires a habit of enforcing powerful scaffolding (like forcing user code to inherit from some base clases and their metaclasses), so that these can step in at any moment and fix future changes transparently. The Way of the Cross that has been the python2to3 migration taught us that the pace of core developers is far from the pace of the silent mass of users (python3.0 was released more than 10 years ago, and I still get panicked requests of enterprises trying to migrate their huge codebase of python and C extensions); that there is always big optimism bias when introducing breaking changes (remember the u"" notation, that had to be reintroduced back because its removal was too much burden for project maintainers?); that preferring purity to practicality is always a bad idea (it seemed ugly to leave import aliases in the stdlib, as a result all big projects import from *six*, so it's the same ugliness and twice the porting efforts). I feel that the whole Django ecosystem needs more awareness on this point. Another example is the Turbogears framework. It was like a pythonist's dream, mixing "best of breeds" libraries; but whenI tried to install it, years ago, after several hours of efforts I couldn't find a "known working set". Lots of small libraries broke compatibility as if they were alone in the world, ruining (for me, YMMV) a perfectly valid concept. *"If I understand correctly, you're proposing that:1. The Django project should maintain a higher level of backwards compatibility.2. This would be easier to achieve outside the main codebase, in a submodule or in a separate repository.3. This would reduce the amount of work required for maintaining Django (triaging tickets, fixing bugs, adding features)."* Indeed. Note that just letting comptibility shims 2 or 3 times longer would also solve the problem, without resorting to new concepts. *"I'm quite skeptical of the "same effort or less". The more behaviors Django maintains — and you're proposing essentially to maintain all behaviors that existed at all previous releases — the more complexity accrues. You can't solve that just by shuffling deprecation warnings or compatibility imports in another module (and it seems to me that it would be harder to maintain, for the reason the Python ecosystem usually eschews monkey-patching: non local effects). Keeping old ways around forever increases the barrier to mastering Django for people who haven't been writing Django code for ten years like you and me. It also increases the scope on which compatibility must be maintained, and that can get in the way of making Django better."* My own experience, although much smaller than that of Django maintainers, differs quite much from these assertions. When I mentored a dozen students in internships, from a very basic knowledge of python and the web to an intermediate level in Django, I can't think of a single time where having some deprecated aliases and behaviours around hindered them. Beginners who delve into official (and up-to-date) tutorials are not supposed to meet legacy examples. If they integrate/audit third-party packages, and encounter legacy code, they might have some questions. With long-term compatibility, these questions will be answered on launch by explicit logging and warnings; with current deprecation policy, the scaffolding being forcibly destroyed, beginners will face completely abstruse crashes, or worse, will wonder for hours why their own code is never called. That's what I consider a steep learning curve (and a big waste of time). Regarding the "scope" of maintenance, of course keeping more compatibility is harder than a "fire and forget" approach. But the benefit is huge for the community, my experiments show that fixers are quite easy to maintain (only once did I have to refactor an older fixer to account for a new breaking change on the same feature), and they don't seem to block the evolution of Django at all, since they "time travel". If a blocking case occurs one day, THIS might be a valid reason to drop compatibility on a particular feature, but at leats it'll have been justified (and will have lasted as long as possible). No one demands eternal compatibility, just a breakage pace compatible with little-maintained projects and big software. As of today I can't envision how we could have Wordpress-size ecosystems, with themes shops and auto-install plugins, without it falling immediately into dependency hell. A decade seems a good start to me. *"One way to frame the debate is: does Django aim at being a fossilizing ecosystem or a living one? Up to this point, we've chosen to maintain a living ecosystem. At a given point in time, all maintained libraries support 2 or 3 versions of Django (latest, previous, LTS). Things change a bit; unmaintained libraries that don't adapt fall out of favor; new libraries build upon the experience of previous ones and try to do better. One can see this as progress and a way to keep up with the moving web ecosystem. One can also see this as useless churn. As you call it "walk or die", it's pretty clear which camp you're in :-) I'd say there's truth in both!"* I don't really buy the dilemma between "living" and "fossilizing" here. Long term compatibility doesn't encourage project maintainers to be lazy, unless new versions of the framework bring nothing interesting (which would be a problem in itself). Better compatibility just allows low-community plugins - or those who block on other little-maintained dependencies - to keep working until a next surge of activity, or until the approach of the plugin is really deemed obsolete. It encourages people to upgrade their Django instead of dreading and delaying the next upgrade. It almost makes supporting multiple versions (and LTS...) useless, since people just have to "pip -U django" if security issues arise. It removes a LOT of burden from pluggable app maintainers, time that they can allocate to handling normal tickets or - who knows - pushing their best ideas towards Django itself. What I'm sure of, is that a "living" ecosystem doesn't have to stab half his packages (and definitely murder a good part of them) at each minor version release. The awesome https://djangopackages.org/grids/ sometimes looks like a half-cemetery, in which one doesn't compare packages by features, but by "what are the odd that it still works with my particular Django version"? This hasn't to be so. "Perfectionnists with deadlines" should avoid wasting time needlessly. On the precise subject of compatibility, we should take inspiration from in-browser ecosystems like jQuery. One can take datepickers, grid displayers, notification plugins, sometimes 5 or 10 years old, they just works No need to read release nodes, to do dependency conflict resolution, to hack JS sources and rebuild, to fallback on less relevant but alive plugins. Except some rare cases (like the .live() deprecation in favor of .on()), one can just download the latest versions of big frameworks, stick them in its statifiles, and all works; in a web-browser ecosystem which is yet known for rushing like mad. Why couldn't Django offer the same thing, with all the magic Python has built-in (magic which keeps expanding, with keyword-only and soon position-only arguments, helping future compatibility shims)? In the releases notes of the last 7 versions of Django, how many compatibility shims were dropped because they really hindered Django evolution? How many trivial shims were dropped, at the contrary, "because that's what we do"? *"As an experiment, let's just consider the first backwards-incompatible change from the latest release: https://docs.djangoproject.com/en/2.2/releases/2.2/#admin-actions-are-no-longer-collected-from-base-modeladmin-classes (I'm skipping the database backends changes because they're explicitly out of the backwards-compatibility policy, but we started documenting them for the maintainers of third-party database backends.) This is a good example. Django behaved in a non-Pythonic way; it didn't respect the Principle of Least Astonishment. Since we believe Django is alive and will have infinitely more future users than past users, we make the change. How can we maintain backwards-compatibility for this? We can try to detect if subclasses have all the actions of their parent classes and, if not, raise a warning. But then we raise inappropriate deprecations warnings in legitimate use cases where a subclass mustn't have some actions of its parent class, which is probably the use case for which this change was requested."* Thanks for showcasing this one change, it illustrates perfectly lots of ideas I'm trying to push forward. This is here an example of non-trivial change; contrary to many fixers I had to code, which were just aliases and tiny wrappers - fixers asking for about zero thinking time and zero maintenance. This kind of change requires more brainstorming, but you are right on this aspect: if it's changed in-place, without any deprecation path, django-compat-patcher is not able to automatically fix it (though a fixer could expose a setting so that project maintainers indicate the ModelAdmins they want fixed). This is imho one particularly shocking and inimical change: a perfectly documented and widespread feature removed suddenly, without deprecation path if I understand correctly (nor the usual advice for lib maintainers on how to handle both behaviours), letting who knows how many Django users wondering why some controls disappeared from their website. That's a way of making people paranoid, or ensuring that the least experienced spend half their time digging on StackOverflow. Some changes are just not fixable in-place. But I know about none which cant be fixed out-of-place. In this case you mention, we should have found another suitable English word (there are 170.000+ available I heard), or combination of words, to express a similar idea. For example "admin_actions". These admin_actions attributes would have been the new documented feature, with a properly expected and pythonic behaviour. The old "actions" attribute would have silently and slowly died of old age, keeping until the end their weird but getting-stuff-done behaviour. A compatibility shim in admin submodule (or outsourced to a Compat patcher) would have handled the legacy behavior for years and decades, without harming anyone. What I'm describing here is similar to what happened to MIDDLEWARE_CLASSE=>MIDDLEWARES migration, I guess. I have no idea why these admin actions were changed in the most brutal way possible, but you are right on this point: like any compatibility shims, compatibility fixers need a tiny bit of collaboration from project maintainers, else sometimes the harm is irreparable. These "unfixable cases" are maybe the main reason I'm trying to raise awareness about compatibility (that, and the fact that I'm worried to see people waste their time in bugtrackers discussing how to work around breakages). It's one thing to be confronted to breaking changed, it's another thing to be prevented from applying one's own compatibility shims. I'd like to point that this kind of breaking chance is especially surprising as it goes against the own policy of Django : *"All the public APIs (everything in this documentation) will not be moved or renamed without providing backwards-compatible aliases. [...] If, for some reason, an API declared stable must be removed or replaced, it will be declared deprecated but will remain in the API for at least two feature releases. Warnings will be issued when the deprecated method is called. [...] We’ll only break backwards compatibility of these APIs if a bug or security hole makes it completely unavoidable."https://docs.djangoproject.com/en/2.2/misc/api-stability/https://docs.djangoproject.com/en/2.2/internals/release-process/#official-releases* *"Experimenting with a third party module like you did is absolutely the way to go. You're already ahead of the usual advice :-) Keep in mind that the barrier for making a third-party project an official Django project or to merging it in Django is very high. With 1 fork and 2 stars, django-compat-patcher doesn't pass it yet.Writing a DEP to formalize arguments on both sides is a valid idea. Don't forget the other side. In your freecodecamp article, you're spending two lines on the downsides of Compat Patchers, and this is not enough. How would Django communicate to users which backwards-incompatible changes are covered by Compat Patchers and which aren't? How would we respond to users asking for a Compat Patcher for a backwards-incompatible change for which it's impossible? Eventually, will every library document the list of Compat Patchers it requires or it's incompatible with?"* Sure I would list these remarks, but haven't most be long answered by lots of other projects? Compatibility IS maintained by default, unless its is not possible for "spelled out and explained reasons" (security, obvious contradiction of behaviour, really too heavy development effort for too few users impacted...). The good new is that people worried about compatibility could submit patches to help this effort, instead of breaking their neck against official policies. And libraries don't have to know anything about compat patchers, they are just meant to aim for the latest version of the framework, and themselves seek wide support (eg. with the django-compat lib, not to be confused with DCP), letting project maintainers tweak their patcher config as needed (by looking at Django version requirements of dependencies, or better disabling fixers' families one by one until unit-tests break, or better again checking the warnings and the usage report of the patcher - the latter not implemented yet - to know what fixers must be left enabled). I'm worried about the reversal of the burden of proof here, though. Historically, it has always been the role of people wanting to break compatibility, to justify their goal; to demonstrate that few people will be impacted; that proposed compatibility shims are wrong or unmaintainable; that the old behaviour is too harmful or blocks evolutions. Now it's as if *I* had to demonstrate (in 2019) that a strong commitment to backwards compatibility is important; that Django users and projects all around are negatively impacted by these changes; that long-term compatibility is *not* hard, nor big waste of time, nor doomed. What am I to do, pay for a world survey of Django developers? Wait for 10 years so I can tall "See? My websites are still compatible with Django 1.7, told yah"? Do big data or prophecies to compute how much time exactly maintaining compatibility shims will take to core devs? If it's a cultural axiom of the whole team that the current situation is just fine, it's not a technical issue anymore, it becomes like trying to convince a torero that corrida is bad. *"I think Aymeric is making a good point here on why any project like django-compat-patcher is ultimately doomed to be a failure. You simply cannot magically "fix" changes like this without knowing user intent. I can also remember the change from get_query_set to get_queryset (1.6 IIRC), you simply cannot magically patch that to work with multiple sublcasses and dynamic attributes. There will __always__ be situations where this will break in horrible and subtle ways. I'd argue that this would be worse then not being able to use a project that is not updated for a while."* I agree with you, in some cases, the dynamic nature of the language can play against compatibility. But once again, no one demands perfection, just a *sufficient* level of compatibility. I hadn't followed the get_query_set() removal effort (https://code.djangoproject.com/ticket/15363), I don't know what happend to the nice proposals of the ticket ("renamed_method_factory" and the likes), but all I know is that with rather limited efforts I've revived the vast majority of the modules I needed ; and upgraded to Django 2.2 my site, when major dependencies explicitely prohibited anything else than Django 1.11. That's not what I call a doomed failure ^^ *" > With *less* work than currently, except small changes in procedures, we can revive the majority of existing packages (except python2vs3 troubles), ... This is a bold argument for which I'd like to see some proof, also *less* work for whom?"* It's only a rough estimate, but I think that the time spent removing shims from the codebase is equal-or-above the rare bugfixing (or removal) of oldest compatibility fixers, for the corde dev team. Of course, I assumed the presence of deprecation paths. Labor-wise, even Compat Patchers can't beat the "Instant Death" policy that lead to the unfixable situation you both mentioned. Concerning third-party plugin maintainers, long term compatibility wouldmean a huge relief, of course, and that could "backfire" positively for everyone. *"And last but not least, if one assumes all your arguments hold true, then why isn't django-compat-patcher used by existing 3rd party libraries (At least according to public usage https://github.com/pakal/django-compat-patcher/network/dependents)? Either the usecase you are suggesting isn't as strong as you make it to be or 3rd party packages are maintained well enough for this to be not a problem? Personally I also think that a package that wasn't updated since 2015 should probably not be magically patched to theoretically work on a current Django."* Let's note that DCP is aimed at project maintainers, not library developers, who rather rely on django-compat and the likes. Without official support from Django, it'd indeed be bold of a library to impose DCP as dependency (instead of just targeting the latest Django version as it should). There is some download activity https://pypistats.org/packages/django-compat-patcher ; no idea if its bots, curious people or real users though. Concerning projects, I see lots of possible answers though: - I'm bad at (and hate doing) communication/marketing on my projects - it's a quite new concept of "companion application for long-term compatibility", so people are naturally reluctant - people naturally doubt the relevance and longevity of new packages, that are not born of figures of authority (look how asyncio overcame trio). - tons of F.U.D regarding monkey-patching and even the possibility of having long term compatibility - there is a cultural acceptation of the situation, of the resigned "necessity" to do useless forks, or waste time repairing regression with patch-ups jobs, as if it made software better - indeed people staying close of "mainstream" packages can indeed do without DCP (I just did on my latest 30-days project); but what a have fun trying to build a CMS site with usual niceties on a non-monolithic framework... I don't know if DCP is the best way to achieve long-term compatibility, I just know that it was easy to code (except the import alias mechanism), and worked like a charm for my dependency-full projects. And that when wandering on repositories I cross too many regression tickets, or even people complaining about the time it takes to upgrade (e.g. https://www.reddit.com/r/django/comments/7u84gj/django_release_schedule_and_python_3/ when I was seeking a release schedule image). I know that on this issue the interests of Django core devs are not exactly the same as those of end users, but I hope some progress can be made somehow, at least to ensure that those who need compatibility can achieve it on their own. Sorry for the long post, regards, Pascal Chambon -- You received this message because you are subscribed to the Google Groups "Django developers (Contributions to Django itself)" group. To unsubscribe from this group and stop receiving emails from it, send an email to django-developers+unsubscr...@googlegroups.com. To post to this group, send email to django-developers@googlegroups.com. Visit this group at https://groups.google.com/group/django-developers. To view this discussion on the web visit https://groups.google.com/d/msgid/django-developers/6a057eb3-cd3e-4833-8318-1a43e6961fec%40googlegroups.com. For more options, visit https://groups.google.com/d/optout.