Main Ecosystems
HomePython CorePandas ReferenceNumPy ScientificFastAPI & PydanticDjango Enterprise
More Ecosystems
Environment & SetupRequests & HTTPAsyncIO ConcurrencyObject-Oriented OOPPyTorch Deep LearningScikit-Learn MLFlask FrameworkWeb ScrapingDatabase & ORMDevOps & Docker

Flask/Werkzeug: ImportError: cannot import name url_decode from werkzeug.urls

Verified FixPython 3.10+Werkzeug 3.0+, Flask 3.0+Silo: flask

Quick Fix / Solution Rapide

This error occurs when Python tries to import url_decode from werkzeug.urls, which was removed in Werkzeug 3.0. Use Python's built-in urllib.parse.parse_qs or urllib.parse.parse_qsl instead.

Root Cause Analysis

This error occurs when Python web applications running Flask or older extensions attempt to import url_decode, url_encode, or url_quote from the werkzeug.urls module after upgrading to Werkzeug 3.0.0+.

Root Cause 1: Werkzeug 3.0.0 Breaking Changes and Deprecation Removals

In Werkzeug 3.0.0, the Pallets team eliminated redundant URL parsing wrappers that duplicated Python's standard library. Legacy functions such as url_decode, url_encode, url_quote, url_unquote, and url_parse were completely removed from werkzeug.urls. Any code attempting from werkzeug.urls import url_decode fails immediately with ImportError: cannot import name 'url_decode' from 'werkzeug.urls'.

Root Cause 2: Outdated Flask Extensions (Flask-Login, Flask-JWT, Flask-Admin)

Older versions of popular Flask extensions were authored when url_decode was standard. When developers run pip install --upgrade flask werkzeug, the updated Werkzeug 3.x library breaks un-upgraded third-party extensions that still import the legacy functions.

Root Cause 3: Manual Query String Parsing in View Functions

Developers who copied legacy Werkzeug snippets for manual query string or form data parsing encounter this error when migrating their projects to modern Python 3.11/3.12 environments.

Root Cause 4: Discrepancy Between url_decode and urllib.parse

The standard Python library provides urllib.parse.parse_qs and urllib.parse.parse_qsl directly in Python core, which is standard, fast, and does not require third-party dependencies.

Reproduction Code (MCVE)

Example: Bug Reproduction
# Simulating Werkzeug 3.0+ environment where werkzeug.urls deprecated helpers are removed
class MockWerkzeugUrlsModule:
    """Simulates modern werkzeug.urls without deprecated url_decode."""
    __name__ = "werkzeug.urls"

werkzeug_urls = MockWerkzeugUrlsModule()

# Attempting to import removed url_decode raises ImportError
if not hasattr(werkzeug_urls, "url_decode"):
    raise ImportError("cannot import name 'url_decode' from 'werkzeug.urls'")

Solution 1: Use Python's Built-in urllib.parse.parse_qs

Replace werkzeug.urls.url_decode with Python's standard library urllib.parse.parse_qs or urllib.parse.parse_qsl.

Example: Recommended Solution
import urllib.parse

# Solution 1: Use standard library urllib.parse
query_string = "search=python&category=backend&page=2&tags=flask&tags=web"

# parse_qs returns a dictionary mapping keys to lists of values
parsed_dict = urllib.parse.parse_qs(query_string)
print("Parsed with parse_qs:")
print(parsed_dict)

# parse_qsl returns a list of (key, value) pairs preserving duplicate keys
parsed_pairs = urllib.parse.parse_qsl(query_string)
print("\nParsed with parse_qsl:")
print(parsed_pairs)

assert parsed_dict["search"] == ["python"]
assert parsed_dict["page"] == ["2"]

Solution 2: Use Flask request.args for Incoming HTTP Requests

In Flask view functions, access parsed query parameters directly via request.args without manually decoding raw query strings.

Example: Alternative Solution
from werkzeug.datastructures import MultiDict

# Demonstrating how modern Flask/Werkzeug exposes query arguments via MultiDict
query_data = MultiDict([("search", "python"), ("tag", "web"), ("tag", "api")])

print("Single value lookup:", query_data.get("search"))
print("Multi-value list lookup:", query_data.getlist("tag"))

assert query_data.get("search") == "python"
assert query_data.getlist("tag") == ["web", "api"]

If this error originates inside a third-party package in your virtual environment (such as an older flask-restx or flask-jwt-extended), you should upgrade that package with pip install --upgrade flask-restx. If the package is unmaintained, you can pin Werkzeug in your requirements.txt (werkzeug<3.0.0) as a temporary stopgap until you migrate to modern alternatives.

Another related Werkzeug 3.0 change is werkzeug.urls.url_quote being replaced by urllib.parse.quote.

Contrast url_decode with json.loads: url_decode parses URL percent-encoded query strings (key1=val1&key2=val2), whereas json.loads parses structured JSON objects ({"key1": "val1"}).