Blinker signals are how Flask apps can talk to each other without knowing who’s listening.
Let’s see this in action. Imagine a Flask app that handles user signups. When a new user signs up, we want to do a few things: send a welcome email, update a user count in a separate service, and maybe log the event.
from flask import Flask
from blinker import Namespace
app = Flask(__name__)
my_signals = Namespace()
# Define signals
user_signed_up = my_signals.signal('user-signed-up')
@app.route('/signup', methods=['POST'])
def signup():
username = request.form['username']
# ... actual signup logic ...
user_id = create_user(username) # Assume this creates the user and returns their ID
# Send the signal
user_signed_up.send(app, user_id=user_id, username=username)
return jsonify({"message": "User created successfully!"}), 201
def send_welcome_email(user_id, username):
print(f"Sending welcome email to {username} (ID: {user_id})")
# ... actual email sending logic ...
def update_user_count(user_id, username):
print(f"Updating user count for {username} (ID: {user_id})")
# ... logic to increment a counter in another service ...
def log_signup_event(user_id, username):
print(f"Logging signup event for {username} (ID: {user_id})")
# ... logging logic ...
# Connect receivers to the signal
user_signed_up.connect(send_welcome_email)
user_signed_up.connect(update_user_count)
user_signed_up.connect(log_signup_event)
if __name__ == '__main__':
app.run(debug=True)
When a POST request hits /signup, user_signed_up.send(app, user_id=user_id, username=username) fires. This broadcasts the user_signed_up signal. app is the sender, and user_id and username are keyword arguments passed along. Blinker then finds all functions that have been connected to this specific signal and calls them with the sender and the keyword arguments. So, send_welcome_email, update_user_count, and log_signup_event all get executed, but the /signup route doesn’t need to know they exist.
This pattern solves the problem of tightly coupled code. In a traditional setup, the signup route might directly call send_welcome_email(), update_user_count(), and log_signup_event(). If you wanted to add a new action, like updating a CRM, you’d have to modify the signup route itself. With Blinker, you just connect a new receiver function to the user_signed_up signal. The signup route remains untouched. This makes your application more modular, easier to test, and far more extensible. You can easily add new features or modify existing ones without cascading changes.
The Namespace object is crucial for organizing your signals. If you have a large application, you might define different namespaces for different parts of your app (e.g., auth_signals = Namespace(), payment_signals = Namespace()). This prevents signal name collisions and keeps your signal definitions tidy. When you connect a receiver, you’re connecting it to a specific signal instance, like user_signed_up. This signal instance is bound to the namespace it was created in.
The sender argument in signal.send(sender, **kwargs) can be anything, but it’s conventional to use the object that initiated the action, often the Flask application instance itself. The keyword arguments (**kwargs) are how you pass data to your receivers. They are unpacked and passed as keyword arguments to the connected receiver functions. This is flexible because receivers can choose to accept only the arguments they need. For instance, log_signup_event might only care about username, while send_welcome_email needs both user_id and username.
A common misconception is that signals are only for different modules or services. You can absolutely use signals to decouple logic within the same module. For example, if your signup route has multiple distinct post-signup tasks that could grow independently, using signals keeps the route function cleaner and focused solely on the primary task of user creation and signal broadcasting.
When you disconnect a receiver, you use signal.disconnect(receiver_function). This is useful during testing or when a background task is completed and no longer needs to listen for a particular event.
The next hurdle you’ll likely encounter is managing more complex signal workflows, such as ensuring signals are processed in a specific order or handling potential errors within receiver functions without crashing the sender.