Skip to content

Django Signals

Django signals let you run some code automatically whenever a specific event happens in your app. They’re great for keeping “extra” stuff (like logging, sending notifications, tracking analytics, or clearing a cache) separate from your main app logic, instead of mixing it all together.

A signal is just a message that Django sends out when something happens. The thing that sends this message is called the sender, and the function that reacts to it is called the receiver (also called a signal handler). Receivers usually accept sender and **kwargs as arguments, because Django also passes along useful extra info - like the object that was just saved, whether it was newly created, and other small details.

You can think of a signal like an event hook. The code that sends the signal doesn’t need to know who is listening for it, and the code that listens doesn’t need to know exactly where the signal came from. This is the main reason signals are so useful - they keep different parts of your code from depending too much on each other.

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

Django comes with several built-in signals you can use right away in your app. Some of the most commonly used ones are:

A signal is made up of three main parts:

  1. Sender: The model or class that sends out the signal.
  2. Receiver: The function that picks up the signal and runs some code in response.
  3. Signal: The actual event object that gets sent when something happens.

Signals are great for keeping different parts of your app loosely connected (decoupled). They let one part of your code react to an event happening somewhere else, without the two parts needing to know much about each other.

It’s best to use signals mainly for smaller, secondary side effects. For anything that’s a core business rule, it’s better to call a clear, direct function or service instead - that way your main flow stays easy to follow and easy to debug.

You can keep your signal handlers in one single signals.py file, or organize them inside a package, like signals/handlers.py.

If you go with the package style, a common folder layout looks like this:

myapp/
apps.py
models.py
signals/
__init__.py
handlers.py

Make sure to import your handlers inside AppConfig.ready(), so Django actually registers your receivers when the app starts up.

Receiver functions are the functions that run whenever a signal gets sent. They usually take sender and **kwargs, plus any extra arguments that come with that specific signal (like instance, created, raw, and so on).

For example, say you have a User model and a UserProfile model. You can use the post_save signal to automatically create a profile every time 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)

Now import this module inside apps.py, so Django knows to register the receiver.

# 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’re using this config class, make sure your app is listed as myapp.apps.MyAppConfig inside INSTALLED_APPS (or that Django is set up to load this config automatically on its own).

If the built-in signals don’t cover what you need, you can also create your own custom signals.

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

Now let’s fire (send out) this signal right after a user successfully logs in.

# 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 sends the signal out to every receiver that’s listening. If any one receiver throws an error, that error will stop the rest of the receivers from running.

  • send_robust(): This works almost the same way, but it catches any errors from a receiver and keeps going, calling the rest of the receivers anyway. It gives you back a (receiver, response_or_exception) pair for each receiver, so you can see what happened with each one.

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}")