Understanding Django Signals: The Complete Beginner to Advanced Guide
Django Signals are one of the most powerful yet underrated features of Django. Learn how signals work, why they exist, and how to use them to create clean, decoupled, and event driven Django applications.

Written by Zisanur Haque
AI Product engineer writing about systems, growth, and the craft behind the code.
Django Signals allow different parts of a Django application to communicate with each other when certain events occur without tight coupling. Think of them as "event listeners" that let you execute extra logic automatically when something happens, like saving a user, deleting an order, or updating a profile.
What Are Django Signals?
In simple terms, a signal is a notification sent by one part of your code that something has happened. Other parts of your app can "listen" for these notifications and perform specific actions in response. This makes your code modular, maintainable, and easier to extend.
Imagine your Django app as a company: when a new employee joins (a model instance is created), the HR department automatically sends a welcome email. Instead of calling HR from every department, Django Signals let HR listen for "employee_created" events and act automatically.
Why Use Signals?
Signals help you keep your code loosely coupled. Instead of adding logic everywhere (like sending an email every time a user registers inside multiple views), you can centralize it in one place that automatically reacts to an event.
- Keeps your app modular and clean
- Prevents repetitive logic across views or serializers
- Makes event-driven architectures easier to implement
How Django Signals Work Internally
Django's signal system is based on the Observer Pattern. A signal is an instance of Django's Signal class, and functions that respond to it are called receivers.
When an event happens (for example, a model instance is saved), Django triggers a signal like post_save. Any receiver function connected to that signal gets executed automatically.
Signal Components
- Signal: The event itself (e.g.,
post_save,pre_delete) - Sender: The model or object that sends the signal
- Receiver: A function that performs an action when the signal is triggered
- Dispatcher: The mechanism that connects signals and receivers
Built-in Django Signals
Django provides several built-in signals for models, requests, and the ORM:
Model Signals
pre_save: Sent before a model'ssave()is calledpost_save: Sent after a model'ssave()is calledpre_delete: Sent before a model'sdelete()is calledpost_delete: Sent after a model'sdelete()is calledm2m_changed: Sent when a ManyToMany relation changes
Request and Authentication Signals
request_startedandrequest_finisheduser_logged_in,user_logged_out,user_login_failed
Basic Example: Sending a Welcome Email After User Signup
Let's walk through a simple use case: sending a welcome email after a new user is created.
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.dispatch import receiver
@receiver(post_save, sender=User)
def send_welcome_email(sender, instance, created, **kwargs):
if created:
print(f"Welcome {instance.username}! Your account has been created.")
# Here you could send an actual email
Explanation:
- @receiver decorator connects the function to a signal.
- sender=User tells Django to trigger this function only when a
Userinstance is saved. - created parameter tells whether it's a new object or an update.
Without Decorators: The Manual Way
You can connect signals manually using the connect() method:
from django.db.models.signals import post_save
from django.contrib.auth.models import User
def send_welcome_email(sender, instance, created, **kwargs):
if created:
print(f"Welcome {instance.username}!")
post_save.connect(send_welcome_email, sender=User)
Where to Keep Signal Files
Best practice is to keep signals in a dedicated signals.py file inside your app. Then import it in the apps.py file to ensure Django loads it when the app starts.
Example Structure:
myapp/
├── apps.py
├── models.py
├── signals.py
└── __init__.py
apps.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
name = "myapp"
def ready(self):
import myapp.signals
This ensures signals are registered when Django starts up. Without this, your receivers won't get triggered.
Example: Automatically Create a Profile When a User Registers
from django.db.models.signals import post_save
from django.contrib.auth.models import User
from django.dispatch import receiver
from .models import Profile
@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
if created:
Profile.objects.create(user=instance)
Now whenever a user is created, their profile is automatically created too — clean, simple, and decoupled.
Custom Signals
You can also create your own signals. This is useful when you want to define custom application events.
from django.dispatch import Signal
order_completed = Signal()
# Sending the signal
order_completed.send(sender=None, order=order_instance)
# Listening to it
from django.dispatch import receiver
@receiver(order_completed)
def notify_user(sender, order, **kwargs):
print(f"Order {order.id} completed successfully!")
Signal Lifecycle Example
Let's summarize what happens when a signal fires:
- A model action occurs (like saving a user)
- The model emits a signal (e.g.,
post_save) - The dispatcher looks for connected receiver functions
- The receivers execute their logic
Common Mistakes and Best Practices
- Always import signals in
apps.py, notmodels.py - Keep your receiver functions small and focused
- Avoid circular imports by using lazy imports inside signal functions
- Use signals for side effects, not core logic
- Don't overuse signals, too many can make debugging hard
Real-World Use Cases
- Automatically creating related models (User → Profile)
- Logging important actions (like updates or deletions)
- Sending notifications or emails on specific triggers
- Maintaining audit trails and history
Advanced Tip: Using dispatch_uid
To prevent a signal from being registered multiple times, use dispatch_uid:
post_save.connect(send_welcome_email, sender=User, dispatch_uid="unique_welcome_email_signal")
Conclusion
Django Signals are an elegant way to make your applications more modular, decoupled, and reactive. Whether you're sending a welcome email, auto-creating profiles, or tracking logs, signals allow your app to behave intelligently in response to events — without unnecessary dependencies or code duplication. Once you master them, you can design cleaner, event-driven Django systems with ease.