Skip to main content

Flask Signals

Introduction

Flask signals provide a way for your application to be notified when certain actions occur. Signals are based on the observer pattern, allowing certain senders to notify subscribers when specific events happen. This creates a powerful system for decoupling components in your Flask application, making your code more maintainable and extensible.

In this tutorial, we'll explore Flask signals, understand how they work, and see practical examples of how they can improve your Flask applications.

What are Flask Signals?

Signals are a way of sending and receiving notifications when certain events occur in your application. They work like this:

  1. A sender triggers a signal when something happens
  2. Receivers (or subscribers) that have registered interest in that signal receive a notification
  3. The receivers can then take action based on that notification

Flask signals are implemented using the Blinker library, which must be installed to use signals effectively.

Setting Up Blinker

Before we start using signals, we need to install the Blinker library:

bash
pip install blinker

Flask will use Blinker automatically if it's available. If Blinker is not installed, Flask signals will be non-functional, but your application will still work (signals will simply not be sent).

Built-in Flask Signals

Flask comes with several built-in signals that notify you about events within the framework. Here are some of the most commonly used signals:

SignalDescription
request_startedSent when Flask starts processing a request
request_finishedSent when Flask finishes processing a request
request_tearing_downSent when the request context is tearing down
template_renderedSent when a template was rendered successfully
got_request_exceptionSent when an exception occurs during request handling
appcontext_tearing_downSent when the application context is tearing down
appcontext_pushedSent when an application context is pushed
appcontext_poppedSent when an application context is popped

Basic Signal Usage

Let's look at how to use signals in a Flask application:

python
from flask import Flask, request, g
from flask.signals import request_started, request_finished

app = Flask(__name__)

# Connect a function to a signal
@request_started.connect
def log_request_started(sender, **extra):
print(f"Request started: {request.endpoint}")

@request_finished.connect
def log_request_finished(sender, response, **extra):
print(f"Request finished: {request.endpoint} with status {response.status_code}")

@app.route('/')
def index():
return "Hello, Signals!"

@app.route('/error')
def error():
raise Exception("Deliberate error")

if __name__ == '__main__':
app.run(debug=True)

When you run this application and make requests to the routes, you'll see log messages showing when requests start and finish.

Creating Custom Signals

You can create your own signals to notify parts of your application about custom events:

python
from flask import Flask
from blinker import signal

app = Flask(__name__)

# Create a custom signal
user_registered = signal('user-registered')

@app.route('/register')
def register():
# ... registration logic ...

# Send our custom signal
user_registered.send(app, user=new_user)

return "User registered successfully!"

# Connect to our custom signal
@user_registered.connect
def send_welcome_email(sender, user, **extra):
print(f"Sending welcome email to {user.email}")
# Logic to send an email would go here

Connect Decorators vs. Direct Connection

There are two ways to connect to signals:

Using a Decorator

python
@request_started.connect
def log_request_info(sender, **extra):
print("Request started")

Direct Connection

python
def log_request_info(sender, **extra):
print("Request started")

request_started.connect(log_request_info)

Both methods are equivalent. Choose the one that fits your coding style.

Connecting to Specific Senders

Sometimes you only want to receive signals from specific senders:

python
from flask import Flask, render_template
from flask.signals import template_rendered

app = Flask(__name__)

def log_template_renders(sender, template, context, **extra):
print(f"Rendering template {template.name} with {context}")

# Only listen for template_rendered signals from our app specifically
template_rendered.connect(log_template_renders, app)

@app.route('/')
def index():
return render_template('index.html', title="Signal Example")

Practical Example: Activity Logging

Let's implement a more useful example - logging user activity throughout the application:

python
from flask import Flask, request, g, session
from flask.signals import request_started, request_finished
import time
from datetime import datetime

app = Flask(__name__)
app.secret_key = 'your-secret-key'

# Custom signal for user actions
user_action = signal('user-action')

class ActivityLogger:
def __init__(self):
self.setup_signal_handlers()

def setup_signal_handlers(self):
request_started.connect(self.on_request_started)
request_finished.connect(self.on_request_finished)
user_action.connect(self.on_user_action)

def on_request_started(self, sender, **extra):
g.start_time = time.time()

def on_request_finished(self, sender, response, **extra):
if hasattr(g, 'start_time'):
duration = time.time() - g.start_time
user_id = session.get('user_id', 'anonymous')
path = request.path
method = request.method
status = response.status_code

print(f"[{datetime.now()}] User {user_id} made {method} request to {path}, "
f"returned {status} in {duration:.2f}s")

def on_user_action(self, sender, action, **extra):
user_id = session.get('user_id', 'anonymous')
print(f"[{datetime.now()}] User {user_id} performed action: {action}")
# In a real app, you might save this to a database


# Initialize our logger
logger = ActivityLogger()

@app.route('/')
def index():
return "Welcome to the homepage!"

@app.route('/login', methods=['POST'])
def login():
# Simulate a login
session['user_id'] = request.form.get('username', 'demo_user')

# Send a custom signal for this important action
user_action.send(app, action='login', username=session['user_id'])

return "Logged in successfully!"

@app.route('/profile')
def profile():
if 'user_id' not in session:
return "Please log in first", 401

# Signal that the user viewed their profile
user_action.send(app, action='view_profile')

return f"Profile page for {session['user_id']}"

if __name__ == '__main__':
app.run(debug=True)

Signal Namespace

If you're building a Flask extension or a large application, you might want to create a namespace for your signals:

python
from blinker import Namespace

# Create a namespace for your signals
my_signals = Namespace()

# Create signals within this namespace
user_created = my_signals.signal('user-created')
user_deleted = my_signals.signal('user-deleted')

Testing with Signals

When testing code that uses signals, you might want to capture signal emissions:

python
from flask import Flask
from blinker import signal
import unittest

app = Flask(__name__)
user_created = signal('user-created')

@app.route('/create_user')
def create_user():
# ... create user ...
user_created.send(app, username='testuser')
return 'User created'

class TestSignals(unittest.TestCase):
def test_user_created_signal(self):
# Create a receiver that will record if the signal was sent
received = []

def receiver(sender, username, **extra):
received.append(username)

# Connect our test receiver
user_created.connect(receiver)

# Make a test client request
client = app.test_client()
response = client.get('/create_user')

# Check if our signal was received
self.assertEqual(received, ['testuser'])

# Clean up by disconnecting
user_created.disconnect(receiver)

Common Use Cases for Signals

Here are some practical scenarios where Flask signals are particularly useful:

  1. Logging and monitoring: Track specific events without modifying core application logic
  2. Authentication events: React to logins, logouts, and authentication failures
  3. Cache invalidation: Clear caches when models are updated
  4. Email notifications: Send emails in response to certain events
  5. Analytics: Record user actions for later analysis

Signal Handling Best Practices

When working with signals, here are some best practices to follow:

  1. Keep handlers lightweight: Signal handlers should execute quickly and not block the main application flow
  2. Handle exceptions: Wrap your signal handlers in try-except blocks to prevent exceptions from affecting the main application
  3. Document your signals: If you create custom signals, document them clearly for other developers
  4. Consider asynchronous handling: For time-consuming operations, consider using a task queue from signal handlers
  5. Don't overuse signals: Signals are great for decoupling, but direct function calls are simpler when appropriate

Summary

Flask signals provide an elegant way to decouple components of your application by implementing the observer pattern. They allow certain senders to notify subscribers when specific events happen.

In this tutorial, you learned:

  • What Flask signals are and how they work
  • How to use built-in Flask signals
  • How to create and use custom signals
  • Practical examples of signal usage
  • Best practices for working with signals

With signals, you can build more maintainable and extensible Flask applications by separating concerns and reducing tight coupling between components.

Further Resources

Exercises

  1. Create a Flask application that uses the template_rendered signal to log every template that gets rendered along with the context data.
  2. Implement a custom signal that fires whenever a user changes their profile information, and use it to update a "last modified" timestamp.
  3. Create a signal-based audit system that records all data modifications in your application.
  4. Build a simple analytics system using signals to track page views and user interactions.
  5. Extend the activity logging example to save logs to a database instead of printing them.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)