Django Signals
Django signals let you run code when specific events happen in your app. They are useful for decoupling side effects (for example: logging, notifications, analytics, cache invalidation) from the main business flow.
What is a Signal?
Section titled “What is a Signal?”A signal is a notification that Django sends when something happens. The object that sends the notification is called the sender, and the function that reacts to it is called the receiver or signal handler. Receivers usually accept sender and **kwargs, because Django passes useful context like the saved instance, whether the object was newly created, and other metadata.
Think of a signal as an event hook. The code that emits the event does not need to know who is listening, and the listening code does not need to know exactly where the event was triggered. That is the main reason signals help with separation of concerns.
Types of Signals
Section titled “Types of Signals”Django provides several built-in signals that you can use in your application. Some of the most commonly used signals include:
pre_save: Sent before a model’ssave()method is called.post_save: Sent after a model’ssave()method is called.pre_delete: Sent before a model’sdelete()method is called.post_delete: Sent after a model’sdelete()method is called.- etc. Docs: https://docs.djangoproject.com/en/4.2/topics/signals/#built-in-signals
Components of a Signal
Section titled “Components of a Signal”A signal consists of three main components:
- Sender: The model or class that sends the signal.
- Receiver: The function that receives the signal and executes code in response.
- Signal: The event object that is sent when something happens.
Why Use Signals?
Section titled “Why Use Signals?”Signals are useful for decoupling different parts of your application. They let you react to events without tightly coupling the trigger code and the response code.
Use signals mainly for secondary side effects. For critical business rules, prefer explicit service/function calls so the flow stays obvious and easier to debug.
Directory and File Structure
Section titled “Directory and File Structure”You can put signal handlers in a single signals.py file or in a package like signals/handlers.py.
If you choose a package-based structure, a common layout is:
myapp/ apps.py models.py signals/ __init__.py handlers.pyImport the handlers inside AppConfig.ready() so receivers are registered when Django starts.
Receiver Functions
Section titled “Receiver Functions”Receiver functions run when a signal is sent. They typically accept sender and **kwargs, plus signal-specific arguments (like instance, created, raw, etc.).
For example, if you have User and UserProfile models, you can use post_save to create a profile when a new user is created.
# myapp/signals/handlers.pyfrom django.db.models.signals import post_savefrom django.dispatch import receiverfrom django.contrib.auth import get_user_modelfrom ..models import UserProfile
User = get_user_model()
@receiver(post_save, sender=User)def create_user_profile(sender, instance, created, **kwargs): if created: UserProfile.objects.create(user=instance)Then import this module in apps.py to ensure receivers are registered.
# apps.pyfrom django.apps import AppConfig
class MyAppConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'myapp'
def ready(self): import myapp.signals.handlersIf you use this config class, make sure your app is referenced as myapp.apps.MyAppConfig in INSTALLED_APPS (or that Django loads this config automatically in your setup).
Custom Signals
Section titled “Custom Signals”You can also define custom signals when built-in ones are not enough for your use case.
# signals/__init__.pyfrom django.dispatch import Signal
# Define a custom signallogin_succeeded = Signal()Now fire this signal after a successful login.
# views.pyfrom django.utils import timezonefrom .signals import login_succeeded
def login_view(request): # ... your login logic here ...
# After successful login, send the custom signal login_succeeded.send( sender=type(request.user), user=request.user, timestamp=timezone.now(), )-
send(): This method sends the signal to all connected receivers. If any receiver raises an exception, the exception will propagate and may prevent other receivers from being called. -
send_robust(): This method catches exceptions raised by receivers and continues calling the remaining receivers. It returns(receiver, response_or_exception)tuples for each receiver.
Demo Handler for Custom Signal
# signals/handlers.pyfrom django.dispatch import receiverfrom . import login_succeeded
@receiver(login_succeeded)def handle_user_logged_in(sender, user, timestamp, **kwargs): print(f"User {user} logged in at {timestamp}")