Signals
Reviewed & published by Brayan K
Django signals are a notification system that lets certain senders tell a set of receiver functions that an action has taken place — for example, running code automatically every time a model instance is saved or deleted — so you can decouple side effects from the code that triggers them.
Part of the free Django 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 meet the built-in model signals, learn how to connect receivers with the @receiver decorator, build the classic "auto-create a profile when a user is created" pattern, and register everything correctly so it actually fires.
What You'll Learn in This Lesson
- What signals are and the sender/receiver model
- The built-in model signals: pre_save, post_save, pre_delete, post_delete
- The sender, instance, and created arguments
- Connecting receivers with @receiver and .connect()
- Registering signals in the app's ready() method
- Defining and sending your own custom signals
1 Built-in Model Signals
Every Django model fires signals at key moments in its lifecycle. The most important ones live in django.db.models.signals and fire around saving and deleting a row:
- pre_save — sent just before an instance is written to the database.
- post_save — sent right after the instance is saved (you get a created flag).
- pre_delete — sent just before an instance is deleted.
- post_delete — sent right after the instance is deleted.
A receiver function always accepts the same keyword arguments. Django passes sender (the model class), instance (the actual object), and for save signals a created boolean. You should always end the signature with **kwargs so future arguments don't break your code.
from django.db.models.signals import post_save
from django.dispatch import receiver
from blog.models import Article
@receiver(post_save, sender=Article)
def on_article_saved(sender, instance, created, **kwargs):
if created:
print(f"New article created: {instance.title}")
else:
print(f"Article updated: {instance.title}")The example below builds a tiny signal dispatcher in plain Python so you can watch post_save fire without a database. It mirrors how Django calls each connected receiver.
# A tiny stand-in for Django's signal system, in pure Python.
# Django keeps a list of receivers per signal and calls each one.
class Signal:
def __init__(self, name):
self.name = name
self.receivers = []
def connect(self, func):
self.receivers.append(func)
def send(self, sender, **kwargs):
for func in self.receivers:
func(sender=sender, **kwargs)
# Create the built-in-style signals
post_save = Signal("post_save")
# A receiver always ends with **kwargs to stay future-proof
def on_saved(sender, instance, created, **kwargs):
if created:
print(f"post_save: created {sender} -> {instance}")
else:
print(f"post_save: updated {sender} -> {instance}")
post_save.connect(on_saved)
# Simulate Django saving a brand-new row, then updating it
post_save.send(sender="Article", instance="Hello World", created=True)
post_save.send(sender="Article", instance="Hello World", created=False)
# ✅ Expected output:
# post_save: created Article -> Hello World
# post_save: updated Article -> Hello World2 Connecting Receivers with @receiver
A receiver is just a function. To make it listen for a signal you connect it. The cleanest way is the @receiver decorator, which takes the signal and a sender to filter on:
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Profile
# Classic pattern: create a Profile automatically for every new User
@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)You can also connect receivers manually with .connect() — useful when the receiver is defined elsewhere or connected conditionally:
from django.db.models.signals import post_save
from django.contrib.auth.models import User
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
# Same effect as the decorator, written out by hand:
post_save.connect(create_profile, sender=User)The example below recreates that "auto-create a profile" flow with the same mini dispatcher, so you can see the created guard doing its job.
# Recreating the "auto-create a Profile when a User is created" pattern.
class Signal:
def __init__(self):
self.receivers = []
def connect(self, func, sender=None):
self.receivers.append((func, sender))
def send(self, sender, **kwargs):
for func, want in self.receivers:
if want is None or want == sender:
func(sender=sender, **kwargs)
post_save = Signal()
# Pretend database of profiles, keyed by username
profiles = {}
def create_profile(sender, instance, created, **kwargs):
# The created guard means we only build a profile ONCE
if created:
profiles[instance] = {"user": instance, "bio": ""}
print(f"Profile created for {instance}")
else:
print(f"User {instance} updated (no new profile)")
post_save.connect(create_profile, sender="User")
# New user -> profile is created
post_save.send(sender="User", instance="alice", created=True)
# Same user updated later -> NO duplicate profile
post_save.send(sender="User", instance="alice", created=False)
print("Profiles:", profiles)
# ✅ Expected output:
# Profile created for alice
# User alice updated (no new profile)
# Profiles: {'alice': {'user': 'alice', 'bio': ''}}3 Registering Signals & Custom Signals
Receivers only register when their module is imported and the @receiver decorators actually run. The documented place to make that happen is the ready() method of your app's config in apps.py:
# blog/apps.py
from django.apps import AppConfig
class BlogConfig(AppConfig):
default_auto_field = "django.db.models.BigAutoField"
name = "blog"
def ready(self):
# Importing the module runs its @receiver decorators
import blog.signals # noqa: F401Beyond the built-ins, you can define your own custom signals for events that matter to your app. Create one with Signal() and fire it with .send():
# signals definition
from django.dispatch import Signal
# A custom signal for "an order was paid"
order_paid = Signal()
# somewhere in your payment view, after success:
order_paid.send(sender=Order, order=order, amount=order.total)
# a receiver listens for it like any other signal
from django.dispatch import receiver
@receiver(order_paid)
def send_receipt(sender, order, amount, **kwargs):
print(f"Emailing receipt for order {order.id}: {amount}")The runnable example wires up a custom signal end to end — define it, connect a receiver, and send it.
🎯 Your Turn: Connect the Receiver
Fill in the blank so the receiver listens for the signal that fires right after a row is saved. The function should only print on creation.
# 🎯 YOUR TURN: replace ___ with the signal name "post_save"
class Signal:
def __init__(self, name):
self.name = name
self.receivers = []
def connect(self, func):
self.receivers.append(func)
def send(self, **kwargs):
for func in self.receivers:
func(**kwargs)
post_save = Signal("post_save")
pre_save = Signal("pre_save")
def welcome(sender, instance, created, **kwargs):
if created:
print(f"Welcome, {instance}!")
# Connect to the signal that fires AFTER the save:
___.connect(welcome)
post_save.send(sender="User", instance="bob", created=True)
# EXPECTED OUTPUT:
# Welcome, bob!Common Errors (And How to Fix Them)
❌ My signal never fires
The receiver module is never imported, so the @receiver decorators never run and nothing is connected.
✅ Fix: import your signals module from the ready() method of your app's AppConfig in apps.py.
❌ A duplicate profile is created on every update
The receiver runs on every save because the created flag isn't being checked.
✅ Fix: wrap the side effect in if created: so it only runs on the first save.
❌ RecursionError: maximum recursion depth exceeded
A post_save receiver calls .save() on the same instance, which fires post_save again, looping forever.
✅ Fix: update fields in a pre_save receiver instead, or use queryset.update() which does not re-fire the signal.
📋 Quick Reference
| Signal / Tool | When It Fires / What It Does |
|---|---|
| pre_save | Just before an instance is written to the database |
| post_save | Right after a save; provides the created flag |
| pre_delete | Just before an instance is deleted |
| post_delete | Right after an instance is deleted |
| @receiver | Decorator that connects a function to a signal and sender |
🏆 Mini Challenge: Audit Log Receiver
Build a small audit log. Connect receivers to both a save and a delete signal, then fire each one and watch the log fill up.
# 🏆 Mini Challenge: log every save and delete event.
class Signal:
def __init__(self, name):
self.name = name
self.receivers = []
def connect(self, func):
self.receivers.append(func)
def send(self, **kwargs):
for func in self.receivers:
func(**kwargs)
post_save = Signal("post_save")
post_delete = Signal("post_delete")
audit_log = []
def log_save(sender, instance, created, **kwargs):
action = "created" if created else "updated"
audit_log.append(f"{sender} {instance} was {action}")
def log_delete(sender, instance, **kwargs):
audit_log.append(f"{sender} {instance} was deleted")
post_save.connect(log_save)
post_delete.connect(log_delete)
post_save.send(sender="Article", instance="Django 101", created=True)
post_save.send(sender="Article", instance="Django 101", created=False)
post_delete.send(sender="Article", instance="Django 101")
for entry in audit_log:
print(entry)
# EXPECTED:
# Article Django 101 was created
# Article Django 101 was updated
# Article Django 101 was deleted❓ Frequently Asked Questions
Lesson complete — you can wire up signals!
You now know the built-in model signals, the sender/instance/created arguments, how to connect receivers with @receiver and .connect(), where to register them in ready(), and how to define your own custom signals.
Practice quiz
What are Django signals used for?
- Running code automatically when an event like a save occurs
- Defining URL routes
- Validating forms
- Creating database indexes
Answer: Running code automatically when an event like a save occurs. Signals let receivers run code automatically when senders report an action, e.g. a save.
When does the pre_save signal fire?
- After the row is written
- Just before an instance is written to the database
- Only on delete
- When the server starts
Answer: Just before an instance is written to the database. pre_save fires just before the instance is saved; on a first save it has no primary key yet.
Which signal provides a 'created' boolean argument?
- pre_save
- pre_delete
- post_save
- post_delete
Answer: post_save. post_save fires after the save and passes created=True/False to the receiver.
Which decorator connects a function as a signal receiver?
- @signal
- @connect
- @listener
- @receiver
Answer: @receiver. The @receiver(signal, sender=...) decorator registers a function as a receiver.
Why should a receiver function end its signature with **kwargs?
- To rename arguments
- To stay future-proof against new signal arguments
- To make it run faster
- To skip the sender
Answer: To stay future-proof against new signal arguments. Ending with **kwargs keeps the receiver working if Django adds new keyword arguments.
In the auto-create-profile pattern, why guard with 'if created'?
- So a profile is created only on the first save, not every update
- To speed up queries
- To avoid importing models
- To delete old profiles
Answer: So a profile is created only on the first save, not every update. Without the created guard, the receiver would try to create a profile on every save.
Where is the documented place to import your signals module so receivers register?
- In settings.py
- In urls.py
- In the AppConfig.ready() method in apps.py
- In manage.py
Answer: In the AppConfig.ready() method in apps.py. Importing the signals module from AppConfig.ready() reliably registers the receivers.
Which method fires a signal to all connected receivers?
- signal.fire()
- signal.emit()
- signal.trigger()
- signal.send()
Answer: signal.send(). You dispatch a signal with signal.send(sender=..., **kwargs).
What argument does Django pass as the actual saved object?
- instance
- sender
- model
- obj
Answer: instance. instance is the actual object; sender is the model class.
How can you connect a receiver without the @receiver decorator?
- signal.add(func)
- signal.connect(func, sender=...)
- signal.register(func)
- signal.bind(func)
Answer: signal.connect(func, sender=...). signal.connect(receiver, sender=...) connects a receiver manually.
Continue this course
- Previous: Serializers & ViewSets
- Next: Middleware — Hook into every request and response with custom middleware
- Quick reference: Django cheat sheet
Frequently asked questions
What are Django signals used for?
Signals let you run code automatically when something happens elsewhere in the framework — most often when a model instance is saved, deleted, or when a request finishes. They are ideal for side effects that should fire no matter where the triggering action happens: creating a related profile when a user is created, clearing a cache when a record changes, or sending a notification after an order is placed. They keep that side-effect code out of your views and model methods.
What is the difference between pre_save and post_save?
Both are model signals tied to saving an instance. The pre_save signal fires just before the row is written to the database, so the instance does not yet have a primary key on its first save. The post_save signal fires immediately after the save completes, so the instance has a primary key and you receive a 'created' keyword argument telling you whether the row was newly inserted or updated. Use pre_save to adjust fields before they are stored, and post_save to react once the data is safely in the database.
Why are my signals never firing?
The most common reason is that the module containing your receiver functions is never imported, so the @receiver decorators never run and never register. Django gives you a dedicated place to fix this: the ready() method of your app's AppConfig in apps.py. Import your signals module there and the receivers register when the app loads. Importing the module at the top of models.py or views.py can also work, but apps.py is the documented, reliable location.
Should I use signals or override the save() method?
If the side effect belongs to the model itself and you control every save, overriding save() is simpler and easier to follow. Reach for signals when you need to react to a model you do not own (like the built-in User), when several unrelated pieces should respond to the same event, or when you want to keep the side effect decoupled from the model's own logic. Signals trade a little indirection for decoupling, so use them when that decoupling is genuinely worth it.