Signals (Blinker)
Reviewed & published by Brayan K
Signals are a publish/subscribe mechanism: one part of your app broadcasts a named event and any number of connected receiver functions run in response, with neither knowing about the other.
Part of the free Flask course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
In this lesson you'll connect receivers to Flask's built-in signals, define your own with Blinker, and use signals to decouple side effects from your core logic.
What You'll Learn in This Lesson
- What publish/subscribe signals are
- Connecting receivers to request_started
- Listening to template_rendered
- Creating custom signals with Blinker
- Fanning one event out to many receivers
- Decoupling side effects from core logic
1 Flask's Built-In Signals
Flask emits signals at key moments — for example request_started fires at the start of every request. A receiver is a function you connect to a signal; Flask calls it whenever the signal is sent. The sender doesn't know who is listening, which keeps things loosely coupled.
The runnable example below — using real Flask signals — connects a receiver to request_started and records each request. Both a normal request and a 404 trigger it, because the signal fires before routing decides the outcome.
# Flask's built-in signals: request_started fires on every request
from flask import Flask, request, request_started
app = Flask(__name__)
events = []
# A receiver is connected to a signal; it runs when the signal is sent.
def on_request_started(sender, **extra):
events.append(f"request started: {request.path}")
request_started.connect(on_request_started, app)
@app.route("/hello")
def hello():
return "hi"
# Fire two requests; the receiver runs for both (even the 404):
with app.test_client() as c:
c.get("/hello")
c.get("/world") # 404, but request_started still fires
for line in events:
print(line)
# request started: /hello
# request started: /world
# app.run() would serve this forever, so it goes last and stays commented
# out — the test client above is how Flask itself exercises routes:
# if __name__ == "__main__":
# app.run(debug=True)
# ✅ Expected output:
# request started: /hello
# request started: /worldBoth requests are recorded — including the 404 — because request_started notifies you the moment a request begins, independent of how it ends.
2 Listening to template_rendered
Signals carry data. The template_rendered signal fires after Jinja renders a template and passes the template and the context (the variables it received). This is invaluable for debugging and testing — you can assert which template ran and what data it was given.
The runnable example connects a receiver that records the template name and the context keys whenever a page renders.
# template_rendered: a built-in signal Flask fires after a template renders
from flask import Flask, render_template_string, template_rendered
app = Flask(__name__)
rendered = []
def record(sender, template, context, **extra):
rendered.append((template.name or "<string>", sorted(context.keys())))
template_rendered.connect(record, app)
@app.route("/page")
def page():
return render_template_string("Hello {{ name }}", name="Ada")
with app.test_client() as c:
c.get("/page")
print("templates rendered:", len(rendered)) # templates rendered: 1
name, keys = rendered[0]
print("name:", name) # name: <string>
print("'name' in context:", "name" in keys) # 'name' in context: True
# ✅ Expected output:
# templates rendered: 1
# name: <string>
# 'name' in context: TrueThe receiver captured one render with name in its context — exactly the pattern Flask's own test helpers use to assert which template a view rendered.
3 Custom Signals & Decoupling
The real power is your own signals. With Blinker (shipped with Flask) you create a named signal, connect any number of receivers, and broadcast with send. This decouples side effects: your registration code just announces "a user registered" and walks away — auditing, email, and analytics each subscribe on their own.
The runnable example fires one user-registered signal and watches two independent receivers — an audit logger and a welcome-emailer — both react, without the core register_user function knowing they exist.
# A custom signal with blinker (the library Flask's signals use)
from blinker import signal
# Define a named signal that any part of the app can send or listen to
user_registered = signal("user-registered")
audit_log = []
emails_sent = []
# Two independent receivers subscribe to the same signal:
@user_registered.connect
def write_audit(sender, **kw):
audit_log.append(f"audit: {kw['username']} registered")
@user_registered.connect
def send_welcome(sender, **kw):
emails_sent.append(f"welcome email -> {kw['username']}")
def register_user(username):
# core logic stays simple; side effects are decoupled via the signal
user_registered.send("auth", username=username)
return username
register_user("ada")
register_user("bob")
print(audit_log) # ['audit: ada registered', 'audit: bob registered']
print(emails_sent) # ['welcome email -> ada', 'welcome email -> bob']
# ✅ Expected output:
# ['audit: ada registered', 'audit: bob registered']
# ['welcome email -> ada', 'welcome email -> bob']🔬 Signals Cheat Sheet
| You want to... | Use |
|---|---|
| Create a signal | signal("my-event") |
| Connect a receiver | sig.connect(fn) |
| Broadcast it | sig.send(sender, **data) |
| Scope to one app | sig.connect_via(app) |
| Disconnect | sig.disconnect(fn) |
🎯 Your Turn: Fill in the Blanks
Complete the code below. Replace each ___ to define a signal, connect a receiver, and broadcast it.
# 🎯 YOUR TURN: replace each ___ to wire up the signal
from blinker import signal
order_placed = ___("order-placed") # create the named signal
log = []
@order_placed.___ # connect this receiver
def on_order(sender, **kw):
log.append(f"order {kw['order_id']} placed")
order_placed.___("shop", order_id=101) # broadcast the event
order_placed.send("shop", order_id=102)
print(log) # ['order 101 placed', 'order 102 placed']Common Errors & Quick Reference
❌ My receiver never runs
Either you never connected it, or you connected to a different signal instance. Use the same named signal("x") everywhere — Blinker returns the same object for the same name.
❌ Receiver is garbage-collected and stops firing
Blinker holds receivers with weak references by default, so a local function can be collected. Keep a reference, or use connect(fn, weak=False) to keep it alive.
| Built-in signal | Fires when |
|---|---|
| request_started | A request begins |
| request_finished | A response is ready |
| template_rendered | A template finishes rendering |
| got_request_exception | An unhandled error occurs |
🏆 Mini Challenge: A Page-View Tracker
Build a custom signal with two receivers that track page views differently.
- One receiver counts views per path into a dict
- Another keeps an ordered list of recent paths
- Send the signal for a few paths and inspect both
# 🏆 Mini Challenge: a custom signal with multiple receivers
from blinker import signal
page_viewed = signal("page-viewed")
views = {} # path -> count
recent = [] # paths in order
@page_viewed.connect
def count_view(sender, **kw):
path = kw["path"]
views[path] = views.get(path, 0) + 1
@page_viewed.connect
def track_recent(sender, **kw):
recent.append(kw["path"])
for p in ["/home", "/about", "/home", "/home"]:
page_viewed.send("router", path=p)
print(views) # {'/home': 3, '/about': 1}
print(recent) # ['/home', '/about', '/home', '/home']
print("receivers:", len(page_viewed.receivers)) # receivers: 2❓ Frequently Asked Questions
Lesson complete — your app components can talk without coupling!
You connected receivers to request_started and template_rendered, created custom signals with Blinker, fanned one event out to many receivers, and decoupled side effects from your core logic.
Practice quiz
What pattern do Flask signals implement?
- Request/response
- Map/reduce
- Publish/subscribe
- Master/worker
Answer: Publish/subscribe. Signals are publish/subscribe: a sender broadcasts and receivers react.
Which library powers Flask's signals?
- Celery
- Redis
- Werkzeug
- Blinker
Answer: Blinker. Flask's signals are built on the Blinker library, shipped with Flask.
What is a function that runs when a signal is sent called?
- A receiver
- A sender
- A middleware
- A blueprint
Answer: A receiver. A receiver is connected to a signal and runs when it is sent.
Which built-in signal fires at the start of every request?
- template_rendered
- request_started
- got_request_exception
- request_finished
Answer: request_started. request_started fires when a request begins, before routing decides the outcome.
What does the template_rendered signal pass to its receivers?
- The status code
- The session
- The template and the context
- The request body
Answer: The template and the context. template_rendered passes the rendered template and its context dict.
How do you create a custom named signal with Blinker?
- Signal.new("name")
- blinker.create("name")
- event("name")
- signal("name")
Answer: signal("name"). signal("name") returns the signal object (same instance for the same name).
How do you broadcast a signal to its receivers?
- sig.send(sender, **data)
- sig.emit(data)
- sig.fire()
- sig.publish()
Answer: sig.send(sender, **data). sig.send(sender, **data) notifies all connected receivers.
Which built-in signal fires when an unhandled error occurs?
- request_started
- got_request_exception
- template_rendered
- request_finished
Answer: got_request_exception. got_request_exception fires when an unhandled exception is raised.
How are signals different from a before_request hook?
- Signals run only once
- Signals modify the response
- Signals are decoupled notifications that can't change the outcome
- Signals replace routing
Answer: Signals are decoupled notifications that can't change the outcome. Hooks process the request and can short-circuit it; signals are decoupled side-effect notifications.
Why might a Blinker receiver stop firing unexpectedly?
- Signals expire after one send
- It must be async
- Flask disables signals in debug
- It was garbage-collected due to weak references
Answer: It was garbage-collected due to weak references. Blinker holds receivers weakly by default; keep a reference or use weak=False.
Continue this course
- Previous: Custom CLI Commands (flask CLI)
- Next: Async Views & ASGI — Write async def views and run Flask under an ASGI server
- Quick reference: Flask cheat sheet
Frequently asked questions
What are signals in Flask?
Signals are a publish/subscribe mechanism: one part of your app sends a named event, and any number of receiver functions connected to it run in response. Flask ships several built-in signals (like request_started and template_rendered) and uses the Blinker library, which is included with Flask, to power them.
How is a signal different from a request hook?
A before_request hook is a single pipeline step that can modify or short-circuit the request, and order matters. Signals are notifications: senders don't know or care who is listening, receivers can't change the outcome, and you can connect or disconnect them freely. Signals are for decoupled side effects, hooks for request processing.
What is Blinker?
Blinker is a small, fast signal/event library for Python. You create a named signal with signal('name'), connect receivers with @signal.connect, and broadcast with signal.send(sender, **data). Blinker ships as a dependency of Flask, so you can use it for your own custom events without installing anything extra.
When should I use a custom signal?
Use one when several unrelated things should happen after an event but you don't want to hard-wire them into the core logic — for example, after a user registers you might write an audit record, send a welcome email, and update analytics. A signal lets each of those subscribe independently, keeping the registration code clean.
Do receivers run in order, and can they fail the sender?
All connected receivers run when a signal is sent, but you should not rely on their order. By default an exception in one receiver propagates, so wrap risky receiver code in try/except if a failure there should not break the sender. Signals are best for side effects, not for returning values to the sender.