Fixtures, Serialization & Initial Data

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against Django 6.1 (6.1.1, released 2026-09-02; 6.1.2 is scheduled for 2026-10-06) on Python 3.12 to 3.14, with the 5.2 LTS differences noted where they matter, and was checked on 2026-10-03. The examples ran on Django 6.1.1 and Python 3.13 with SQLite and PyYAML 6.0.3, in a throw-away project with a small catalog app (the Bookshelf catalogue that grows across this section); the factory examples ran with factory_boy 3.3.3, whose PyPI classifiers list Django only up to 5.1 but which worked on Django 6.1.1 in that project. Nothing here needs PostgreSQL or Redis.

Sooner or later every project needs to move data between databases or into a database: seed a development machine with a realistic catalogue, ship a handful of reference rows with an app, copy a few records from production into staging, give a test the objects it needs, or hand JSON to another system. Django covers all of this with one mechanism, the serialization framework, and builds the dumpdata and loaddata commands and fixtures on top of it. This page covers the commands and the fixture search path, natural keys (the feature that makes fixtures portable), the serialization formats and the Python API behind them, the supported ways of providing initial data, and the reasons why most test suites should create their data with factories instead of fixtures.

The pieces and how they fit

Four terms are easy to confuse, so it helps to separate them.

Table 1. Vocabulary
Term Meaning

Serializer

A class registered for a format name (json, xml, yaml, jsonl, python) that turns a queryset or any iterable of model instances into text, and a matching Deserializer that turns text back into unsaved instances.

Fixture

A named collection of serialized objects stored in files, found by loaddata (or by the fixtures attribute of a test case) on a search path.

Natural key

A tuple of values, such as an ISBN, that identifies a row without using its primary key, so a fixture does not depend on database-generated ids.

Initial data

Rows the application needs in every environment: lookup tables, default groups, feature flags. Django offers no automatic "initial data" file; you provide it with a data migration, a signal handler or a command.

Nothing in this machinery sits in the request/response cycle. It runs from management commands, from migrations and from tests, and only when you call the serializer API explicitly (for example to build a JSON response) does it run inside a view. The serializers are synchronous; call them from async code through sync_to_async() or asgiref.sync, and note that the queryset is evaluated while serializing.

The example models

The examples use three small models. Each model that other rows refer to has a natural key: a manager method get_by_natural_key() to load by it and a model method natural_key() to dump by it. Both are explained in the natural keys section; they are shown here once so every later example is complete.

catalog/models.py
from django.db import models


class AuthorManager(models.Manager):
    def get_by_natural_key(self, name):
        return self.get(name=name)


class Author(models.Model):
    name = models.CharField(max_length=100, unique=True)
    mentor = models.ForeignKey(
        "self", null=True, blank=True, on_delete=models.SET_NULL, related_name="mentees"
    )

    objects = AuthorManager()

    def natural_key(self):
        return (self.name,)

    def __str__(self):
        return self.name


class TagManager(models.Manager):
    def get_by_natural_key(self, name):
        return self.get(name=name)


class Tag(models.Model):
    name = models.CharField(max_length=50, unique=True)

    objects = TagManager()

    def natural_key(self):
        return (self.name,)

    def __str__(self):
        return self.name


class BookManager(models.Manager):
    def get_by_natural_key(self, isbn):
        return self.get(isbn=isbn)


class Book(models.Model):
    title = models.CharField(max_length=200)
    isbn = models.CharField(max_length=13, unique=True)
    author = models.ForeignKey(Author, on_delete=models.CASCADE, related_name="books")
    tags = models.ManyToManyField(Tag, blank=True)

    objects = BookManager()

    def natural_key(self):
        return (self.isbn,)

    natural_key.dependencies = ["catalog.author"]

    def __str__(self):
        return self.title

Source: Natural keys.

dumpdata: producing a fixture

dumpdata writes the serialized contents of the database to standard output, or to a file with --output. With no arguments it dumps every installed app; pass app_label or app_label.ModelName labels to dump less. The output is valid input for loaddata.

Dumping the catalogue
python manage.py dumpdata catalog --indent 2
[
{
  "model": "catalog.author",
  "pk": 1,
  "fields": {
    "name": "Ursula Le Guin",
    "mentor": null
  }
},
{
  "model": "catalog.book",
  "pk": 1,
  "fields": {
    "title": "The Dispossessed",
    "isbn": "9780061054884",
    "author": 1,
    "tags": [
      1,
      2
    ]
  }
}
]

(The output above is trimmed to two objects; the real output lists every row of catalog.author, catalog.tag and catalog.book.) Source: dumpdata.

Table 2. dumpdata options
Option Effect

app_label[.ModelName]

Limits the dump to apps or models. Without labels every installed app is dumped.

--format

The output format; defaults to json. Any registered format works: json, jsonl, xml, yaml (with PyYAML installed) or a custom one.

--indent

Number of spaces used to indent the output. Without it everything is written on one line.

--exclude, -e

Excludes an app or an app_label.ModelName; repeat the option to exclude several.

--natural-foreign

Uses natural_key() to serialize foreign keys and many-to-many relations to models that define it.

--natural-primary

Omits the primary key of objects that have a natural key, because it can be recomputed on load.

--pks

A comma-separated list of primary keys. Only valid when dumping a single model; otherwise the command fails with You can only use --pks option with one model.

--output, -o

Writes to a file instead of standard output. Ending the file name in .bz2, .gz, .lzma or .xz compresses it; a progress bar is shown when --verbosity is above 0.

--database

The database alias to dump from; defaults to default.

--all, -a

Uses the base manager instead of the default manager, so records hidden by a filtering default manager are included.

dumpdata uses the model’s default manager to select rows. If your default manager filters out soft-deleted rows, those rows silently disappear from the dump unless you pass --all. See Managers & Custom QuerySets for why the default manager deserves care.

Writing fixtures to files
# portable fixture: natural keys, no database-generated ids
python manage.py dumpdata catalog --natural-foreign --natural-primary \
    --indent 2 -o catalog/fixtures/bookshelf.json

# a single model, compressed, as JSON Lines
python manage.py dumpdata catalog.tag --format jsonl -o tags.jsonl.xz

# everything except a model
python manage.py dumpdata catalog --exclude catalog.tag

Two habits keep dumps useful. Dump only your own apps: never dump contenttypes, auth.Permission or sessions into a fixture, because those rows are generated by migrate and their primary keys depend on when and in which order it ran (see Natural keys for the failure modes). And prefer a small hand-picked fixture to a full dump of production: a fixture is source code, it is read in code review and it should contain only what its consumer needs. Never put personal data from a production database into a fixture that is committed.

loaddata: using a fixture

loaddata searches for the named fixtures, deserializes them and saves the objects. All fixtures named in one call are loaded in a single transaction, in the order you list them.

Loading fixtures
python manage.py loaddata bookshelf
Installed 6 object(s) from 1 fixture(s)

Source: loaddata.

Table 3. loaddata options
Option Effect

fixture [fixture …​]

Fixture labels: a name (bookshelf), a name with extension (bookshelf.json) or a path (catalog/bookshelf, /abs/path/bookshelf).

--database

The database alias to load into; defaults to default.

--app

Searches the fixtures directory of one app only.

--format

The format of data read from standard input (required when the fixture name is -).

--exclude, -e

Skips an app or app_label.ModelName in the fixture. Repeatable.

--ignorenonexistent, -i

Ignores fields and models that no longer exist, so an old fixture still loads after a model lost a column. It does not ignore a missing fixture file.

Standard input, apps and a missing fixture
python manage.py dumpdata catalog.tag | python manage.py loaddata --format=json -
python manage.py loaddata --app catalog bookshelf
python manage.py loaddata nope
Installed 2 object(s) from 1 fixture(s)
Installed 6 object(s) from 1 fixture(s)
CommandError: No fixture named 'nope' found.

Source: loaddata.

How fixtures are discovered

Django looks for each fixture in three places: the fixtures directory of every installed app, every directory listed in the FIXTURE_DIRS setting, and the literal path named by the label. It loads every match with the requested name from all those places, in INSTALLED_APPS order. If the label has an extension (bookshelf.json) only that type is loaded; without one, every registered type is searched. Directory components in the label extend the search path, so loaddata shared/extra_tags looks for shared/extra_tags.* below each location. An absolute path switches the default search off. Use -v 2 to watch the search.

Adding a project-level fixture directory
# config/settings.py
FIXTURE_DIRS = [BASE_DIR / "seed"]
python manage.py loaddata shared/extra_tags -v 2
Checking '.../g3/task19/seed/shared' for fixtures...
Installing yaml fixture 'extra_tags' from '.../g3/task19/seed/shared'.
Installed 2 object(s) from 1 fixture(s)

Because Django loads all fixtures of the same name, name your app fixtures distinctively (catalog_bookshelf.json, not data.json), otherwise a fixture from another app may be loaded together with yours.

File names, compression and databases

  • A fixture file is <name>.<format>, for example genres.yaml. If two files with the same name but different formats are found (mydata.json and mydata.xml.gz), installation aborts and everything installed by that loaddata call is rolled back.

  • Fixtures can be compressed with zip, gz, bz2, lzma or xz: loaddata mydata.json finds mydata.json.gz and the others. The first file inside an archive is used. XZ support arrived in Django 3.2.

  • In a multiple-database setup a fixture named mydata.users.json is only loaded when --database users is given. See Transactions, Raw SQL & Multiple Databases.

What loaddata does to the database

Three behaviours cause most surprises.

  1. Rows are saved as they are. The model’s save() is not called and no validation runs. pre_save, post_save and (since Django 6.1) m2m_changed are sent with raw=True.

  2. Existing rows are overwritten. An object with the same primary key (or the same natural key) is updated, so rerunning loaddata restores the fixture values and discards manual edits to those rows. It does not remove rows that are missing from the fixture.

  3. Constraints are checked at the end of the load. If a fixture refers to a row that is neither in the fixture nor in the database, the load fails with an IntegrityError and nothing is kept.

The second behaviour is what makes a natural-key fixture idempotent. Loading the same fixture twice into the example project reports Installed 6 object(s) both times and leaves two authors and two books in place.

Because signals receive raw=True, a receiver that touches related objects must skip raw saves; the related rows may not exist yet.

catalog/signals.py
from django.db.models.signals import post_save
from django.dispatch import receiver

from .models import Book

SEEN = []


@receiver(post_save, sender=Book)
def announce_book(sender, instance, created, raw=False, **kwargs):
    if raw:  # loaddata / deserialization: related objects may not exist yet
        SEEN.append(("raw", instance.isbn))
        return
    SEEN.append(("normal", instance.isbn))

The receiver is connected in CatalogConfig.ready() (from . import signals). Loading the bookshelf fixture and then creating a book in the shell gave [('raw', '9780061054884'), ('raw', '9780807083697')] followed by ('normal', '1').

Source: How fixtures are saved to the database. See Signals for receivers in general.

Natural keys

By default a serialized foreign key or many-to-many relation stores the related row’s primary key. That is fragile whenever the key is generated by the database: the primary key of a ContentType, a Permission or a Group depends on how and when migrate ran, so the same number can point to a different row in another database, or to none. Django warns against including automatically generated objects in a fixture at all: with bad luck the keys happen to match and the load does nothing, and in the likelier case they differ and it fails with an IntegrityError.

A natural key is a tuple of values that uniquely identifies an object without its primary key. The fields behind it must identify one row, which normally means unique=True on one field or a UniqueConstraint over several; uniqueness need not be enforced by the database, but then it is your responsibility.

Natural keys have two halves, and you can implement either one alone:

  • Loading needs get_by_natural_key(*key) on the model’s default manager. It resolves a key tuple to a row, both for foreign keys and for objects that have no primary key in the fixture.

  • Dumping needs natural_key() on the model, returning the tuple. dumpdata --natural-foreign (or use_natural_foreign_keys=True) then writes references to that model as the tuple, and dumpdata --natural-primary (or use_natural_primary_keys=True) omits the model’s own pk.

The models in The example models implement both. The same books, dumped with natural keys:

Dumping with natural keys
python manage.py dumpdata catalog.book --natural-foreign --natural-primary --indent 2
[
{
  "model": "catalog.book",
  "fields": {
    "title": "The Dispossessed",
    "isbn": "9780061054884",
    "author": [
      "Ursula Le Guin"
    ],
    "tags": [
      [
        "sci-fi"
      ],
      [
        "classic"
      ]
    ]
  }
},
{
  "model": "catalog.book",
  "fields": {
    "title": "Kindred",
    "isbn": "9780807083697",
    "author": [
      "Octavia Butler"
    ],
    "tags": [
      [
        "sci-fi"
      ]
    ]
  }
}
]

The fixture contains no database-generated numbers. Loaded into an empty database (after flush), it created both books with the right authors and tags, and a second loaddata updated them instead of duplicating them.

Source: Natural keys.

Dependencies and ordering

When --natural-foreign is used, models that define natural_key() are serialized before models that do not. If a natural key itself depends on another object (a book identified by title and author, for instance), declare the dependency on the method so the other model is dumped first:

def natural_key(self):
    return (self.isbn,)

natural_key.dependencies = ["catalog.author"]

The dependencies attribute lists app-label.model labels (lowercase model name). Without it a fixture may contain a book before its author.

Forward references

A fixture can still reference an object that appears later in the same file. Whether that loads depends on the foreign key. With the non-nullable Book.author it fails. With the nullable Author.mentor it works, because Django can save the row with NULL and fill the reference in once the target exists. This fixture lists Octavia Butler before the mentor she refers to:

fwd.json
[
  {"model": "catalog.author", "fields": {"name": "Octavia Butler", "mentor": ["Harlan Ellison"]}},
  {"model": "catalog.author", "fields": {"name": "Harlan Ellison", "mentor": null}}
]
python manage.py loaddata fwd.json
Installed 2 object(s) from 1 fixture(s)

loaddata handles this for you. When you call the deserializer yourself you must opt in with handle_forward_references=True and finish the job with save_deferred_fields():

from django.core import serializers

deferred = []
for obj in serializers.deserialize("json", data, handle_forward_references=True):
    obj.save()
    if obj.deferred_fields is not None:
        deferred.append(obj)

for obj in deferred:
    obj.save_deferred_fields()

Run against the fixture above, this restored Octavia Butler’s mentor. Without the flag the same data raised DeserializationError: Author matching query does not exist.. The same load against a fixture whose book comes before its (non-nullable) author fails at the end of the load with IntegrityError: …​ NOT NULL constraint failed: catalog_book.author_id.

Opting out in a subclass

Since Django 6.1 a subclass of a model that defines natural_key() can return an empty tuple () from it. The serializer then falls back to the primary key for that subclass, so dumpdata --natural-primary still writes the pk. The opt-out is new in 6.1; on 5.2 LTS a subclass inherits the parent’s natural_key().

Serialization formats and the Python API

The same machinery is available in code, which is how you read and write data in views, tasks and tests.

Table 4. Built-in formats
Format Notes

json

The default. Encoded with DjangoJSONEncoder, so dates, decimals, UUIDs and lazy strings work. Unicode is written as-is since Django 3.1 (pass ensure_ascii=True for the old behaviour). Since Django 6.0 the output ends with a newline even without indent.

jsonl

JSON Lines: one object per line, convenient for streaming and large files. Added in Django 3.2.

xml

A simple XML dialect (<django-objects> with <object> and <field> elements). Serializing control characters not allowed in XML 1.0 raises ValueError; since Django 6.1 the deserializer raises SuspiciousOperation when it finds unexpected nested tags.

yaml

Available only if PyYAML is installed. Like JSON, it writes Unicode as-is since 3.1.

python

Not a text format: it produces and consumes Python dictionaries and lists, and is the base class other serializers build on.

serializers.get_serializer_formats() lists the registered names; in the example project it returned ['xml', 'python', 'json', 'yaml', 'jsonl'].

Serializing

from django.core import serializers

from catalog.models import Book, Tag

# a queryset, choosing the fields and using natural keys for relations
serializers.serialize(
    "json",
    Book.objects.all(),
    fields=["title", "author"],
    use_natural_foreign_keys=True,
)
[{"model": "catalog.book", "pk": 1, "fields": {"title": "The Dispossessed", "author": ["Ursula Le Guin"]}}, {"model": "catalog.book", "pk": 2, "fields": {"title": "Kindred", "author": ["Octavia Butler"]}}]

serialize(format, queryset, **options) accepts any iterable of model instances, so a single object is passed as [obj]. Options: fields (a subset; note that the result cannot be loaded back for a model whose required fields are missing), use_natural_foreign_keys, use_natural_primary_keys, stream (write to a file-like object instead of returning a string), indent, and format-specific ones such as cls= for a custom JSON encoder. serializers.get_serializer("json") returns the class when you want to keep the serializer object and call getvalue() yourself. Serializing a model that inherits from a concrete parent writes only the child’s own fields, so serialize the parent rows as well when you want them in the output.

with open("authors.json", "w") as out:
    serializers.serialize("json", Author.objects.all(), stream=out)

serializers.serialize("jsonl", Tag.objects.all())
{"model": "catalog.tag","pk": 1,"fields": {"name": "sci-fi"}}
{"model": "catalog.tag","pk": 2,"fields": {"name": "classic"}}

The same data in YAML (dumpdata catalog --natural-foreign --natural-primary --format yaml) and XML (dumpdata catalog.author --format xml --indent 2):

- model: catalog.author
  fields:
    name: Ursula Le Guin
    mentor: null
- model: catalog.book
  fields:
    title: The Dispossessed
    isbn: '9780061054884'
    author:
    - Ursula Le Guin
    tags:
    - - sci-fi
    - - classic
<?xml version="1.0" encoding="utf-8"?>
<django-objects version="1.0">
  <object model="catalog.author" pk="1">
    <field name="name" type="CharField">Ursula Le Guin</field>
    <field name="mentor" rel="ManyToOneRel" to="catalog.author"><None></None></field>
  </object>
</django-objects>

(The YAML output is trimmed to two objects.) Source: Serialization formats.

Deserializing

serializers.deserialize(format, data_or_stream, **options) returns an iterator of DeserializedObject instances. Each wraps a created but unsaved model instance in .object and its many-to-many data in .m2m_data; call .save() to write it. If the primary key is None, the manager’s get_by_natural_key() is used to look for an existing row, and a new row is inserted otherwise. The point of the iterator is that you can inspect each object and decide whether to save it.

from django.core import serializers

data = serializers.serialize("xml", Author.objects.all())
for item in serializers.deserialize("xml", data):
    print(item.object, item.object.pk, item.m2m_data)
    item.save()
Ursula Le Guin 1 {}
Octavia Butler 2 {}

Two options matter in practice:

  • ignorenonexistent=True skips fields (and models) that no longer exist. Without it, data naming a missing field raises DeserializationError: Error deserializing object: Tag has no field named 'nme'.

  • handle_forward_references=True is covered above.

Deserialization is not validation. DeserializedObject.save() writes the instance with raw=True semantics: full_clean() is not called, validators do not run, and save() of the model is bypassed. Run against [{"model": "catalog.tag", "pk": 99, "fields": {"name": ""}}], the loop saved a tag with an empty name even though the field is not blank=True. Never deserialize data from an untrusted source straight into the database; use a form, a DRF serializer or a Ninja schema (see Django REST Framework and Django Ninja) for input that crosses a trust boundary, and keep Django’s serializers for backups, fixtures and data you produced yourself. The Django serializers are not an API layer: they have no per-field output control beyond fields, no input validation and no nested representation.

A custom serialization format

A serializer is a module that exposes Serializer and Deserializer classes, registered under a format name with the SERIALIZATION_MODULES setting. Both classes usually subclass an existing format. Since Django 5.2 every built-in format defines a Deserializer class rather than a function, which makes extending one easier. This example registers a compact format that flattens each object to {"id": 1, "name": …​}, which is handy for feeding a small front-end widget.

catalog/compact_format.py
from django.core.serializers.json import Deserializer, Serializer as JSONSerializer

__all__ = ["Serializer", "Deserializer"]


class Serializer(JSONSerializer):
    """Flat JSON: {"id": 1, "title": ...} instead of model/pk/fields."""

    def get_dump_object(self, obj):
        data = super().get_dump_object(obj)
        return {"id": data["pk"], **data["fields"]}
config/settings.py
SERIALIZATION_MODULES = {"compact": "catalog.compact_format"}
python manage.py dumpdata catalog.author --format compact
[{"id": 1, "name": "Ursula Le Guin", "mentor": null}, {"id": 2, "name": "Octavia Butler", "mentor": null}]

Because Deserializer is the stock JSON one, this format is write-only: it cannot be fed back to loaddata, since the output no longer has the model and fields keys. A format that must round-trip needs a matching Deserializer, as in the CSV example of the reference. To register a format at runtime rather than in settings, use serializers.register_serializer(format, serializer_module).

Providing initial data

Early Django versions loaded a file named initial_data automatically after syncdb; that stopped for apps with migrations in Django 1.7 and was removed in 1.9, so any book or blog post that tells you to name a fixture initial_data.json is obsolete. Django has no automatic loader today. There are four supported techniques, which differ in when they run and in how safe they are to rerun.

Table 5. Choosing a way to provide initial data
Technique Runs Best for

Data migration (RunPython)

Once per database, as part of migrate, including the test database.

Reference rows the app cannot work without (statuses, countries, default tags).

post_migrate signal

After every migrate that touches the app.

Rows derived from other apps' state, such as groups and their permissions, which exist only after those apps' migrations.

Fixture + loaddata

When someone runs it (or from TestCase.fixtures).

Sample or demo data, and data copied between environments.

Management command

When someone or a deploy script runs it.

Idempotent seeding and refreshing, including environment-specific data.

A data migration

Migrations run when the test database is created, so rows created by a data migration are available to tests (with the limitation described under fixtures in tests). Write it as a RunPython operation with a reverse function, and always obtain the model through apps.get_model(): the real catalog.models.Tag may have changed since the migration was written, while apps.get_model() gives the model as it was at that point in the history.

catalog/migrations/0003_seed_tags.py
from django.db import migrations

DEFAULT_TAGS = ["fiction", "non-fiction", "reference"]


def create_default_tags(apps, schema_editor):
    # Use the historical model, never an import from catalog.models.
    Tag = apps.get_model("catalog", "Tag")
    db_alias = schema_editor.connection.alias
    for name in DEFAULT_TAGS:
        Tag.objects.using(db_alias).get_or_create(name=name)


def remove_default_tags(apps, schema_editor):
    Tag = apps.get_model("catalog", "Tag")
    db_alias = schema_editor.connection.alias
    Tag.objects.using(db_alias).filter(name__in=DEFAULT_TAGS).delete()


class Migration(migrations.Migration):
    dependencies = [("catalog", "0002_author_mentor")]

    operations = [migrations.RunPython(create_default_tags, remove_default_tags)]
python manage.py migrate catalog
python manage.py migrate catalog 0002
  Applying catalog.0003_seed_tags... OK
  Unapplying catalog.0003_seed_tags... OK

After the first command Tag.objects held ['fiction', 'non-fiction', 'reference']; the second removed them again. The historical model has no custom methods and no custom managers (unless use_in_migrations = True), which is why the example uses get_or_create on the plain manager. using(db_alias) keeps the migration correct in a multiple-database setup, and get_or_create makes it safe to rerun. See Migrations for RunPython, ordering and reversibility in depth.

A migration can also call call_command("loaddata", …​), but the fixture is loaded with your current models rather than the historical ones, so the migration breaks as soon as a later migration changes a model the fixture touches. Prefer RunPython for data that ships with the schema.

Source: RunPython and Data migrations.

A post_migrate handler

post_migrate is sent after migrate finishes for each app, even when there was nothing to apply. Connect the handler in AppConfig.ready() with sender=self so it runs once per migrate, not once per app, and make it idempotent. This is the same mechanism Django uses to create permissions and content types. Handlers use the current models, so they suit rows that cannot be created inside a migration, such as groups attached to other apps' permissions.

catalog/apps.py
from django.apps import AppConfig
from django.db.models.signals import post_migrate


def create_editor_group(sender, using="default", **kwargs):
    from django.contrib.auth.models import Group

    Group.objects.using(using).get_or_create(name="Editors")


class CatalogConfig(AppConfig):
    name = "catalog"

    def ready(self):
        from . import signals  # noqa: F401

        # sender=self: run once, after *this* app's migrations
        post_migrate.connect(create_editor_group, sender=self)

After deleting all groups, python manage.py migrate recreated ['Editors'].

A management command

For demo data, or data that differs per environment, write a command that creates or updates rows idempotently. It can be run from a deploy script, a Docker entrypoint or by hand, which makes it the most flexible option.

catalog/management/commands/seed_demo.py
from django.core.management.base import BaseCommand
from django.db import transaction

from catalog.models import Author, Book

DEMO = [
    ("Ursula Le Guin", "The Dispossessed", "9780061054884"),
    ("Octavia Butler", "Kindred", "9780807083697"),
]


class Command(BaseCommand):
    help = "Create or refresh the demo catalogue (idempotent)."

    @transaction.atomic
    def handle(self, *args, **options):
        for author_name, title, isbn in DEMO:
            author, _ = Author.objects.get_or_create(name=author_name)
            _, created = Book.objects.update_or_create(
                isbn=isbn, defaults={"title": title, "author": author}
            )
            self.stdout.write(f"{'created' if created else 'updated'} {title}")
python manage.py seed_demo
python manage.py seed_demo
created The Dispossessed
created Kindred
updated The Dispossessed
updated Kindred

Unlike a fixture, this command goes through the model’s save() and signals, so it also runs your business rules. See Management Commands & Shell for the command machinery and call_command.

Admin users and secrets

An initial superuser belongs in createsuperuser --noinput with DJANGO_SUPERUSER_USERNAME, DJANGO_SUPERUSER_EMAIL and DJANGO_SUPERUSER_PASSWORD taken from the environment or a secret store, not in a fixture. A fixture with a password hash is a credential in your repository. See Settings & Configuration for how to keep secrets out of source control.

Source: createsuperuser.

Fixtures in tests

A test case can name fixtures in its fixtures attribute. For TestCase, they are loaded once for the class, inside the class-level transaction, and each test starts from that state. A hand-written or dumped fixture is the oldest way to give tests data and it still works:

catalog/tests.py (fixture based)
from django.test import TestCase

from .models import Book


class FixtureTests(TestCase):
    fixtures = ["bookshelf"]

    def test_fixture_is_loaded_before_each_test(self):
        self.assertEqual(Book.objects.count(), 2)
        book = Book.objects.get_by_natural_key("9780807083697")
        self.assertEqual(book.author.name, "Octavia Butler")

Source: Fixture loading.

Fixtures work, but most teams find that they scale badly:

  • The data is far from the assertion. The test reads book.author.name == "Octavia Butler", but the reason for that value is in another file; nobody can tell which of the thirty fixture rows the test depends on.

  • Fixtures go stale. Add a required field to a model and every fixture that mentions it must be edited; a fixture is only validated when something loads it, and loading does not run model validation.

  • They are all or nothing. A fixture creates the same rows for every test, so tests that need one row pay for all of them and tests that need a variation need another fixture.

  • They couple tests to each other. Shared fixtures tend to accumulate rows that unrelated tests silently rely on; a change for one test breaks several.

  • Primary keys leak into tests. Without natural keys, tests assert against pk=1.

A factory builds an object in code with sensible defaults, and the test overrides only what it cares about. It is the approach the factory_boy library implements for Django through DjangoModelFactory (see Testing and pytest, Factories & Browser Tests for the full toolbox, and model-bakery as an alternative that infers the defaults from the model).

catalog/factories.py
import factory

from .models import Author, Book


class AuthorFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Author
        django_get_or_create = ("name",)

    name = factory.Sequence(lambda n: f"Author {n}")


class BookFactory(factory.django.DjangoModelFactory):
    class Meta:
        model = Book

    title = factory.Sequence(lambda n: f"Book {n}")
    isbn = factory.Sequence(lambda n: f"{9780000000000 + n}")
    author = factory.SubFactory(AuthorFactory)
catalog/tests.py (factory based)
from django.test import TestCase

from .factories import AuthorFactory, BookFactory


class FactoryTests(TestCase):
    @classmethod
    def setUpTestData(cls):
        cls.author = AuthorFactory(name="Ursula Le Guin")
        cls.books = BookFactory.create_batch(3, author=cls.author)

    def test_only_what_the_test_needs(self):
        self.assertEqual(self.author.books.count(), 3)

    def test_build_does_not_touch_the_database(self):
        book = BookFactory.build()
        self.assertIsNone(book.pk)

setUpTestData() runs once per class inside the class-level transaction, which keeps factory-built data as cheap as a fixture. Factory.build() makes an unsaved instance for tests that do not need the database at all, and a Sequence keeps unique fields (isbn, name) from colliding. The factory calls the model’s real save(), so validation-like logic and signals behave as in production, and adding a required field means changing one factory.

Fixtures still have a place: they are the natural way to test code that itself reads fixtures or serialized data, such as loaddata, and a small stable reference set can be convenient. For tests of serialization, round trip the data in the test, as below.

A serialization round trip test
from django.core import serializers
from django.core.management import call_command
from django.test import TestCase

from .factories import BookFactory
from .models import Author, Book


class RoundTripTests(TestCase):
    def test_serialize_then_deserialize(self):
        BookFactory.create_batch(2)
        data = serializers.serialize(
            "json",
            [*Author.objects.all(), *Book.objects.all()],
            use_natural_foreign_keys=True,
            use_natural_primary_keys=True,
        )
        Book.objects.all().delete()
        Author.objects.all().delete()
        for obj in serializers.deserialize("json", data):
            obj.save()
        self.assertEqual(Book.objects.count(), 2)

    def test_call_command_loaddata(self):
        call_command("loaddata", "bookshelf", verbosity=0)
        self.assertEqual(Author.objects.count(), 2)

All the test classes on this page ran with python manage.py test catalog (6 tests, OK).

TransactionTestCase and migration data

A TransactionTestCase does not wrap tests in a transaction; it flushes the tables after each test instead. That also removes the rows your data migrations created. In the example project a TransactionTestCase that ran after another one found 0 rows of the migration-seeded fiction tag. Setting serialized_rollback = True asks Django to restore the database from a snapshot taken when the test database was created; it brought the row back when that class ran first, but in the example project it raised an IntegrityError on django_content_type when it ran after another flushing class, and with the post_migrate group handler above it raised one on auth_group.name, because the flush re-sends post_migrate. So with TransactionTestCase, create the rows each test needs (with a factory, or a fixture through the fixtures attribute) rather than relying on migration data. Since Django 5.2, data from fixtures and from serialized_rollback is already available in TransactionTestCase.setUpClass().

Pitfalls

  • Dumping generated tables. Do not include contenttypes, auth.Permission or sessions in a fixture; use --exclude, or dump named models.

  • Excluding a dependency. loaddata --exclude catalog.tag bookshelf failed with DeserializationError: Tag matching query does not exist because the books refer to the tags by natural key. Excludes must respect references.

  • A fixture does not validate. Bad data loads; check it by loading it in a test.

  • Overwriting edits. loaddata replaces rows with the same key; never use a fixture to seed rows that users are allowed to edit unless you want that.

  • Default manager filtering. dumpdata uses the default manager; add --all for filtered ones.

  • Signals with raw=True. Receivers that read related objects must skip raw saves.

  • Natural keys that are not unique. get_by_natural_key() raising MultipleObjectsReturned (or DoesNotExist for a missing target) aborts the load.

  • Natural keys that change. If the key is something users can edit, such as a name, renaming a row makes old fixtures create a duplicate; use an immutable key such as an ISBN, a slug or a code.

  • Not a backup tool. A fixture is not a substitute for pg_dump: it does not carry sequences, indexes, permissions or triggers. See Databases for backups.

Where the books are outdated, and what changed recently

The books consulted for this section (see the bibliography) show fixtures as the way to seed data and to feed tests, and show natural keys with dumpdata/loaddata. The concepts are unchanged, but several details have moved.

Table 6. What is outdated
In the books Today

An initial_data fixture is loaded automatically.

Not loaded for apps with migrations since Django 1.7 and removed in 1.9. Use a data migration, post_migrate, a command or an explicit loaddata.

Test data comes from fixtures; Two Scoops of Django recommends factories.

Factories (factory_boy, model-bakery) are now the common default; keep fixtures for data that must be loaded as a fixture.

Data migrations can import the real model.

Use apps.get_model() for the historical model and using(schema_editor.connection.alias).

fixtures are enough for any serialized data.

Serializers are not validators; validate input at the boundary with forms or API serializers.

Table 7. What changed recently
Version Change

2.0

loaddata can read from standard input (loaddata --format=json -).

3.1

The JSON and YAML serializers, and therefore dumpdata, write Unicode as-is instead of escaping it.

3.2

JSON Lines (jsonl) serializer; loaddata reads XZ archives; dumpdata -o compresses to bz2, gz, lzma or xz.

5.2

Each serialization format defines a Deserializer class instead of a function. Fixture data and serialized_rollback data are available in TransactionTestCase.setUpClass().

6.0

The JSON serializer writes a trailing newline even without indent.

6.1

loaddata sends m2m_changed with raw=True; a model can opt out of natural-key serialization by returning () from natural_key(); the XML deserializer raises SuspiciousOperation for unexpected nested tags.