Django is built on the principle of simplicity and reusability. One of its most elegant and powerful features is the signal framework, which allows different parts of a Django application to communicate with each other without tightly coupling their logic. Signals help you trigger specific actions automatically when certain events occur — for example, sending a welcome email when a new user registers, updating a cache after a model is saved, or logging events when objects are deleted.
In this post, we will explore Django signals in depth — what they are, how they work, their practical use cases, and how to implement them effectively in real-world Django projects.
1. What Are Django Signals?
Django Signals are a mechanism that allows certain senders to notify a set of receivers when specific actions have taken place. The sender does not need to know which functions are listening — it simply emits a signal, and any connected receiver will respond.
In simpler terms:
- A sender sends out a message (signal).
- A receiver listens for that message and performs an action when it occurs.
This design pattern follows the Observer Pattern, which promotes a clean separation between components.
1.1 Why Signals Exist
Without signals, developers often write additional logic directly into model methods such as save() or delete(). This leads to tight coupling, meaning the model and its side effects are bound together, making maintenance harder.
For example, if you want to send an email whenever a user registers, you could place that logic inside the User model’s save() method — but that would make the model responsible for email delivery, which violates the principle of separation of concerns.
With signals, you can place the email-sending logic elsewhere, keeping your models clean and your application modular.
2. How Django Signals Work
Django’s signal framework provides the following key components:
- Signal – The object that defines an event.
- Sender – The source that emits (sends) the signal.
- Receiver – The function that listens for and responds to the signal.
- Dispatcher – The mechanism that connects senders and receivers.
2.1 Built-in Signals
Django includes several built-in signals that you can use without extra configuration. Some commonly used ones are:
| Signal | Description |
|---|---|
pre_save | Sent before a model’s save() method is called. |
post_save | Sent after a model’s save() method completes. |
pre_delete | Sent before a model’s delete() method is called. |
post_delete | Sent after a model’s delete() method completes. |
m2m_changed | Sent when a ManyToManyField is modified. |
request_started | Sent when an HTTP request starts. |
request_finished | Sent when an HTTP request finishes. |
user_logged_in | Sent when a user logs in. |
user_logged_out | Sent when a user logs out. |
3. A Simple Example of Django Signals
Let’s explore a simple example using Django’s built-in post_save signal.
We’ll send an email to a user after their account is created.
3.1 Define a Signal Receiver
In your app’s signals.py file:
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from django.core.mail import send_mail
@receiver(post_save, sender=User)
def send_welcome_email(sender, instance, created, **kwargs):
if created:
subject = "Welcome to Our Platform"
message = f"Hello {instance.username}, thank you for registering!"
from_email = "[email protected]"
recipient_list = [instance.email]
send_mail(subject, message, from_email, recipient_list)
Explanation:
- The
@receiverdecorator connects the function to the signal. post_saveis the signal being listened to.sender=Userensures it only listens for events related to theUsermodel.- The
createdflag tells us whether a new instance was created (True) or updated (False).
3.2 Registering the Signal
You must ensure the signal is loaded when Django starts.
In your app’s apps.py file, modify the ready() method:
from django.apps import AppConfig
class UsersConfig(AppConfig):
name = 'users'
def ready(self):
import users.signals
Now, when a new user registers, Django automatically triggers the send_welcome_email function.
4. Creating Custom Signals
Besides built-in signals, you can create custom signals for specific use cases in your application.
4.1 Define a Custom Signal
In signals.py:
from django.dispatch import Signal
order_created = Signal()
Here, order_created is a custom signal you can use anywhere in your project.
4.2 Send a Custom Signal
You can send this signal manually using the send() method.
In views.py:
from django.shortcuts import render
from .signals import order_created
def create_order(request):
# Some order creation logic here
order = {"id": 123, "total": 250}
order_created.send(sender=None, order=order)
return render(request, 'order_success.html')
4.3 Receive a Custom Signal
In signals.py, add a receiver:
from django.dispatch import receiver
@receiver(order_created)
def handle_order_created(sender, **kwargs):
order = kwargs.get('order')
print(f"New order created: {order['id']} worth {order['total']}")
Whenever order_created.send() is called, the handle_order_created function executes automatically.
5. Connecting Signals Without the Decorator
Sometimes you may not want to use the @receiver decorator. Django allows you to manually connect signals.
Example:
from django.db.models.signals import post_delete
from django.contrib.auth.models import User
def user_deleted(sender, instance, **kwargs):
print(f"User deleted: {instance.username}")
post_delete.connect(user_deleted, sender=User)
This achieves the same effect as the decorator but is more explicit.
6. Real-World Use Cases of Django Signals
6.1 Sending Notification Emails
You can use signals to send notification emails when specific actions occur.
Example: Notify admin when a new blog post is published.
@receiver(post_save, sender=Blog)
def notify_admin(sender, instance, created, **kwargs):
if created:
send_mail(
subject="New Blog Post Published",
message=f"{instance.title} was published by {instance.author}",
from_email="[email protected]",
recipient_list=["[email protected]"]
)
6.2 Automatically Creating Related Objects
When a new user registers, you might want to create a profile automatically.
@receiver(post_save, sender=User)
def create_user_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
This ensures that every user has a profile without writing extra logic in the registration view.
6.3 Logging Activity
Use signals to log user activity automatically.
@receiver(post_delete, sender=User)
def log_user_deletion(sender, instance, **kwargs):
with open("deletions.log", "a") as f:
f.write(f"User {instance.username} was deleted.\n")
6.4 Clearing Cache After Model Update
If your site uses caching, you can use signals to clear cache whenever an object changes.
from django.core.cache import cache
@receiver(post_save, sender=Article)
def clear_article_cache(sender, instance, **kwargs):
cache.delete(f"article_{instance.id}")
6.5 Tracking User Login and Logout
Django provides user_logged_in and user_logged_out signals to track authentication events.
from django.contrib.auth.signals import user_logged_in, user_logged_out
@receiver(user_logged_in)
def user_logged_in_handler(sender, request, user, **kwargs):
print(f"{user.username} just logged in.")
@receiver(user_logged_out)
def user_logged_out_handler(sender, request, user, **kwargs):
print(f"{user.username} just logged out.")
7. Signal Execution Flow
To understand what happens under the hood:
- The sender emits the signal (using
send()or a built-in event likepost_save). - The dispatcher looks for connected receivers.
- Each receiver function is called sequentially.
- The receiver executes its logic and may modify or act upon the data received.
8. Avoiding Common Mistakes
8.1 Not Importing the Signal Module
If you forget to import your signals.py in apps.py, Django won’t load the signal handlers.
Always include:
def ready(self):
import yourapp.signals
8.2 Using Signals for Business Logic
While signals are convenient, they should not replace core business logic.
Use them for side effects — such as logging, notifications, or analytics — not for essential operations like saving database entries critical to app flow.
8.3 Infinite Loops
Be careful not to trigger infinite loops.
For example, if your post_save receiver modifies and saves the same model instance again, it will re-trigger the signal.
Example of what not to do:
@receiver(post_save, sender=User)
def risky_signal(sender, instance, **kwargs):
instance.username = instance.username.upper()
instance.save() # This triggers post_save again!
To prevent this, use conditional checks or update() instead of save() when necessary.
9. Disconnecting Signals
Sometimes you need to disconnect a signal (for example, during testing).
You can use the disconnect() method.
post_save.disconnect(send_welcome_email, sender=User)
This temporarily disables the signal receiver.
10. Performance Considerations
Each signal adds overhead because it triggers additional function calls.
For high-traffic systems, make sure signals:
- Perform lightweight operations.
- Avoid unnecessary queries.
- Use asynchronous processing (like Celery) for long-running tasks such as email sending.
Example using Celery in a signal:
from .tasks import send_welcome_email_task
@receiver(post_save, sender=User)
def async_welcome_email(sender, instance, created, **kwargs):
if created:
send_welcome_email_task.delay(instance.id)
This keeps the signal fast and responsive.
11. Organizing Signals in a Django Project
For better maintainability:
- Create a dedicated
signals.pyfile in each app. - Keep related signal handlers together.
- Load them in
apps.py(not inmodels.py).
Example project structure:
myproject/
│
├── users/
│ ├── models.py
│ ├── signals.py
│ ├── apps.py
│ └── views.py
└── blog/
├── models.py
├── signals.py
└── apps.py
This keeps your app modular and clean.
12. Advanced Signal Patterns
12.1 Sending Extra Arguments
Signals can include custom arguments.
order_created = Signal(providing_args=["order", "user"])
Then when sending:
order_created.send(sender=None, order=order, user=user)
12.2 Connecting Multiple Receivers
Multiple receivers can listen to the same signal.
@receiver(post_save, sender=User)
def send_notification(sender, instance, created, **kwargs):
pass
@receiver(post_save, sender=User)
def update_user_stats(sender, instance, created, **kwargs):
pass
Both will be triggered when a User object is saved.
12.3 Anonymous Senders
You can set sender=None when connecting a signal, allowing any sender to trigger it.
@receiver(post_save, sender=None)
def global_post_save_handler(sender, **kwargs):
print(f"Object saved from model: {sender.__name__}")
This listens for all model saves.
13. Testing Django Signals
Testing ensures your signals are connected and working properly.
Example using Django’s TestCase:
from django.test import TestCase
from django.contrib.auth.models import User
from django.db.models.signals import post_save
from users.signals import send_welcome_email
class SignalTests(TestCase):
def test_signal_triggers(self):
post_save.connect(send_welcome_email, sender=User)
user = User.objects.create(username='testuser', email='[email protected]')
self.assertEqual(user.username, 'testuser')
14. When to Use and When Not to Use Signals
Use Signals When:
- You need to trigger secondary actions (e.g., logging, analytics).
- You want to keep models/views clean.
- You want to avoid circular dependencies.
Avoid Signals When:
- The logic is business-critical.
- You need explicit, predictable flow (use service layers instead).
- The project is large and has complex data flows — signals can make debugging harder.
15. Django Signals vs Middleware
While both signals and middleware can react to events, they serve different purposes:
- Signals are tied to model or request events.
- Middleware processes every request/response globally.
Use signals for object-level actions, and middleware for request-level actions.
Leave a Reply