Skip to content

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.

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.

graph TD A[Event happens] --> B[Django sends signal] B --> C[Receiver listens] C --> D[Extra action runs] D --> E[Related data updated]

Django provides several built-in signals that you can use in your application. Some of the most commonly used signals include:

A signal consists of three main components:

  1. Sender: The model or class that sends the signal.
  2. Receiver: The function that receives the signal and executes code in response.
  3. Signal: The event object that is sent when something happens.

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.

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.py

Import the handlers inside AppConfig.ready() so receivers are registered when Django starts.

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.py
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth import get_user_model
from ..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.py
from django.apps import AppConfig
class MyAppConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'myapp'
def ready(self):
import myapp.signals.handlers

If 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).

You can also define custom signals when built-in ones are not enough for your use case.

# signals/__init__.py
from django.dispatch import Signal
# Define a custom signal
login_succeeded = Signal()

Now fire this signal after a successful login.

# views.py
from django.utils import timezone
from .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.py
from django.dispatch import receiver
from . import login_succeeded
@receiver(login_succeeded)
def handle_user_logged_in(sender, user, timestamp, **kwargs):
print(f"User {user} logged in at {timestamp}")