Patrick's Software Blog

Flask Tip 1 - Basic Flask Application

In a new folder, create the app.py file:

from flask import Flask

# Create the Flask application
app = Flask(__name__)

@app.route('/')
def index():
    return '<h1>Welcome to the Flask App!</h1>'

In the terminal, run:

$ flask --app app --debug run

Navigate to http://127.0.0.1:5000/ to view the website!

Flask Tip 2 - Flask Development Server

The Flask Development Server is bundled with Flask to allow for easy testing during the development phase of a project when running on a local computer:

Flask Development Server

The Flask Development Server is run when executing flask run.

Flask Tip 3 - Flask Shell

Prototyping with the Python interpreter is really beneficial with a Flask application too!

Start the Python interpreter with the Flask application loaded to prototype with the Flask application:

$ flask shell
>>> print(app.url_map)
>>> print(app.blueprints)

Flask Tip 4 - Adding Automatic Imports to the Flask Shell

Additional automatic imports can be added to the Flask shell using shell_context_processor():

# ... After creating the Flask application (`app`) ...
@app.shell_context_processor
def shell_context():
    return {'database': database}

The database object is now automatically loaded in the Flask shell:

$ flask shell
>>> print(database)

Flask Tip 5 - Flask Routes Command

To easily see all the routes defined in a Flask application:

$ flask routes
Endpoint  Methods  Rule
--------  -------  -----------------------
index     GET      /
static    GET      /static/<path:filename>

Flask Tip 6 - Static Files

Flask automatically creates a static endpoint to serve static files (HTML, CSS, JS, Images).

To serve an image, copy the image into the static folder of the Flask project. Create a new route:

from flask import current_app

@app.route('/logo')
def flask_logo():
    return current_app.send_static_file('flask-logo.png')

The image is now viewable at 'http://127.0.0.1:5000/logo'!

Flask Tip 7 - CLI Commands (Part I)

The flask command is written using Click. Click can be used to create sub-commands for the flask command.

Create a CLI command for initializing the database:

from click import echo

@app.cli.command('init_db')
def initialize_database():
    """Initialize the SQLite database."""
    database.drop_all()
    database.create_all()
    click.echo('Initialized the SQLite database!')

Run this custom CLI command:

$ flask init_db

The Flask-Migrate package is a great example of using custom CLI commands.

Flask Tip 8 - CLI Commands (Part II)

Custom CLI commands in Flask are automatically included in the help information. After defining the init_db command:

$ flask --help
...
Commands:
  init_db  Initialize the SQLite database.
  run      Run a development server.
...

Flask Tip 9 - CLI Commands (Part III)

Custom CLI commands in Flask can be added to specific blueprints, not just to the Flask application (app):

from flask import Blueprint
import click

users_blueprint = Blueprint('users', __name__)

@users_blueprint.cli.command('init_db')
@click.argument('email')
def create(email):
    """Create a new user."""
    ...
    click.echo(f'Added new user (email: {email}!')

Blueprint CLI commands are sub-groups (users):

$ flask --help
...
Commands:
  init_db  Initialize the SQLite database.
  run      Run a development server.
  users
$ flask users --help
...
Commands:
  create  Create a new user.

Flask Tip 10 - HTTP Methods

In Flask, the route decorator accepts an optional second argument: a list of allowable HTTP methods (GET, POST, PUT, DELETE):

from flask import request, render_template

@app.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        # Validate request

    return render_template('register.html')

Flask Tip 11 - Variable Routing (Part I)

Flask supports variables in a URL when using the route decorator:

@users_blueprint.route('/<id>')
def get_user(id):
    return f'<h2>Data for user #{id}</h2>'

Variable routing is critical when defining separate functions for each unique URL would become unreasonable, such as accessing user profiles!

Flask Tip 12 - Variable Routing (Part II)

Flask supports the following types for variable routes:

Type Description Examples
string (Default) Accepts any text without slashes About page
int Accepts positive integers Blog Posts page
path Similar to string, but also accepts slashes Search page
uuid Accepts UUID strings Database Access page

Example:

app.route(/blog/<int:index)
def get_blog_post(index):
    # Retrieve blog post

Flask Tip 13 - Single HTTP Methods

If you only want to allow a single HTTP method for a route in Flask, the route decorator can be replaced with a shorthand version:

@app.get('/stocks')
def stocks():
    return render_template('stocks.html')

@app.post('/add_stock')
def add_stock():
    # process request

The common HTTP methods are supported: get(), post(), put(), patch(), delete().

Flask Tip 14 - HTTP Return Codes

Flask allows an HTTP return code to be specified from a view function:

@users_api_blueprint.route('/register', methods=['POST'])
def register():
    # Return 201 (Created) to indicate the new user was created
    return '<h2>New User registered!</h2>', 201

Flask Tip 15 - abort()

The abort() function in Flask raises an HTTP exception for the given status code.

Helpful for exiting a view function when an error is detected:

@journal_api_blueprint.route('/<int:index>', methods=['GET'])
def get_journal_entry(index):
    ...

    # Check that the journal entry is associated
    # with the current user
    if entry.user_id != user.id:
        abort(403)
    return entry

Flask Tip 16 - Custom Error Pages

Flask allows custom error pages for specific status codes, which can provide a better user experience by allowing users to easily navigate back through your application:

@app.errorhandler(404)
def page_not_found(e):
    return render_template('404.html'), 404

Flask Tip 17 - Application Factory Function (Part I)

An application factory function should be used to create the Flask application object (app).

Benefit: create different versions of the Flask app using the same interface (the application factory).

# app.py
from project import create_app

# Call the application factory function to construct a Flask application instance
app = create_app()

Flask Tip 18 - Application Factory Function (Part II)

The application factory function for a Flask application initializes the Flask application:

# ----------------------------
# Application Factory Function
# ----------------------------

def create_app():
    # Create the Flask application
    app = Flask(__name__)

    initialize_extensions(app)
    register_blueprints(app)
    configure_logging(app)
    register_app_callbacks(app)
    register_error_pages(app)
    register_cli_commands(app)
    return app

Flask Tip 19 - Request Object (Part I)

In Flask, the request object provides information about the request being processed in the view functions.

For example, the request object provides the HTTP method used:

from flask import request

@users_blueprint.route('/register', methods=['GET', 'POST'])
def register():
    if request.method == 'POST':
        # Validate request
        return '<p>New user registered!</p>'

    return '<h2>Registration Form</h2>'

Flask Tip 20 - Request Object (Part II)

In Flask, the request object can be used to log the IP address of the sender using request.remote_addr:

@users_blueprint.route('/login')
def login():
    if request.method == 'POST':
        # Log in the user
        current_app.logger.info(f'New login request from from IP address: {request.remote_addr}')
        return '<p>User logged in!</p>'

    return '<h2>Login Form</h2>'

Flask Tip 21 - Request Object (Part III)

In Flask, the request object contains any form data submitted, which can be processed in a view function:

from flask import request

@journal_blueprint.route('/<int:index>', methods=['PUT'])
def update_journal_entry(index):
    if request.method == 'PUT':
        # Update the journal entry in the database
        ...
        entry.update(request.form['entry_title'],
                     request.form['entry_text'])

Flask Tip 22 - Request Object (Part IV)

In Flask, the request object can be used to check that a request was made using a secure protocol via request.is_secure: * HTTPS - HTTP Secure * WSS - WebSockets over SSL/TLS

from flask import request, current_app

@journal_blueprint.route('/journal', methods=['GET'])
def get_journal():
    # Only support secure protocols (HTTPS or WSS)
    if request.is_secure:
        current_app.logger.info(f'Journal request using protocol: {request.scheme}')
        return '<p>Journal Entries</p>'

Flask Tip 23 - Request Object (Part V)

In Flask, the request object is used for uploading files via request.files.

For a detailed file upload example, refer to the Flask docs on uploading files: https://flask.palletsprojects.com/en/2.1.x/patterns/fileuploads/

from flask import request, current_app
from werkzeug.utils import secure_filename

import os

@journal_blueprint.route('/upload_file', methods=['POST'])
def upload_file():
    if 'file' in request.files:
        file = request.files['file']
        filename = secure_filename(file.filename)
        file.save(os.path.join(current_app.config['UPLOAD_FOLDER'], filename))
        return '<p>File uploaded!</p>'

Flask Tip 24 - Request Object (Part VI)

In Flask, the request object stores any parsed URL parameters (text after the question mark) in request.args:

  • http://127.0.0.1:5000/users/login?next=%2Fusers%2Fprofile

Always be careful when parsing user inputs to avoid URls like this:

  • http://127.0.0.1:5000/users/login?next=http://www.givemeyourcreditcard.com/
from flask import request, current_app, abort
from urllib.parse import urlparse

@users_blueprint.route('/login')
def login():
    ...

    # Redirect the user to the specified URL after login
    if 'next' in request.args:
        next_url = request.args.get('next')

        # Only accept relative URLs
        if urlparse(next_url).scheme != '' or urlparse(next_url).netloc != '':
            current_app.logger.info(f'Invalid next path in login request: {next_url}')
            return abort(400)

        current_app.logger.info(f'Redirecting after valid login to: {next_url}')
        return '<p>User logged in!</p>'

Flask Tip 25 - Jinja Templates (Part I)

The Jinja templating engine is one of the key building blocks of a Flask application.

A template file (containing variables and logic) is rendered into an output file (typically HTML).

Example:

from flask import render_template

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

Flask Tip 26 - Jinja Templates (Part II)

What's the advantage of using Jinja templates in a Flask application?

  1. Use static HTML template files to decouple the routes from the HTML
  2. Separate the HTML structure from the content
  3. Use basic programming constructs -- variables, conditionals, and for loops -- directly in the HTML to manipulate the final rendered output

Flask Tip 27 - Jinja Templates (Part III)

In Flask, variables can be passed as arguments to render_template() to use those variables in the template file:

# app.py
@app.route('/about')
def about():
    return render_template('about.html', organization='TestDriven.io')

# about.html
<h2>Course developed by {{ organization }}.</h2>

Flask Tip 28 - Jinja Templates (Part IV)

Jinja templates can contain logic (IF-ELSE, FOR loops) to control the output:

{% if organization %}
  <h2>Course developed by {{ company_name }}.</h2>
{% else %}
  <h2>Enjoy the course!</h2>
{% endif %}

<ul>
{% for user in users %}
  <li>{{ user.email }}</li>
{% endfor %}
</ul>

Flask Tip 29 - Jinja Templates (Part V)

Flask makes "global" variables and key functions available in Jinja templates:

  • request
  • session
  • g
  • url_for()
  • get_flashed_messages()

Example:

{% if session['email'] %}
    <h1>Welcome {{ session['email'] }}!</h1>
{% else %}
    <h1>Welcome! Please <a href="{{ url_for('login') }}">log in.</a></h1>
{% endif %}

Flask Tip 30 - Jinja Templates (Part VI)

If using Flask-Login, it makes current_user available in the Jinja templates:

{% if current_user.is_authenticated %}
  <li><a href="{{ url_for('stocks.list_stocks') }}">Portfolio</a></li>
{% else %}
  <li><a href="{{ url_for('users.register') }}">Register</a></li>
  <li><a href="{{ url_for('users.login') }}">Login</a></li>
{% endif %}

Flask Tip 31 - Jinja Templates (Part VII)

Template inheritance is an amazing feature in Jinja (and Flask)!

It allows a base template to define a structure and then child templates to define the details.

Template inheritance is similar to classes in object-oriented design.

<!-- base.html -->
<body>
  <main>
    <!-- child template -->
    {% block content %}
    {% endblock %}
  </main>
</body>

<!-- index.html -->
{% extends 'base.html' %}

{% block content %}
<h1>Welcome to the Flask App!</h1>
{% endblock %}

Flask Tip 32 - Jinja Templates (Part VIII)

In Flask, template inheritance can be used to add custom CSS files to provide unique styling to a page:

<!-- base.html -->
<head>
  <!-- Additional Styling -->
  {% block styling %}
  {% endblock %}
</head>

<!-- child.html -->
{% extends 'base.html' %}

{% block styling %}
<link rel="stylesheet" href="{{ url_for('static', filename='css/user_profile_style.css') }}">
{% endblock %}

Flask Tip 33 - Jinja Templates (Part IX)

In Flask, Python methods for a variable's type can be called in a Jinja template.

For example, a datetime object can be displayed in a more readable string format using strftime():

<p>Joined on {{ current_user.registered_on.strftime('%A, %B %d, %Y') }}</p>

Flask Tip 34 - Flask Components

Flask is comprised of 5 key components: * click: package for creating command-line interfaces (CLI) * itsdangerous: cryptographically sign data * Jinja2: templating engine * MarkupSafe: escapes characters so text is safe to use in HTML and XML * Werkzeug: set of utilities for creating a Python application that can talk to a WSGI server

(venv)$ pip install Flask
(venv)$ pip freeze
click==8.1.3
Flask==2.2.3
itsdangerous==2.1.2
Jinja2==3.1.2
MarkupSafe==2.1.2
Werkzeug==2.2.3

Flask Tip 35 - Werkzeug (Introduction)

Werkzeug (key component of Flask) provides a set of utilities for creating a Python application that can talk to a WSGI server, like Gunicorn.

Werkzeug provides following functionality (which Flask uses): * Request processing * Response handling * URL routing * Middleware * HTTP utilities * Exception handling

For more details on Werkzeug: What is Werkzeug?

Flask Tip 36 - Werkzeug (Development Server)

The Flask Development Server should really be called the Werkzeug Development Server, as Werkzeug provides the development server with auto re-load capability and a built-in debugger.

From the Flask source code:

from werkzeug.serving import run_simple

run_simple(host, port, app, use_reloader=reload, use_debugger=debugger, ...)

Flask Tip 37 - Werkzeug (Simple WSGI Application)

Werkzeug provides a collection of libraries to build a WSGI-compatible application:

from werkzeug.wrappers import Request, Response


class HelloWorldApp(object):
    """Implements a WSGI application."""
    def __init__(self):
        pass

    def dispatch_request(self, request):
        """Dispatches the request."""
        return Response('Hello World!')

    def wsgi_app(self, environ, start_response):
        """WSGI application that processes requests and returns responses."""
        request = Request(environ)
        response = self.dispatch_request(request)
        return response(environ, start_response)

    def __call__(self, environ, start_response):
        """The WSGI server calls this method as the WSGI application."""
        return self.wsgi_app(environ, start_response)


def create_app():
    """Application factory function"""
    app = HelloWorldApp()
    return app


if __name__ == '__main__':
    # Run the Werkzeug development server to serve the WSGI application (HelloWorldApp)
    from werkzeug.serving import run_simple
    app = create_app()
    run_simple('127.0.0.1', 5000, app, use_debugger=True, use_reloader=True)

Flask Tip 38 - Werkzeug (Utilities)

Werkzueg (key component of Flask) provides a library for hashing passwords:

from werkzeug.security import generate_password_hash, check_password_hash

class User(database.Model):
    ...
    def is_password_correct(self, password_plaintext: str):
        return check_password_hash(self.password_hashed, password_plaintext)

    def set_password(self, password_plaintext: str):
        self.password_hashed = generate_password_hash(password_plaintext)

Flask Tip 39 - Werkzeug (Context Locals)

Werkzueg (key component of Flask) provides a library for local data storage (context locals) in "werkzeug.local".

Context locals expands on thread-local data in Python to work with threads, processes, or coroutines.

Each context accesses the data in a context-safe manner and the data is always unique to the specific context.

Example:

from werkzeug.local import LocalStack
import random
import threading
import time

# Create a global LocalStack object for storing data about each thread
thread_data_stack = LocalStack()


def long_running_function(thread_index: int):
    """Simulates a long-running function by using time.sleep()."""

    thread_data_stack.push({'index': thread_index, 'thread_id': threading.get_native_id()})
    print(f'Starting thread #{thread_index}... {thread_data_stack}')

    time.sleep(random.randrange(1, 11))

    print(f'LocalStack contains: {thread_data_stack.top}')
    print(f'Finished thread #{thread_index}!')
    thread_data_stack.pop()


if __name__ == "__main__":
    threads = []

    # Create and start 3 threads that each run long_running_function()
    for index in range(3):
        thread = threading.Thread(target=long_running_function, args=(index,))
        threads.append(thread)
        thread.start()

    # Wait until each thread terminates before the script exits by 'join'ing each thread
    for thread in threads:
        thread.join()

    print('Done!')

More details: Deep Dive into Flask's Application and Request Contexts: Context-Locals

Flask Tip 40 - Jinja Templating Engine

While the Jinja templating engine is integrated with Flask, it can also be used without Flask.

For example, it can be used to generate static HTML pages (such as for a blog):

from jinja2 import Environment, PackageLoader, select_autoescape


# Create a template environment that loads template files from:
#    ./blog/templates/
#
# NOTE: auto-escaping and template inheritance are automatically
#       when using a template loader (PackageLoader).
env = Environment(
    loader=PackageLoader("blog"),
    autoescape=select_autoescape()
)

# Load a template file and write the rendered HTML
template = env.get_template("index.html")
output = template.render()

with open("index.html", "w") as html_output:
    html_output.write(output)

Flask Tip 41 - Frozen-Flask

Did you know that Flask can be used to generate a static site (blog, portfolio, etc.) using Frozen-Flask?

Frozen-Flask "freezes" a Flask application into a set of static files that can be easily deployed, such as with Netlify.

Frozen-Flask simulates the requests to all possible URLs in the Flask application and writes the responses to corresponding HTML files.

For more details on using Frozen-Flask: Generating a Static Site with Flask and Deploying it to Netlify

Flask Tip 42 - Sessions (Part I)

How are sessions implemented in Flask?

In order to store data across multiple requests, Flask utilizes cryptographically-signed cookies (stored on the web browser) to store the data for a session. This cookie is sent with each request to the Flask app on the server-side where it's decoded.

Since session data is stored in cookies that are cryptographically-signed (not encrypted!), sessions should NOT be used for storing any sensitive information. You should never include passwords or personal information in session data.

For more details on using Sessions in Flask: Sessions in Flask

Flask Tip 43 - Sessions (Part II)

Sessions in Flask can be considered "client-side", as sessions are stored client-side in browser cookies.

Pros: * Validating and creating sessions is fast (no data storage) * Easy to scale (no need to replicate session data across web servers)

Cons: * Sensitive data cannot be stored in session data, as it's stored on the web browser * Session data is limited by the size of the cookie (usually 4 KB) * Sessions cannot be immediately revoked by the Flask app

Flask Tip 44 - Sessions (Part III)

In Flask, you can store information specific to a user for the duration of a session using the session object.

Saving data for use throughout a session allows the Flask app to keep data persistent over multiple requests.

Example:

from flask import request, session

@app.route('/set_email', methods=['GET', 'POST'])
def set_email():
    if request.method == 'POST':
        # Save the form data to the session object
        session['email'] = request.form['email_address']
        return redirect(url_for('get_email'))

    return """
        <form method="post">
            <label for="email">Enter your email address:</label>
            <input type="email" id="email" name="email_address" required />
            <button type="submit">Submit</button
        </form>
        """

Flask Tip 45 - Sessions (Part IV)

In Flask, the session object can be read (in the same manner as a dictionary) to retrieve data unique to the session.

The session object is conveniently available in Jinja templates:

from flask import render_template_string

@app.route('/get_email')
def get_email():
    return render_template_string("""
            {% if session['email'] %}
                <h1>Welcome {{ session['email'] }}!</h1>
            {% else %}
                <h1>Welcome! Please enter your email <a href="{{ url_for('set_email') }}">here.</a></h1>
            {% endif %}
        """)

Flask Tip 46 - Sessions (Part V)

In Flask, data stored in the session object can be deleted by popping a specific element from the session object:

from flask import session

@app.route('/delete_email')
def delete_email():
    # Clear the email stored in the session object
    session.pop('email', default=None)
    return '<h1>Session deleted!</h1>'

Flask Tip 47 - Sessions (Part VI)

In Flask, the session object is implemented as a Werkzeug.CallbackDict object, which expands on a Python dict to include tracking when elements are modified.

Therefore, the session object won't automatically detect changes to mutable data types (list, dictionary, set, etc.).

Example:

session['shopping_cart'] = []
...
# Since a mutable data type (list) is being modified, this change
# is not automatically detected by the session object
session['shopping_cart'].append('bike')

# Therefore, mark the session object as modified
session.modified = True

Flask Tip 48 - Sessions (Part VII)

By default, the session object in Flask remains in place until the browser is closed.

However, if you want to change the life of the session object, define the PERMANENT_SESSION_LIFETIME configuration variable after creating the Flask app.

When setting the data in the session, specify that the sessions should be permanent (time will be based on PERMANENT_SESSION_LIFETIME):

# config.py
import datetime
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(minutes=1)

# app.py
# Save the form data to the session object
session['email'] = request.form['email_address']
session.permanent = True

Flask Tip 49 - Server-Side Sessions (Introduction)

Server-side sessions store the data associated with the session on the server in a particular data storage solution.

Pros: - Sensitive data is stored on the server, not in the web browser - Store as much session data as you want without worrying about the cookie size - Sessions can easily be terminated by the Flask app

Cons: - Difficult to set up and scale - Increased complexity since session state must be managed

Flask Tip 50 - Server-Side Sessions (Part I)

How do server-side sessions work with the Flask-Session extension?

Flask-Session uses Flask's Session Interface, which provides a simple way to replace Flask's built-in session implementation.

You can continue to use the session object as you normally would with the built-in client-side session implementation.

More details: https://testdriven.io/blog/flask-server-side-sessions/

Flask Tip 51 - Server-Side Sessions (Part II)

Flask-Session works great with a Redis database!

After configuring the interface to Redis, the session object can be used (but data is stored on the server!):

import redis
from flask import Flask, session, render_template_string
from flask_session import Session


# Create the Flask application
app = Flask(__name__)

# Configure Redis for storing the session data on the server-side
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_PERMANENT'] = False
app.config['SESSION_USE_SIGNER'] = True
app.config['SESSION_REDIS'] = redis.from_url('redis://localhost:6379')

# Create and initialize the Flask-Session object AFTER `app` has been configured
server_session = Session(app)

@app.route('/get_email')
def get_email():
    return render_template_string("""<h1>Welcome {{ session['email'] }}!</h1>""")

Full example: https://gitlab.com/patkennedy79/flask-server-side-sessions

Flask Tip 52 - url_for() (Part I)

In Flask, the url_for() function builds the URL to a specific function.

url_for() is really useful in templates to easily include URLs:

<header class="site-header">
  <a href="{{ url_for('stocks.index') }}">Flask App</a>
  <nav>
    <ul>
      <li><a href="{{ url_for('users.register') }}">Register</a></li>
      <li><a href="{{ url_for('users.login') }}">Login</a></li>
    </ul>
  </nav>
</header>

Flask Tip 53 - url_for() (Part II)

In Flask, the url_for() function can be passed an argument to specify the variable part of a URL:

# Flask View Function with Variable Routing (Python)
@stocks_blueprint.route('/stocks/<id>')
def stock_details(id):
    stock = Stock.query.filter_by(id=id).first_or_404()
    return render_template('stocks/stock_details.html', stock=stock)


# Jinja Template (HTML)
  <ul>
    {% for stock in stocks %}
      <li><a href="{{ url_for('stocks.stock_details', id=stock.id) }}">{{ stock.stock_symbol }}</a></li>
    {% endfor %}
  </ul>

Flask Tip 54 - redirect() (Part I)

In Flask, the redirect() function is used to redirect a user to a different URL.

redirect() can greatly improve the navigation through a site by automatically redirecting users to the expected pages:

@app.route('/add_stock', methods=['GET', 'POST'])
def add_stock():
    if request.method == 'POST':
        # ... save the data ...

        return redirect(url_for('list_stocks'))  # <-- !!

    return render_template('add_stock.html')

Flask Tip 55 - redirect() (Part II)

How does redirect() work in Flask?

Check the log messages from the Flask development server when adding data:

127.0.0.1 - - [08/Jan/2022 17:09:07] "POST /add_stock HTTP/1.1" 302 -
127.0.0.1 - - [08/Jan/2022 17:09:07] "GET /stocks/ HTTP/1.1" 200 -

The status code of 302 (Found) is used to redirect the user to a new URL that is specified in the header field of the response.

Flask Tip 56 - Message Flashing (Part I)

Flash messages are used to provide useful information to the user based on their actions with the app.

In Flask, the flash() function is used to create a flash message to be displayed in the next request (when the list of stocks is displayed):

from flask import request, redirect, url_for, render_template, flash

@stocks_blueprint.route('/add_stock', methods=['GET', 'POST'])
def add_stock():
    if request.method == 'POST':
        # ... save the data ...

        flash(f"Added new stock ({stock_data.stock_symbol})!")  # <-- !!
        return redirect(url_for('stocks.list_stocks'))

    return render_template('stocks/add_stock.html')

Flask Tip 57 - Message Flashing (Part II)

In Flask, the get_flashed_messages() function is used to retrieve all the flash messages (from the session).

get_flashed_messages() is available in the Jinja templates:

<!-- flash messages -->
{% for message in get_flashed_messages() %}
  <p>{{ message }}</p>
{% endfor %}

Flask Tip 58 - Message Flashing (Part III)

In Flask, flash messages can be categorized to allow different styling (success, error, info) using get_flashed_messages(with_categories=true):

# Flask Route (Python)
@stocks_blueprint.route('/stocks/<id>/delete')
def delete_stock(id):
    # ... delete the data ...
    flash(f'Stock ({stock.stock_symbol}) was deleted!', 'success')  # <-- !!
    return redirect(url_for('stocks.list_stocks'))

# Jinja Template (HTML)
<!-- flash messages - supported categories: success, info, error, message (default) -->
  {% with messages = get_flashed_messages(with_categories=true) %}
    {% if messages %}
      {% for category, message in messages %}
        <div class="flash-message flash-message-{{ category }}">
          <p>{{ message }}</p>
        </div>
      {% endfor %}
    {% endif %}
  {% endwith %}