diff --git a/docs/advanced.rst b/docs/advanced.rst
index 8da7de58..88aa1cae 100644
--- a/docs/advanced.rst
+++ b/docs/advanced.rst
@@ -124,8 +124,6 @@ including the complexity around related fields and multi-table inheritance.
:pypi:`django-polymorphic` offers a utility function :func:`~polymorphic.utils.prepare_for_copy`
that resets all necessary fields on a model instance to prepare it for copying:
-.. code-block:: python
-
from polymorphic.utils import prepare_for_copy
obj = ModelB.objects.first()
@@ -133,6 +131,124 @@ that resets all necessary fields on a model instance to prepare it for copying:
obj.save()
# obj is now a copy of the original ModelB instance
+
+Working with Signals and Fixtures
+----------------------------------
+
+When using Django's :django-admin:`loaddata` command with polymorphic models, you may notice that
+``post_save`` signal handlers receive instances that appear incomplete – parent class attributes
+may be empty and ``pk``/``id`` fields may not match. **This is expected Django behavior** for
+multi-table inheritance during deserialization, not a bug in :pypi:`django-polymorphic`.
+
+Understanding the Issue
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+During fixture loading, Django deserializes parent and child table rows separately
+(see :Django:`fixture loading `).
+
+When a child model's ``post_save`` signal fires, Django passes a ``raw=True`` parameter to indicate
+the data is being loaded from a fixture. At this point, parent attributes may not yet be fully
+accessible.
+
+Django’s :Django:`post_save [` signal is called with ``raw=True`` during
+fixture loading, and signal handlers should not rely on related or inherited model data at this
+stage.
+
+For example, with this model hierarchy:
+
+.. code-block:: python
+
+ class Endpoint(PolymorphicModel):
+ id = models.UUIDField(primary_key=True, default=uuid.uuid4)
+ name = models.CharField(max_length=250)
+
+ class Switch(Endpoint):
+ ip_address = models.GenericIPAddressField()
+
+ @receiver(post_save, sender=Switch)
+ def switch_saved(sender, instance, created, **kwargs):
+ # During loaddata: instance.name may be empty!
+ print(f"Switch: {instance.name}")
+
+During ``loaddata``, the signal may fire before ``instance.name`` is populated, even though the
+fixture contains the correct data.
+
+Recommended Solution: Check for ``raw=True``
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The standard Django pattern is to check the ``raw`` parameter and skip custom logic during
+fixture loading, as recommended in Django’s signal documentation
+(:Django:`post_save ][`):
+
+.. code-block:: python
+
+ from django.db.models.signals import post_save
+ from django.dispatch import receiver
+
+ @receiver(post_save, sender=Switch)
+ def switch_saved(sender, instance, created, raw, **kwargs):
+ # Skip signal logic during fixture loading
+ if raw:
+ return
+
+ if created:
+ # This logic only runs during normal saves, not loaddata
+ print(f"New switch created: {instance.name}")
+ setup_monitoring(instance)
+
+This approach prevents issues caused by incomplete data during fixture deserialization.
+
+Alternative: Use ``post_migrate`` Signal
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If you need to perform setup tasks after loading fixtures, use the ``post_migrate`` signal instead,
+which runs after migrations and fixture loading have completed:
+
+.. code-block:: python
+
+ from django.db.models.signals import post_migrate
+ from django.dispatch import receiver
+
+ @receiver(post_migrate)
+ def setup_switches(sender, **kwargs):
+ """Run after migrations and fixtures are loaded"""
+ from myapp.models import Switch
+
+ for switch in Switch.objects.filter(monitoring_configured=False):
+ # Now all attributes are fully loaded
+ setup_monitoring(switch)
+ switch.monitoring_configured = True
+ switch.save()
+
+Best Practices for Fixtures
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When working with fixtures and polymorphic models:
+
+1. **Always use natural keys** when creating fixtures with :django-admin:`dumpdata`:
+
+ .. code-block:: bash
+
+ python manage.py dumpdata myapp --natural-foreign --natural-primary > fixture.json
+
+ This ensures ``polymorphic_ctype`` references are portable across databases.
+
+2. **Check for raw=True** in signal handlers to avoid accessing incomplete data.
+
+3. **Use post_migrate** for post-fixture setup tasks rather than ``post_save``.
+
+4. **Verify polymorphic_ctype** after loading fixtures if needed:
+
+ .. code-block:: python
+
+ from polymorphic.utils import reset_polymorphic_ctype
+ from myapp.models import Endpoint, Switch
+
+ # After loaddata, ensure ctype is correct
+ reset_polymorphic_ctype(Endpoint, Switch)
+
+For more details, see `issue #502 `_.
+
Using Third Party Models (without modifying them)
-------------------------------------------------
@@ -329,6 +445,8 @@ Restrictions & Caveats
(or any table that has a reference to :class:`~django.contrib.contenttypes.models.ContentType`),
include the :option:`--natural-primary ` and
:option:`--natural-foreign ` flags in the arguments.
+ See :ref:`Working with Signals and Fixtures` for more details on using fixtures with
+ polymorphic models.
* If the ``polymorphic_ctype_id`` on the base table points to the wrong
:class:`~django.contrib.contenttypes.models.ContentType` (this can happen if you delete child
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 893e0e21..536a0a53 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -105,6 +105,9 @@ works fully automatic and just delivers the expected results.
:option:`--natural-foreign ` flag in the arguments. This makes sure
the :class:`~django.contrib.contenttypes.models.ContentType` models will be referenced by name
instead of their primary key as that changes between Django instances.
+
+ If you're using ``post_save`` signals with polymorphic models, see
+ :ref:`advanced-features` for important information about handling signals during fixture loading.
.. note::
]