Flask Redirect: How to Redirect URLs in Python

How to redirect URLs in Flask using redirect(), url_for(), and status codes. Covers redirects after form submission, error handling, and best practices.

Flask handles redirects through its built-in redirect() function, which returns an HTTP response with a Location header and a 3xx status code. The pattern is clean and predictable, but there are details around status codes, URL generation, and post-redirect-get flows that trip up developers regularly. This guide covers every common redirect scenario in Flask. For background on HTTP redirect behavior, see HTTP Redirect Status Codes.

Basic Redirect with redirect()

The redirect() function is imported from Flask and returns a response object:

from flask import Flask, redirect

app = Flask(__name__)

@app.route('/old-page')
def old_page():
    return redirect('/new-page')

By default, redirect() sends a 302 Found response. The browser receives the Location header pointing to /new-page and makes a new GET request to that URL.

Setting the Status Code

Pass the status code as the second argument:

from flask import redirect

@app.route('/old-page')
def old_page():
    return redirect('/new-page', code=301)

Flask supports all standard redirect codes:

  • 301 -- Moved Permanently. Use when the old URL is gone for good. Search engines transfer ranking signals to the new URL.
  • 302 -- Found. The default. Indicates a temporary redirect. Search engines keep the old URL in their index.
  • 303 -- See Other. Tells the browser to follow the redirect with a GET request, regardless of the original method. Useful after POST requests.
  • 307 -- Temporary Redirect. Like 302, but the browser must preserve the original HTTP method.
  • 308 -- Permanent Redirect. Like 301, but the browser must preserve the original HTTP method.

For a detailed comparison of when to use each code, see 301 vs 302 Redirects and Types of Redirects.

Using url_for() with Redirects

Hardcoding URLs in redirects creates maintenance problems. When you rename a route or change its path, every hardcoded redirect breaks. Flask's url_for() function generates URLs from endpoint names, keeping your redirects in sync with your route definitions:

from flask import redirect, url_for

@app.route('/dashboard')
def dashboard():
    return 'Dashboard'

@app.route('/home')
def home():
    return redirect(url_for('dashboard'))

The string 'dashboard' is the function name of the target route. If you later change the route from /dashboard to /app/dashboard, the redirect updates automatically because url_for() generates the URL from the route definition, not from a hardcoded string.

url_for() with Parameters

For routes that accept parameters, pass them as keyword arguments:

@app.route('/user/<int:user_id>')
def user_profile(user_id):
    return f'Profile for user {user_id}'

@app.route('/me')
def my_profile():
    current_user_id = get_current_user_id()
    return redirect(url_for('user_profile', user_id=current_user_id))

Any keyword arguments that do not match route parameters are appended as query string parameters:

url_for('search', q='flask redirect', page=2)
# Produces: /search?q=flask+redirect&page=2

External URLs with url_for()

To generate an absolute URL (including the scheme and domain), use _external=True:

url_for('dashboard', _external=True)
# Produces: https://example.com/dashboard

This is useful when the redirect target needs to be an absolute URL, such as when redirecting to a different subdomain or when constructing URLs for email links.

Redirect After Form Submission (Post-Redirect-Get)

The post-redirect-get (PRG) pattern prevents duplicate form submissions when users refresh the page after submitting a form. Without PRG, refreshing the page resends the POST request, potentially creating duplicate records.

from flask import redirect, url_for, request, flash

@app.route('/contact', methods=['GET', 'POST'])
def contact():
    if request.method == 'POST':
        name = request.form.get('name')
        email = request.form.get('email')
        message = request.form.get('message')

        save_contact_message(name, email, message)
        flash('Message sent successfully.')
        return redirect(url_for('contact_thanks'))

    return render_template('contact.html')

@app.route('/contact/thanks')
def contact_thanks():
    return render_template('contact_thanks.html')

The redirect after the POST ensures that the browser's current request is a GET to /contact/thanks. If the user refreshes, they resend the GET request, not the POST. The flash() function stores a message in the session that the template can display once.

Using a 303 status code makes the PRG pattern explicit:

return redirect(url_for('contact_thanks'), code=303)

A 303 tells the browser to always follow the redirect with GET, even if the original request was POST. This is technically more correct than a 302 for PRG, though both work in practice.

Redirecting Based on Authentication

A common pattern is redirecting unauthenticated users to a login page:

from flask import redirect, url_for, session, request
from functools import wraps

def login_required(f):
    @wraps(f)
    def decorated_function(*args, **kwargs):
        if 'user_id' not in session:
            return redirect(url_for('login', next=request.url))
        return f(*args, **kwargs)
    return decorated_function

@app.route('/login', methods=['GET', 'POST'])
def login():
    if request.method == 'POST':
        user = authenticate(request.form['email'], request.form['password'])
        if user:
            session['user_id'] = user.id
            next_page = request.args.get('next', url_for('dashboard'))
            return redirect(next_page)
        flash('Invalid credentials.')

    return render_template('login.html')

@app.route('/dashboard')
@login_required
def dashboard():
    return render_template('dashboard.html')

The next parameter preserves the user's intended destination. After logging in, they are redirected to the page they originally tried to access. Be careful with the next parameter. Validate that it points to your own domain to prevent open redirect vulnerabilities. See What Is an Open Redirect? for details on this attack.

Error Handling Redirects

Flask's error handlers can redirect users instead of showing an error page:

@app.errorhandler(404)
def page_not_found(error):
    # Optionally redirect old URLs to new ones
    requested_path = request.path
    new_path = lookup_redirect(requested_path)

    if new_path:
        return redirect(new_path, code=301)

    return render_template('404.html'), 404

This pattern is useful during site migrations. When a user hits a URL that no longer exists, you check a redirect map before returning a 404. If a mapping exists, you send a 301 redirect. If not, you show the 404 page.

Redirect on Abort

You can also redirect within Flask's abort() flow by registering error handlers that redirect rather than render:

from flask import abort

@app.route('/old-section/<path:subpath>')
def old_section(subpath):
    abort(301, response=redirect(url_for('new_section', subpath=subpath)))

However, this pattern is unusual and harder to maintain. A direct return redirect() is clearer.

Blueprint Redirects

In larger Flask applications organized with Blueprints, url_for() requires the blueprint name as a prefix:

from flask import Blueprint, redirect, url_for

auth = Blueprint('auth', __name__, url_prefix='/auth')

@auth.route('/logout')
def logout():
    session.clear()
    return redirect(url_for('auth.login'))

@auth.route('/login')
def login():
    return render_template('login.html')

The endpoint name is 'auth.login', not just 'login'. If you omit the blueprint prefix and another blueprint has a login endpoint, Flask may resolve to the wrong one.

Use url_for consistently

Always use url_for() instead of hardcoded URLs in your redirects. It catches broken routes at startup (with url_for in templates) or at request time (with url_for in views), rather than silently sending users to a 404.

Redirect to External URLs

Redirecting to an external URL works the same way as internal redirects:

@app.route('/github')
def github():
    return redirect('https://github.com/your-org/your-repo')

There is no special function for external redirects. Just pass the full URL string to redirect(). Be cautious about redirecting to user-supplied external URLs without validation.

Common Mistakes

Forgetting to return the redirect. The redirect() function creates a response object, but if you do not return it from your view function, Flask ignores it and renders the view's default response (or raises an error if there is no return value).

Using 301 for temporary situations. Browsers cache 301 redirects aggressively. If you redirect with a 301 during development and then remove the redirect, the browser still follows the cached redirect. Use 302 during development and switch to 301 only when the redirect is permanent.

Not validating redirect targets. Any redirect that uses user input (query parameters, form data, headers) to determine the destination URL is a potential open redirect vulnerability. Always validate that the target URL belongs to your domain.

Circular redirects. If route A redirects to route B and route B redirects to route A, the browser displays an error after too many redirects. Test your redirect logic carefully, especially when authentication redirects interact with role-based redirects.

Testing Redirects

Use Flask's test client to verify redirect behavior:

def test_old_page_redirects(client):
    response = client.get('/old-page')
    assert response.status_code == 301
    assert response.headers['Location'] == '/new-page'

def test_redirect_follows(client):
    response = client.get('/old-page', follow_redirects=True)
    assert response.status_code == 200
    assert b'New Page Content' in response.data

The follow_redirects=True parameter tells the test client to follow the redirect chain, which is useful for testing that the final destination renders correctly.

From the command line, verify redirects with curl:

curl -I -L https://example.com/old-page

For a visual chain trace, use Redirect Tracer.

References

  1. Flask Documentation, "API - flask.redirect," https://flask.palletsprojects.com/en/latest/api/#flask.redirect
  2. Flask Documentation, "API - flask.url_for," https://flask.palletsprojects.com/en/latest/api/#flask.url_for
  3. IETF, "RFC 9110 - HTTP Semantics, Section 15.4: Redirection 3xx," June 2022. https://httpwg.org/specs/rfc9110.html#status.3xx
  4. OWASP, "Unvalidated Redirects and Forwards," https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html

Test your Flask redirects end to end

Trace every hop in your redirect chain, verify status codes, and make sure your users land where you intended.

Try Redirect Tracer