The go-to resource for upgrading Python, Django, Flask, and your dependencies.

Flask 3.1 to 4.0 Migration Guide: Breaking Changes in Werkzeug 3.0 Routing


When a viper strikes prey, it commits fully—no partial contact, no hesitation. The fangs sink precisely into vital tissue, venom acting swiftly and surely on the exact target. Any glancing blow wastes energy and risks escape.

Flask 4.0’s new Werkzeug 3.0 router works similarly. The old router permitted partial converter matches (like /users/2df parsing as id=2), leading to unexpected behavior and slower matching. The new state machine router demands full, strict matches, delivering 2-5x faster performance while eliminating those bugs.

This guide walks you through all the breaking routing changes, with old vs. new code examples and step-by-step fixes so we can migrate your app confidently.

Why Migrate to Flask 4.0 / Werkzeug 3.0?

ChangeImpactFix Effort
New Router (Werkzeug 2.2+)Faster matching, fixes bugsTest routes
Custom Converterspart_isolating=False req for / regexUpdate classes
strict_slashes=FalseCorrect /path → /path/ matchingVerify redirects
Host MatchingSERVER_NAME less restrictiveSet TRUSTED_HOSTS
URL CharsetUTF-8 only, no Map.charsetRemove hacks

Benefits: 2-5x faster route matching in benchmarks, enhanced security by rejecting partial converter matches (like <int:2df>).

Trade-offs: Requires testing routes and updating custom converters, but eliminates bugs and improves long-term maintainability.

Prerequisites

# Virtualenv
$ python -m venv flask4-env
$ source flask4-env/bin/activate  # Linux/macOS
$ flask4-env\\Scripts\\activate  # Windows

# Backup app; pin deps
$ pip install flask==3.1 werkzeug==3.0.1
$ flask --version

Key Breaking Changes in Detail

1. New Router: Strict Matching & Prioritization

Werkzeug 2.2+ router fixes partial converters (<int:a> rejects “2df”), prioritizes simple routes.

Old (buggy):

@app.route('/users/<int:id>')  # /users/2df matched id=2

New (correct):

# The same route definition now rejects partial matches.

# test_new_router.py
client = app.test_client()
rv = client.get('/users/2df')
assert rv.status_code == 404, 'Partial match rejected'

rv = client.get('/users/123')
assert rv.status_code == 200, 'Valid int matches'

Run $ python test_new_router.py to verify.

2. Custom Converters: part_isolating=False

Custom converters whose regex matches / now require explicit part_isolating=False (Werkzeug 2.3+ detects / in regex and defaults to False, but we must set it explicitly for clarity).

Old:

from werkzeug.routing import BaseConverter

class SlashConverter(BaseConverter):
    regex = r'[^/]+(/[^/]+)*'  # Matches /

app.url_map.converters['slash'] = SlashConverter()

New:

class SlashConverter(BaseConverter):
    regex = r'[^/]+(/[^/]+)*'
    part_isolating = False  # Required!

app.url_map.converters['slash'] = SlashConverter()

3. strict_slashes=False Fixes & Redirects

/path now matches /path/ correctly; append_slash_redirect defaults 308.

Update (recommended for consistency):\n```python @app.route(‘/path’, strict_slashes=False) # Defaults to 308 redirect now


To verify the redirect behavior:\n```python
client = app.test_client()
rv = client.get('/path')  # No trailing slash
assert rv.status_code == 308
assert rv.headers.get('Location') == '/path/'

This ensures /path redirects to /path/ correctly, improving UX without security risks.

4. Host/Trusted Hosts (Flask 3.1+)

SERVER_NAME no longer blocks subdomains if host_matching=True.

New:

app.config['TRUSTED_HOSTS'] = ['*.example.com', 'localhost']
app.config['SERVER_NAME'] = 'example.com'  # Allows subdomains

5. UTF-8 URLs Only

Map.charset / Request.url_charset removed; always UTF-8.

Old:

url_for('view', param='café')  # Might fail non-UTF8

New:

# url_for now always uses UTF-8 encoding
from flask import url_for
url = url_for('view', param='café')
assert '%C3%A9' in url  # Properly encoded
print(url)  # /view?param=caf%C3%A9

Handle invalid input by sanitizing upstream or catching UnicodeError.

Step-by-Step Migration Guide

Step 1: Update Dependencies

pip install --upgrade "flask>=4.0" "werkzeug>=3.0"
pip freeze > requirements-new.txt

Step 2: Inspect Routes

$ flask routes  # List all; check for conflicts

Step 3: Fix Custom Converters

$ grep -r “BaseConverter\|part_isolating” src/ Add part_isolating = False.

Step 4: Test strict_slashes & Hosts

# test_app.py
from app import app
client = app.test_client()
rv = client.get('/path')  # Expect 308 → /path/
assert rv.status_code in (200, 308)

Step 5: Update Config & Deprecations

Deprecated: Werkzeug removed OrderedMultiDict; replace with MultiDict:\n```python from werkzeug.datastructures import MultiDict headers = MultiDict.fromkeys([‘X-Forwarded-For’], ‘127.0.0.1’)

This maintains insertion order where needed.

## Verification & Testing
```bash
$ pytest  # Run suite
$ flask run --debug  # Manual test routes
$ pip-audit  # Check deps [28-auditing-flask...]

Success: No deprecation warnings, routes match expected.

Troubleshooting

IssueCauseFix
Converter match failsMissing part_isolating=FalseAdd to class
Host 404TRUSTED_HOSTS unsetConfig list
URL encode errorNon-UTF8 charsSanitize input
Partial matchOld WerkzeugUpgrade confirmed

Conclusion

Flask 4.0 with Werkzeug 3.0 provides faster routing and stricter security. Test your custom converters first, then systematically verify all routes and redirects.

Related:

flask routes your app today!

Sponsored by Durable Programming

Need help maintaining or upgrading your Python application? Durable Programming specializes in keeping Python apps secure, performant, and up-to-date.

Hire Durable Programming