on underA few days ago I published a harsh critique of the Flask's team practices with regards to releases, versioning and especially about their weak backwards compatibility track record. This generated a bit of a stir and lots of people, including members of the Flask core development itself, have voiced their opinions.
I'm going to start by admitting that even though I have received some support, there has been a lot of push back as well. I really have no problem with this, as I don't hide from criticism. In this follow up article I'm going to talk about the good and the bad takes that resulted from my blog post, but I especially want to dissect the opposing views.
Before The Storm
Before I start to discuss a list of topics I collected from the feedback to my previous article, I want to mention something I have not said in that post, and I should have. In spite of this very specific disagreement I have with them, I highly appreciate the work that the Flask team does, and have a lot of respect for them. I understand that they are volunteers, that they often face difficult decisions that will always end up affecting someone one way or another, and that they always try to make the best judgements. Basically what I'm saying here with respect to their decision making process is that while I do not agree with some of their decisions, I know that they always have good intentions and come from a good place.
It's unfortunate that David Lord (Flask team lead) has taken my blog post from the other day very personally and considers it an attack on him and the team, which is certainly not. He is not interested in discussing the issues I'm presenting unless I make a public apology for that article first. I can't apologize for exposing something that I believe is an actual problem that needs to be addressed, so for the time being I expect I will not be part of any discussions, even though my door is always open if there is a change of mind.
Okay, let's get on with the show.
"What does this guy want to get, anyway?"
Some people were confused about my intentions. I think this is partly my fault, because I focused too much on the Flask-Login incident, instead of talking about the actual problem.
I think the Flask team needs to evaluate their policies and processes with regards to breaking changes. When are these made, how often, how are they communicated, how far reaching can they be and for what reasons. I think the way this is done currently is too lax, resulting in a constant stream of breaking changes. I would like for Flask and Werkzeug to have less breaking changes, released less frequently.
This is a long article, but if you are interested and manage to read it all you will have a more complete picture of many of the sides from where this problem can be looked at.
"Miguel is right that there is problem, but wrong on what the problem is"
Many people have indicated that my analysis of the issues around breaking changes, Flask-Login, or anything else I discussed on my previous blog post is flawed, and that the issues lie somewhere else.
That's great! I don't claim I'm the only one with the answers. If you think this community has improvements to make with regards to these problems, it is important that you voice your opinions, even if they are not aligned with mine. You are welcome to reach out to me if you want to chat about them, or better yet, join the Flask team's Discord server and tell them directly.
"It's a major release, they can break whatever they want!"
This was by far the most shared critical response I have seen to my article. A variation of this response centered around the fact that I have no idea what semantic versioning is or how it works.
As I said above, I should not have made the Flask-Login incident (which falls on the 3.0 release) the focus of my article, and instead I should have used other instances that expose the problem better. A lot of people only read the start of the article, and missed the overall point I was trying to make.
But in any case, the fact of the matter is that Flask, Werkzeug and other projects closer to the Flask team such as Flask-SQLAlchemy, do not follow semantic versioning. I don't say this as an accusation or as any kind of criticism. I think it is a valid choice for them to not use semantic versioning, and only wish they were more clear about this with the community.
If you think my complaint would have been warranted should this was a minor release, then you probably agree with me more than you think.
"Why did they move Flask from 2.3 to 3.0 if they don't follow semantic versioning?
This is a great question. I would have expected them to go to 2.4, but I believe I can guess why they decided to make a jump to v3.
One of the changes that were introduced in this release is this pull request which has been documented in the change log as "Restructure the code such that the Flask (app) and Blueprint classes have Sans-IO bases.". I expect for some people it may not be clear what this means. While this change is unrelated to my main topic, I need to delve on it a bit, so I hope you forgive me if I bore you with some details.
This was an effort lead by Phil Jones, one of the Flask core maintainers and also author of the Quart framework. The change was a complex reorganization of some of the foundational code of the Flask framework, in a way that allows Quart to inherit some behavior from the Flask code instead of having to duplicate it as it did until now.
The fact that they were able to make this big reorganization without having to introduce any breaking changes to Flask is amazing, and I applaud them for pulling it off. In spite of not adding (or removing) any features, this change is exciting and important for the future of Flask, as it allows Flask and Quart to share more code and become closer.
I can guess that they felt a bit nervous about this change because of its magnitude. They probably considered that bugs or regressions could have been introduced inadvertently, which would break existing features of the framework. And for that reason, maybe they decided this was a good time to jump to version 3, even though they normally would not.
I think this should have been a 2.4 myself, if they wanted to be consistent. It is especially strange to me that they decided to bump Werkzeug also to 3.0, given that this restructure affected only Flask. It appears their intention is to pair the version numbers of Flask and Werkzeug, but I don't recall anything being ever said about this.
The Python Versioning Model
The intention of the Flask team is to follow the Python model of versioning, where the major version number changes on very major releases, and the minor version number changes on major releases that are not an earth-shattering kind of major. They believe any m.n.0 release is a major release, and as such a candidate to receive breaking changes.
You are welcome to check the change logs for Flask and for Werkzeug. Search for "deprecate" to see how breaking changes were introduced in every single release of Flask and Werkzeug that is not a bugfix release as far back as 2.0.
I agree that a version numbering scheme is whatever the maintainers want it to be. If they want to follow the Python versioning style I have absolutely no problem with that, and in fact I use this style of versioning myself for some of my projects. So on this I'm with them.
But here I come once again with the receipts, to show you that their copying of the Python versioning model is partial, and that the Python team respects the limited time their users have to deal with deprecations and removals way more than they respect theirs. Let's look at an example from Python, shall we?
The Python team announced the deprecation of the smtpd
module on the 3.6.1 release, out in March 2017. The removal was carried out in release 3.12, in October 2023. Six and a half years of advance notice for developers to migrate to something else.
Do you want to know the dates for the deprecation and removal of the FLASK_ENV
environment variable in Flask? The deprecation announcement came with Flask 2.2.0, in October 2022. The variable was removed with 2.3.0, out in April 2023, just six months later and without any releases in between other than bug fixes.
Maybe that was an exception, so we should look at another example, just in case. How about the Flask-Login incident? They deprecated the werkzeug.urls
module with release 2.3.0, dated April 2023. The code was removed in release 3.0.0 on September 2023, without any new releases between the deprecation and the removal. This time they gave their users only five months of time to migrate.
I'll let you be the judge on whether their deprecation policy makes sense or not.
"They should have longer deprecation periods"
This was a comment made by people who do not fully agree with me, but see that I might be onto something with my complaining.
Yes, I think longer deprecations would be a nice improvement, even if they keep the volume of deprecations the same.
What the Flask team fails to understand is that people do not upgrade their Flask and Werkzeug packages with every release they put out, so a deprecation message that is up for just one release cycle is going to be missed by a lot of people. Python left the deprecation notice for smtpd
through six releases, between 3.6 (technically 3.6.1) and 3.11 and only then went and removed it in 3.12.
A variation of this was the suggestion that Flask should have long-term support releases (LTS), like Django. To me this feels like a much bigger project to implement, and I'm not sure the Flask team has the resources to manage something like this. It would sure be nice though, because that would allow people to jump from one LTS to the next every 2-3 years, knowing well in advance that there is going to be stuff that will break. An extension developer or content creator that does not have a lot of time could have a policy of supporting only LTS releases, so then the introduction of breaking changes would happen at predictable times and not as often as it happens now. This would allow for plans to be made, and to respond better to these changes.
"I don't have a problem with Flask, but upgrading Werkzeug feels like playing Russian roulette"
Several people shared stories of having issues with Werkzeug upgrades and having to try several different versions to find one that works with Flask and other dependencies. I believe the Flask team is more aggressive with deprecations on Werkzeug than they are with Flask, so I expect more people to have had bad experiences with Werkzeug upgrades.
What is Werkzeug? A collection of web development utilities, many of which are not used by Flask, but available for applications to use directly. So even though it is always referenced as a dependency of Flask, Werkzeug is also a primary dependency for a lot of applications and extensions. What's interesting is that nobody considers it as such, and the assumption is that whenever you install Flask the correct version of Werkzeug will be pulled automatically by the dependency system, which is not the case. I repeat this in case it isn't clear: pip
has no way to know what is the correct version of Werkzeug to use with a given set of Flask extensions.
Advice: think of Werkzeug as a primary dependency, separated from Flask. Make decisions on upgrading it or not upgrading it in the same way you do for Flask. Do not expect the correct Werkzeug version to be resolved automatically by pip
"If Flask-Login had an upper bound check on Flask this would not have happened"
This comment was also made by a lot of people who just read the start of my article and did not bother with the rest.
The argument is flawed in several ways. First of all, I do not believe it is a good idea for Python packages to place upper version constraints on their dependencies. Upper bound checks make it harder for the installer to work. If many packages put them on their dependencies then it is much more likely that there is no way to find versions of all the packages that satisfy all the constraints. So instead of getting a broken install, you would get a pip
error and no install at all.
My understanding is that the Flask team also shares this thinking, since they do not use upper bound version checks either. So what do you know, here one more time I agree with them.
Second, what would be the upper bound for Flask? Now that we are in 3.0 territory, you may suggest that flask<4 werkzeug<4
would be the correct upper bound, but we know that 3.1.0 is very likely going to have breaking changes as all minor releases do. So the upper bound should be what, flask<3.1 werkzeug<3.1
? Imagine what would happen if every Flask extension did this. Flask would release 3.1 and you would not be able to install a single extension anymore! Every extension developer would be forced to update their package (if anything just to bump the upper bound one release), and you would be blocked from upgrading Flask until all the extensions you use do it.
This is a bad solution, yet people continue to repeat it when versioning problems such as the Flask-Login one occur. I have learned as a result of comments about my article that the Dash project does this, and they only do it for Flask and Werkzeug, because they have been burned many times with unexpected breakages. It may be okay for them because nobody else does it, but if this idea catches on, then we would be in a situation that is much worse.
"You should pin all your dependencies and stop complaining!"
This argument has to be seen from two different sides. If you run a pip install
and you get a set of dependencies that work well with each other, then yes, you should generate a requirements file with all your dependencies in case you need to restore your environment at a later date, and you do this by running this command:
$ pip freeze > requirements.txt
The command records the versions of all your dependencies. This is the ones you care about, and the ones that are installed transitively. All of them should have their versions recorded in your requirements file. If you've done this, then at any time you can install the versions that you previously recorded, and it would be unlikely that you would have any trouble.
A lot of people manually add primary dependencies to their requirements file, and do not record transitive dependencies. This is a bad idea. You should always record all your dependencies in your requirements file.
To restore a working environment, you just have to do this:
$ pip install -r requirements.txt
When you want to upgrade a package you try the upgrade, and if you are satisfied, you update your requirements file again.
So this is actually a good advice, and I agree 100% with it, even though it was thrown at me with the implication that I don't know how to manage my projects or dependencies.
What some people miss is that there is another side to this. How do you create a valid requirements file at a time in which package incompatibilities are present, such as right now between Werkzeug and Flask-Login? For a beginner or someone who is not aware of the details of the current incident, there is no easy way to tell pip
to install a working set of versions that make Flask work well with Flask-Login. Consider these examples:
pip install flask flask-login
and this combination does not work.pip install "flask<3" flask-login
which also fails, because the incompatibility is between Werkzeug and Flask-Login.The only way to get a valid install with upper bounds is to learn what the issue is, so that you know that you also need to add an upper bound version check on Werkzeug:
$ pip install "flask<3" "werkzeug<3" flask-login
And this ties back to my advice above, which is that you have a better chance of success when you think of Werkzeug as a primary dependency.
Does the Flask community deserve to be forced into these types of hacks and all the time wasting they cause to get a working environment, even if the issue involves an unresposive extension that is not in direct control of the Flask team? I do not think so. I expect the Flask team to understand that these issues happen, and do everything they can to avoid them.
"This is Flask-Login's fault. What can the Flask team do if Flask-Login is unresponsive?"
Glad that you asked! I'll tell you exactly what the Flask team could have done the moment this problem was discovered, on the day of the release of Flask 3.0.0 and Werkzeug 3.0.0. Ready?
The Flask team could have restored the deleted code in Werkzeug and pushed a 3.0.1 bugfix release, to allow Flask-Login to work and remove any impact on users.
You don't agree? Fine. To me this is not even a choice. The Flask-Login team is unresponsive and have been for a long time, so they cannot be expected to rush out a fix. But the Flask team is right here with us, and could have ended this problem immediately, avoiding the pain the community is still suffering.
You may argue that this is unfair to the Flask team. I agree! But they are the only ones that can currently end this emergency. Nobody is being fair to the people who suffer this issue and do not have the necessary knowledge and experience to figure out the solution. I think it would have been a nice gesture from the Flask team to reinstate the deleted code, at least until Flask-Login can sort itself.
"Don't use Flask-Login"
That's great advice. What would you use instead? If you are working on a web application with Flask, unfortunately Flask-Login has no competition.
I expect a fork will eventually emerge, however, so maybe there will be a migration path in the near future.
Now something that a lot of people don't know, is that you do not have to use Flask-Login to authenticate in an API project (i.e. one that does not serve HTML pages but instead returns data to a front end, for example in JSON format). If you have an API, consider using my Flask-HTTPAuth extension for your authentication needs instead of Flask-Login. Even though I'm unhappy about the situation, I promptly fix all my extensions to account for breaking changes in Flask and Werkzeug.
"Miguel is upset because these changes force him to update his tutorials"
This is a funny one. But yeah, it is true.
As someone who has a collection of tutorials and Flask extensions covering a wide range of areas, I'm often directly affected by breaking changes in Flask and Werkzeug. You got me.
"Our aim is to make stable changes"
This a quote from Flask team member Phil Jones, taken from his blog post published in response to my article. Here is a bit more of his post:
Clearly then we have to make changes to Flask, but as we do so should avoid breaking users code and, if we know we have, communicate these changes well so that users know how to fix their code.
While I agree with this in principle, I feel the Flask team does not live up to this statement.
Before you jump at me claiming that I'm attacking the team, let me break up Phil's statement into a few pieces and discuss each one separately.
I understand that they have to make changes to Flask, and that some are going to be breaking changes.
I agree that they should avoid breaking the code of their users as much as possible, but I disagree with their current criteria, which allows many breaking changes that are unnecessarily disruptive.
The "communicate well" part has a few sides to it. I think they do a good job recording what breaking changes they make, so I compliment them on that. A period of 5-6 months and a single release between a deprecation announcement and removal is insufficient. And in spite of the breaking changes being documented, the fact is that there are too many, and trying to evaluate an upgrade that jumps over a few releases means that there is an overwhelming list of breaking changes to review.
Another passage from Phil's blog post touches on my claim that they often introduce unnecessary breaking changes:
I'd also like to emphasise that changes made simply to benefit the maintainers of Flask are good. We have limited time to cover a large codebase and easier maintenance means we can do more.
I have a bit of a problem digesting this. It sounds good as a general statement, but Phil fails to address that every time a breaking change is introduced, someone has to pay the price for it. I can accept that renaming a variable or argument, or getting rid of some old function can make a Flask maintainer feel better or even save a bit of time, but the cost of these changes is far from free. I would allow this way of thinking for a project that is in its early days and growing, but I really do not want to see things renamed, deprecated or removed in every Flask release. Flask is not only a mature framework, but also the most used in the entire Python ecosystem. Breaking changes in a project of Flask's standing should be the exception, not the norm.
But as I said at the start, it is fine if you disagree with me. Let the Flask team know how you feel about everything I presented, no matter if you are in favor or against, so that they have the best information to decide if a course correction is needed or not.
October 31st, 2023 update: Armin Ronacher, creator of Flask and Werkzeug has tweeted in support of my claims. Note that Armin has not been directly involved with these projects in the last few years, so none of my criticism was directed at him or his choices, but instead at the current maintainers.
In some ways a rare regret of mine in transitioning Flask over to a community is not properly communicating my core values. Backwards compatibility was always the value I held strongest. It's not shared with the folks currently maintaining it which is sad. https://t.co/N1VKuwRUel
— Armin Ronacher (@mitsuhiko) October 30, 2023