← Back to Django Course | Chapter 12: Signals & Middleware | Lesson 2 of 8

The post_save Signal

post_save is Django ringing a bell right after something gets saved to the database, so you can react to it.

What Triggers post_save?

post_save fires right after save() finishes writing to the database. Django passes it sender (the model class), instance (the saved object), and created (True for a new row).

Example: What Triggers post_save?

post_save fires right after save() finishes writing to the database. Django passes it sender (the model class), instance (the saved object), and created (True for a new row).

markup
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Profile

@receiver(post_save, sender=Profile)
def profile_saved(sender, instance, created, **kwargs):
    print(f"Profile {instance.pk} saved, created={created}")
{# Django-only code -- models.py/views.py/urls.py/settings.py snippets, or template markup using Django template tags/variables -- can't run standalone via Judge0 or the browser preview, since it needs a real Django project. Only this course's pure-Python examples (example_lang == 'python', no Django imports) are actually runnable, so those still get the button below. #}

Using the created Flag

A very common pattern is to run setup logic only the first time a row is inserted, such as auto-creating a related Profile whenever a new User is created.

Example: Using the created Flag

A very common pattern is to run setup logic only the first time a row is inserted, such as auto-creating a related Profile whenever a new User is created.

markup
from django.db.models.signals import post_save
from django.dispatch import receiver
from django.contrib.auth.models import User
from .models import Profile

@receiver(post_save, sender=User)
def create_profile(sender, instance, created, **kwargs):
    if created:
        Profile.objects.create(user=instance)
{# Django-only code -- models.py/views.py/urls.py/settings.py snippets, or template markup using Django template tags/variables -- can't run standalone via Judge0 or the browser preview, since it needs a real Django project. Only this course's pure-Python examples (example_lang == 'python', no Django imports) are actually runnable, so those still get the button below. #}

Avoiding Infinite Save Loops

Calling instance.save() again inside a post_save receiver for the same model re-triggers post_save, which can loop forever. Use QuerySet.update() instead, since it doesn't re-fire the signal.

Warning: Never call instance.save() unguarded inside its own post_save receiver.

Example: Avoiding Infinite Save Loops

Calling instance.save() again inside a post_save receiver for the same model re-triggers post_save, which can loop forever. Use QuerySet.update() instead, since it doesn't re-fire the signal.

markup
from django.db.models.signals import post_save
from django.dispatch import receiver
from .models import Article

@receiver(post_save, sender=Article)
def set_slug(sender, instance, created, **kwargs):
    if created and not instance.slug:
        Article.objects.filter(pk=instance.pk).update(slug=f"article-{instance.pk}")
{# Django-only code -- models.py/views.py/urls.py/settings.py snippets, or template markup using Django template tags/variables -- can't run standalone via Judge0 or the browser preview, since it needs a real Django project. Only this course's pure-Python examples (example_lang == 'python', no Django imports) are actually runnable, so those still get the button below. #}

Scoping with sender

Passing sender= when connecting limits a receiver to one model. The same receiver function can be connected to multiple senders if you want shared behavior.

Example: Scoping with sender

Passing sender= when connecting limits a receiver to one model. The same receiver function can be connected to multiple senders if you want shared behavior.

markup
from django.db.models.signals import post_save
from .models import Article, Profile

def log_change(sender, instance, **kwargs):
    print(f"{sender.__name__} #{instance.pk} changed")

post_save.connect(log_change, sender=Article)
post_save.connect(log_change, sender=Profile)
{# Django-only code -- models.py/views.py/urls.py/settings.py snippets, or template markup using Django template tags/variables -- can't run standalone via Judge0 or the browser preview, since it needs a real Django project. Only this course's pure-Python examples (example_lang == 'python', no Django imports) are actually runnable, so those still get the button below. #}
Common Mistakes
  1. Forgetting to check the created flag and running 'new object' logic on every update too.
  2. Causing infinite loops by calling instance.save() again inside a post_save receiver on the same model without a guard.
  3. Connecting a receiver but never importing the module that contains it, so it silently never runs.
Chapter Summary
  • post_save fires immediately after a model instance's save() method completes.
  • The created boolean tells you whether this was an INSERT or an UPDATE.
  • Use sender=Model to scope a receiver to one specific model.
  • Avoid saving the same instance again inside its own post_save receiver to prevent recursion.

Login to run this code

C/C++/Java/PHP execution requires a free account. Your code is saved — you'll land right back in the editor after logging in.