We Have To Talk About Flask

Posted by
on under

Flask 3.0 was released on September 30th, 2023, along with a parallel 3.0 release of Werkzeug, its main dependency. That day, the Flask-Login extension, one of the most popular of all Flask extensions, stopped working due to a backwards incompatible change introduced in Werkzeug. It is October 19th when I'm writing this, and Flask-Login remains broken. As a result, any person using my Flask Mega-Tutorial will hit issues, because my tutorial uses Flask-Login. Not only that, every Flask tutorial that features Flask-Login, from every author, in every language, in written or video form, is going to fail for as long as this problem remains. Hard to believe, right? (Update: a fixed release of Flask-Login was published on October 30th)

If this was the first occurrence of something of this nature in the Flask community, I would hope it would serve as a lesson for the Flask maintainers to learn from and avoid in the future. Sadly, this happens pretty much every time there is a major release of Flask, and sometimes minor ones too. Why does this happen? How can it be avoided? In this article I'll try to make an assessment of the current situation and how it can be prevented going forward.

There is now an update to this post as well.

What Happened with Flask-Login?

In case you have not been affected by this, let me show you what the problem is. Let's go ahead and install Flask-Login:

$ pip install flask-login
...
Installing collected packages: MarkupSafe, itsdangerous, click, blinker, Werkzeug, Jinja2, Flask, flask-login
Successfully installed Flask-3.0.0 Jinja2-3.1.2 MarkupSafe-2.1.3 Werkzeug-3.0.0 blinker-1.6.3 click-8.1.7 flask-login-0.6.2 itsdangerous-2.1.2

You can see here that Flask-Login installed version 0.6.2, which PyPI reports with a release date of July 25, 2022. A few dependencies were imported as well, because I did this in a brand new virtual environment. Among them, the new Flask 3.0.0 and Werkzeug 3.0.0, which are dated September 30th, 2023, also according to PyPI.

So far so good. Let's try to import Flask-Login in a brand new Python session:

>>> import flask_login
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/Users/miguel/venv/lib/python3.12/site-packages/flask_login/__init__.py", line 12, in <module>
    from .login_manager import LoginManager
  File "/Users/miguel/venv/lib/python3.12/site-packages/flask_login/login_manager.py", line 33, in <module>
    from .utils import _create_identifier
  File "/Users/miguel/venv/lib/python3.12/site-packages/flask_login/utils.py", line 14, in <module>
    from werkzeug.urls import url_decode
ImportError: cannot import name 'url_decode' from 'werkzeug.urls' (/Users/miguel/venv/lib/python3.12/site-packages/werkzeug/urls.py). Did you mean: 'urlencode'?

So here is the problem. Flask-Login fails to import because it wants this url_decode() function from the werkzeug.urls module that does not appear to exist.

How big of a deal is this? Let me put it this way. How many Flask books and tutorials can we estimate that exist out there? I'd risk that at least we are talking about a number in the thousands, if not more. I'm going to guess that a large part of these tutorials use Flask-Login. Every person trying to learn Flask with one of these since September 30th, 2023 and until a still unknown date later than today (October 19th) when this is finally addressed, is going to hit a roadblock that they are unlikely to be able to figure out on their own. Maybe they'll think the tutorial they are following is bad or out of date and will switch to a different one, only to get the same error once again. How many people is being affected by this? A few hundred? Thousands? More? I don't really know, but I believe it is a really big number.

The issues board for Flask-Login does not show any open issues about this. We can change the filters in the issue board to look at closed issues instead, and bingo, I counted 21 closed issues from the last couple of weeks where people complain about this. I found 3 pull requests intended to address this issue. Two were rejected, and one was merged on October 2nd. So the issue has actually been fixed, and what's missing is just a release with the fix to be pushed to PyPI, the Python Package Index.

Why is a release being delayed? I'm unclear on how many people have access to publish releases of this package. It appears that the original creator of Flask-Login isn't interested in maintaining the package anymore, and another person to whom he may have given access to the project is currently on vacation, but intends to get a release out when back. So in the end, this appears to be a bus factor issue more than anything else.

The Flask team added a deprecation warning for this function in the previous release of Werkzeug, so they feel the maintainers of Flask-Login had time to address the issue ahead of the function's removal.

But the Flask-Login team is not actively developing the extension anymore. Flask-Login is a mature library that does what it does well, so there hasn't been a need to make changes to it in recent years. The creator of the extension has probably moved on to other projects and maybe isn't using the extension anymore and is certainly not standing by and checking for deprecation messages from Flask to appear.

Blaming Flask-Login does not help when you consider that this url_decode() function that was removed from Werkzeug is used by an uncountable number of projects besides Flask-Login (including some of my own). We know that Flask-Login is likely going to be fixed within days or weeks at the worst, at which point we'll all forget this. But the large number of applications that use this function directly will hit this problem whenever they upgrade Flask, and this can be in the next week or in 5 years. I guarantee you that for years to come, reports of this failed import from Werkzeug are going to continue popping up in Stack Overflow and other developer forums.

This is probably going to get me some enemies, but I think the Flask developers are more responsible for this disaster than the Flask-Login team is. This isn't the first time new Flask releases break extensions or content. In fact, I have come to expect that every Flask release will require me to rush fixes for some of my packages or my content. Sometimes even minor releases manage to break some of my things.

Backwards Compatibility

The current issue with Flask-Login and the many similar ones from the past are all issues of backwards compatibility. New versions of Flask and/or Werkzeug introduce significant changes, so extensions and tutorials that were designed for older versions start to fail and need to be updated so that they continue to work.

Every time I discuss backwards compatibility, what comes to mind are the contrasting views that Microsoft and Apple hold on the topic.

Microsoft has always gone to great lengths to ensure backwards compatibility of their products, especially their Windows operating system, and also their XBox gaming console. They considered it a great asset that people would confidently upgrade their OS or console, knowing that all the applications and games that they owned would continue to work as before. Not sure if this still goes on, but Microsoft used to have entire teams dedicated to testing software on soon to be released versions of Windows, and create patches in the OS to ensure the software continued to work without needing an update from the software vendor. Of course all this effort is unknown to most people, even though it was tremendously successful. Why? Because the success metric for this work is actually very boring, the goal is that nothing breaks and users can continue to use their software through upgrades. And of course this doesn't make big news.

Apple has a different stance. They believe that maintaining old stuff is too much of a burden, so from time to time they introduce major changes and innovations to their products, usually with limited transition options. Their Macintosh line of computers was initially based on the Motorola 68000 processors. In 1994 they decided to migrate to a PowerPC architecture. In 2005 they migrated again, this time to Intel CPUs. Finally in 2020 they started yet another platform migration to their own Apple Silicon, based on the ARM architecture. Interestingly enough, people (and especially developers) love them, in spite of these disruptive changes.

Who do you align with? I honestly do not see these two views as opposing, and can appreciate the good in both.

The Microsoft thinking that the software and games that you use every day are the most important part of your computer or console, and that the operating system is just there in a minor supporting role for them greatly resonates with me.

But I also see how from time to time having a hard reset allows Apple to introduce innovation and freshness into their product lines. Apple users have come to expect these changes and willingly tolerate some inconvenience during the transition period in exchange for significant improvements in efficiency, performance, style or a combination of them.

Flask and Backwards Compatibility

So who do you think the Flask team aligns with in terms of backwards compatibility?

They do not align with Microsoft for sure! But do they align with Apple? I actually do not think so.

Let's look at the specific error that appeared in Flask-Login. The extension tried to import the url_decode() function from Werkzeug. Where has this function gone? I did some digging, and can present you with its complete history:

  • In releases prior to 1.0.0, the function was imported with from werkzeug import url_decode
  • In release 1.0.0 (February 2020), the function was relocated and the import changed to from werkzeug.urls import url_decode
  • In release 2.3.0 (April 2023), a deprecation message was added when the function was used
  • In release 3.0.0 (September 2023), the function was removed, with the idea that people should migrate to a similar function available in the Python standard library.

So as you see, the function did not evolve or become better (at least not in any significant way). It just moved around a couple of times until the day it was removed.

I mentioned above that this isn't the first time that Flask releases cause breakages. In fact, I have seen this pattern repeat time and time again. There are two recent instances that I'm going to mention, just so that you don't think I'm exaggerating. The first one is related to how to configure Flask's debug vs. production modes using an environment variable:

  • In versions prior to Flask 1.0, the FLASK_DEBUG environment variable was used. A value of 1 configured debug mode. A value of 0 or undefined disabled debug mode.
  • In version 1.0 (2018), the FLASK_ENV environment variable was introduced to replace FLASK_DEBUG. You would set it to development to enable debug mode, or production to disable debug mode, similar to the NODE_ENV variable used by the Express framework in Node.js. The FLASK_DEBUG variable was not removed nor deprecated, but the documentation and examples encouraged developers to use FLASK_ENV.
  • In version 2.2.0 (2022), the FLASK_ENV environment variable was deprecated and the documentation was reverted to recommend FLASK_DEBUG.
  • In version 2.3.0 (2023), the FLASK_ENV environment variable was removed, leaving FLASK_DEBUG once again as the only environment variable to control debug mode. A circle of life kind of a thing, I guess.

The other instance that affected me personally was related to the get_engine() method of the Flask-SQLAlchemy extension, which is currently maintained by one of the Flask core team members. This is a publicly documented method that survived without any major changes until the 3.0 release, but then this happened:

  • Prior to version 3.0.0, the method signature was get_engine(app=None, bind=None).
  • In version 3.0.0 (October 2022), the method signature was changed to get_engine(bind_key=None). In addition to the removal of one argument and the renaming of the other, the function was deprecated (why would you change a function in a major way and deprecate it at the same time?).
  • In preparation for release 3.1.0 (September 2023), get_engine() was removed, but then the maintainer changed their mind and added it back (My complaining may have had some influence on this..., not sure).
  • The function is now scheduled to be removed in upcoming release 3.2.0, and will be replaced by a property called engines that provides exactly the same information, but as a dictionary.

I think Flask, like Apple, likes to break things without worrying too much about the past.

But unlike Apple, and this is the part I have a lot of trouble with, these refactorings do not give the community anything in exchange! The Flask team constantly reorganizes the code in silly and meaningless ways, forcing extension developers and content creators to adapt just to keep things from breaking, but these changes do not contribute any tangible improvements that justify the effort.

So Flask is definitely not aligned with Apple either, in my view.

Where Do We Go From Here?

This is unfortunately a question I do not have a clear answer for.

It is likely that the Flask core team will continue to refactor and change the code in ways that I'm sure makes sense to them, but that do not provide substantial benefits to the community at large. If this is the case, I will continue to keep up with their changes, as I'm sure most other extension developers and content creators will. One day I may decide to not care anymore, and then one of my projects may end up being at the center of another incident like the Flask-Login one.

But maybe, given the magnitude of the current issue, this is the straw that broke the camel's back, as they say, and from now on Flask core developers will feel pressured to think it twice or three times before they do more gratuitous refactoring.

I surely hope so!

I have written an update to this post with a collection of comments and feedback I have received.

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.

Become a Patron!

Hello, and thank you for visiting my blog! If you enjoyed this article, please consider supporting my work on this blog on Patreon!

41 comments
  • #26 Bruno Afonso said

    @Chris, You are probably right.

    my error: Lib\site-packages\flask_babelex__init__.py", line 20, in <module>
    from flask import _request_ctx_stack
    ImportError: cannot import name '_request_ctx_stack' from 'flask'

    It references babelex

    I think I will continue the development of the app, then add authentication later.

    Will wait and see if they update Flask-login

  • #27 Moon said

    I think when I ran into this problem recently I found a suggestion on Stack Overflow to replace the utils.py file in the werkzeug extension with the updated file on github. I did that and it fixed the issue:

    https://github.com/maxcountryman/flask-login/blob/main/src/flask_login/utils.py

    Regarding the backwards-incompatible changes for no good reason, I 100% agree that it is senseless and borderline idiotic unless they can demonstrate a very good reason for doing so. And Apple is no role model to follow in this regard, considering their unabashed use of planned obsolescence. They don't care because they know their brainwashed fan base will just buy the latest model of whatever they produce.

  • #28 Michaël de Vries said

    al of these problems would be fixed by semantic versioning:
    you make a backwards incompatible change -> major version bump.
    If the pallets project would implement this versioning system the plugins would be able to reliably pin their versions of flask and werkzeug on a verified major version range.

  • #29 Miguel Grinberg said

    @Michaël: I don't think it would solve "all" the problems, but if they limit themselves to introduce breaking changes only in major releases that would definitely be an improvement. What I see is that they could start releasing way more major releases than they release now. If they change to semver and then put out a major release ever 5-6 months we would still be in the same problem, because at the end of the day, version numbers are just numbers that have no meaning. What matters is the frequency at which breaking changes are introduced.

  • #30 Eliseu said

    It's a big problem.
    Every time I need to update my applications (because I think It's important to keep everything updated), my main dependency is Flask.

    You can imagine the work that is necessary to keep an application updated.

  • #31 Nixellion said

    I am so with you on this. I don't understand the obsession with introducing breaking changes when they can be avoided. I do understand the desire to make the code better structured, to reduce redundant code so it's easer and faster to work with, but I believe something like this should not be done often, on a "1 variable at a time" basis. I believe such restructures can and must happen, but like, once a few years maybe.

    There's a reason why in Linux kernel development the #1 rule set by Linus Torvalds is "Don't break user space" (as far as I know). It's to add to your comparison of Windows vs Apple, Linux is totally on the Windows side of things here.

    What I'm afraid pallets developers don't understand (and this mentality leaks into other projects made by them, like WTForms) is that their projects are a part of a huge global ecosystem. For every project they have there are thousands of other libraries that depend on it, hundreds of thousands of other projects that people use and rely on, and millions of people working on those. And this introduces 2 global problems:

    1. Whenever they introduce a breaking change they force all those people to spend their time fixing their code. For projects you can pin a version, so maybe not every project will suffer from it, but many will have to be updated at some point. For libraries - maintainers just have to update their libraries if they want them to stay relevant. So with the numbers above I think it's safe to say they are wasting hundreeds of thousands if not millions of people's workhours globally. It just seems like such a waste to me, when introducing a change in a backwards compatible manner often takes less than a few minutes, from the examples I've seen. Or when it's not even necessary.

    2. They break their ecosystem, they shed many projects off it. Taking Flask as an example, it's core difference and appeal from Django is it's modularity. Flask competes with frameworks like Django only when it has what essentially is an 'asset store' of extensions that you can choose from. And you can even choose different flavors of the same things, like if flask-login is too complicated or does not align with your project, you can choose flask-simplelogin or something else. Or write your own extension (and suffer from having to fix it on a breaking change every few months). So how many extensions will stop working, and will never be updated to new versions? How many developers will stop maintaining their extensions exactly because of how much time and effort it takes trying to keep up with all the breaking changes?

    A more drastic example, that I don't think fits very well, but I still think is somewhat relevant:
    Imagine what would happen to Android if every few months they introduced a change in an update that broke 90% of apps. Seems like it would just die out and be replaced by something stable.

    And I have to say, that this so far is the single problem I have with flask and pallets projects. They're perfect for me otherwise.

  • #32 Biswajit said

    I think that Flask-Login project should specify a loose dependency of Flask in their setup, like allow it to work with minor changes in Flask but not major changes because as you mentioned Flask has a behavior of breaking things in its major releases.
    With this, Flask-Login maintainers could bump up their Flask dependency only after making sure that package is compatible with newer version of Flask. This would prevent sudden breaking of package.

  • #33 Miguel Grinberg said

    @Biswajit: there are no Flask-Login maintainers. This extension has been undermaintained for many years. Also there are no "minor" releases of Flask. All releases are considered major, except those with a change in the 3rd version number, which are bug fix releases. See my update to this post linked in the introduction for more details.

  • #34 Joshua Kugler said

    This is exactly what I would expect from a major release (3.0 in this case). It appears flask-login was not properly pinning its dependencies to "Flask < 3.0". This is not a probably with Flask, this is a problem with plugin authors not properly pinning their dependencies.

  • #35 Miguel Grinberg said

    @Joshua: you are wrong on several accounts:
    1. Flask does not follow semantic versioning, so a 2.3 or a 3.0 are both major releases, according to them.
    2. Upper bounds on dependencies is a terrible idea. If everybody did that we would be much worse.
    3. The question is not who's to blame, the question is who can fix this the sooner. At this point only the Flask team can, and they do not want to.

    Please read my follow up article to understand why you are wrong.

  • #36 Tim said

    But this is why deprecation warning exists, and why dependencies can be pinned.A package which has gone stale and which is not careful about its dependencies is always vulnerable to future changes. Your complaint is that the flask change is in your opinion trivial. But a major version change is a major change because it can introduce breaking changes; some you may find trivial, but that's not your call. Do you expect a 2022 version of flask-login to work five years time with the latest Flask? Or ten years? So why be arbitrary and complain about this new release? You are directing your ire in the wrong place, and Flask maintainers are perfectly entitled to point to their deprecation warnings and the existing pinning tools. Maybe deprecation warnings should have a lifetime of two major releases, but unless flask-login updates in reaction to the deprecation warning, or pins its dependencies, the same problem will happen, which also means we can point to the solutions which already exist.

  • #37 Miguel Grinberg said

    @Tim: Every release of Flask that ends in ".0" is a major release. 2.1.0, 2.2.0, 2.3.0, and 3.0.0 are all major releases, and they all had a batch of removals and a new batch of deprecations to be removed in the next release. This happens every few months.

    Do you expect a 2022 version of flask-login to work five years time with the latest Flask?

    Considering that Flask is only useful when it is combined with many extensions, I would expect the Flask team to ensure that the extension ecosystem does not break. Your attitude of shrugging and blaming an uninterested extension developer while leaving lots of people with no options is really not what I would expect in a thriving community.

    unless flask-login updates in reaction to the deprecation warning, or pins its dependencies, the same problem will happen

    Flask-Login is not maintained. Nobody who can publish releases of Flask-Login is looking at deprecation warnings. But until a fork or alternative solution emerges, there are no other options. How does it make sense for Flask to put themselves in a position where there is no extension to handle logins anymore?

    I'm sorry but everybody who has a position similar to yours should rethink who is at the center in the Flask ecosystem. Flask core developers are not in the center, they are not the most important. The most important is the community who develops with Flask. They deserve better than this.

  • #38 gadabast said

    I understand the frustration with the current situation. Probably there isn't only one valid point of view here. As other comments pointed out, major releases are allowed to make breaking changes. Your point, very understandably, is that these breaking changes shouldn't be about "refactorings of no importance" (quoting one of your previous comments).
    However, what would have changed if the breaking refactoring had been of the utmost importance instead? This would have broken your tutorials in any case, right?

    Also, I assume that the Flask environment is full of third-party extensions like Flask-Login. I just googled and found this list: https://github.com/humiaozuzu/awesome-flask. So, should Flask maintainers test their major releases against each of these extensions? And what happens if one of them isn't maintained anymore? Should they not release their changes? Or how should they do it?
    I'm referring to major releases and important refactorings. Having breaking changes on minor releases is of course something to avoid.

    I definitely agree with you that this is an issue. I tend to see it more on the side of Flask-Login, but I can see your point and I might change my mind after giving it some more thought. If you were a Flask maintainer, what would you do in a situation in which a (necessary) breaking change would be unsupported by third-party extensions?

  • #39 Miguel Grinberg said

    @gadabast:

    major releases are allowed to make breaking changes

    Flask has released breaking changes in 2.1, 2.2 and 2.3, as well as in 2.0 and 3.0. All Flask releases are major releases.

    However, what would have changed if the breaking refactoring had been of the utmost importance instead? This would have broken your tutorials in any case, right?

    I think you should not be worried about my tutorials breaking. I use a lot of packages in my content, and I have to make updates due to many things changing and improving. I don't really mind when I have to update an article and show an improved solution. That is actually great and I'm all for improving my content as the packages I teach improve.

    So, should Flask maintainers test their major releases against each of these extensions?

    Some of the extensions are important and are used by most Flask applications. So yes, I believe the Flask project should ensure that it plays nice with core extensions and should coordinate with extension maintainers to release breaking changes without disruptions to users.

    And what happens if one of them isn't maintained anymore?

    I would expect the Flask project to avoid breaking it, at least until a replacement for the unmaintained extension exists. The fact that the Flask team is okay breaking Flask-Login, which has no competition in the ecosystem is really weird.

    Should they not release their changes? Or how should they do it?

    They should not have, correct. There is currently no easy way to implement login forms in a Flask application right now. How does that make any sense?

    Having breaking changes on minor releases is of course something to avoid.

    The Flask team release breaking changes with every release.

    what would you do in a situation in which a (necessary) breaking change would be unsupported by third-party extensions?

    I have a much more conservative view on breaking changes. I'm not against them, but I introduce them judiciously. I also have utmost respect for the community of users of my projects, and don't want to break their applications.

  • #40 Jack said

    The new "working" version of flask login has been released but still, with Flask 3.0, Werkzeug 3.1.0 and Flask-login 0.6.3 I still get the ImportError: cannot import name 'url_parse' from 'werkzeug.urls'.

    Why is that ?

  • #41 Miguel Grinberg said

    @Jack: I am not involved with the work that was done to fix Flask-Login, so I don't know. You may want to ask in the Flask discord.

Leave a Comment