We Have To Talk About Flask

13 min read Original article ↗

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.

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

Thank you for visiting my blog! If you enjoyed this article, please consider supporting my work and keeping me caffeinated with a small one-time donation through Buy me a coffee. Thanks!