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:
- A sender triggers a signal when something happens
- Receivers (or subscribers) that have registered interest in that signal receive a notification
- 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:
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:
Signal | Description |
---|---|
request_started | Sent when Flask starts processing a request |
request_finished | Sent when Flask finishes processing a request |
request_tearing_down | Sent when the request context is tearing down |
template_rendered | Sent when a template was rendered successfully |
got_request_exception | Sent when an exception occurs during request handling |
appcontext_tearing_down | Sent when the application context is tearing down |
appcontext_pushed | Sent when an application context is pushed |
appcontext_popped | Sent when an application context is popped |
Basic Signal Usage
Let's look at how to use signals in a Flask application:
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:
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
@request_started.connect
def log_request_info(sender, **extra):
print("Request started")
Direct Connection
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:
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:
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:
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:
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:
- Logging and monitoring: Track specific events without modifying core application logic
- Authentication events: React to logins, logouts, and authentication failures
- Cache invalidation: Clear caches when models are updated
- Email notifications: Send emails in response to certain events
- Analytics: Record user actions for later analysis
Signal Handling Best Practices
When working with signals, here are some best practices to follow:
- Keep handlers lightweight: Signal handlers should execute quickly and not block the main application flow
- Handle exceptions: Wrap your signal handlers in try-except blocks to prevent exceptions from affecting the main application
- Document your signals: If you create custom signals, document them clearly for other developers
- Consider asynchronous handling: For time-consuming operations, consider using a task queue from signal handlers
- 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
- Create a Flask application that uses the
template_rendered
signal to log every template that gets rendered along with the context data. - Implement a custom signal that fires whenever a user changes their profile information, and use it to update a "last modified" timestamp.
- Create a signal-based audit system that records all data modifications in your application.
- Build a simple analytics system using signals to track page views and user interactions.
- 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! :)