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

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: /world

Both 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: True

The 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 signalsignal("my-event")
Connect a receiversig.connect(fn)
Broadcast itsig.send(sender, **data)
Scope to one appsig.connect_via(app)
Disconnectsig.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 signalFires when
request_startedA request begins
request_finishedA response is ready
template_renderedA template finishes rendering
got_request_exceptionAn unhandled error occurs

🏆 Mini Challenge: A Page-View Tracker

Build a custom signal with two receivers that track page views differently.

# 🏆 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

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.

Related lessons