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.
| Term | Meaning |
|---|---|
Serializer |
A class registered for a format name ( |
Fixture |
A named collection of serialized objects stored in files, found by |
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.
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.
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.
| Option | Effect |
|---|---|
|
Limits the dump to apps or models. Without labels every installed app is dumped. |
|
The output format; defaults to |
|
Number of spaces used to indent the output. Without it everything is written on one line. |
|
Excludes an app or an |
|
Uses |
|
Omits the primary key of objects that have a natural key, because it can be recomputed on load. |
|
A comma-separated list of primary keys. Only valid when dumping a single model; otherwise the command fails with |
|
Writes to a file instead of standard output. Ending the file name in |
|
The database alias to dump from; defaults to |
|
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.
# 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
Source: dumpdata and Fixtures compression.
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.
python manage.py loaddata bookshelf
Installed 6 object(s) from 1 fixture(s)
Source: loaddata.
| Option | Effect |
|---|---|
|
Fixture labels: a name ( |
|
The database alias to load into; defaults to |
|
Searches the |
|
The format of data read from standard input (required when the fixture name is |
|
Skips an app or |
|
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. |
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.
# 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)
Source: How fixtures are discovered and FIXTURE_DIRS.
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 examplegenres.yaml. If two files with the same name but different formats are found (mydata.jsonandmydata.xml.gz), installation aborts and everything installed by thatloaddatacall is rolled back. -
Fixtures can be compressed with zip, gz, bz2, lzma or xz:
loaddata mydata.jsonfindsmydata.json.gzand 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.jsonis only loaded when--database usersis given. See Transactions, Raw SQL & Multiple Databases.
Source: Compressed fixtures and Database-specific fixtures.
What loaddata does to the database
Three behaviours cause most surprises.
-
Rows are saved as they are. The model’s
save()is not called and no validation runs.pre_save,post_saveand (since Django 6.1)m2m_changedare sent withraw=True. -
Existing rows are overwritten. An object with the same primary key (or the same natural key) is updated, so rerunning
loaddatarestores the fixture values and discards manual edits to those rows. It does not remove rows that are missing from the fixture. -
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
IntegrityErrorand 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.
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(oruse_natural_foreign_keys=True) then writes references to that model as the tuple, anddumpdata --natural-primary(oruse_natural_primary_keys=True) omits the model’s ownpk.
The models in The example models implement both. The same books, dumped 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:
[
{"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.
Source: Natural keys and forward references.
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.
| Format | Notes |
|---|---|
|
The default. Encoded with |
|
JSON Lines: one object per line, convenient for streaming and large files. Added in Django 3.2. |
|
A simple XML dialect ( |
|
Available only if PyYAML is installed. Like JSON, it writes Unicode as-is since 3.1. |
|
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"}}
Source: Serializing Django objects and Subset of fields.
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=Trueskips fields (and models) that no longer exist. Without it, data naming a missing field raisesDeserializationError: Error deserializing object: Tag has no field named 'nme'. -
handle_forward_references=Trueis 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.
Source: Deserializing data.
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.
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"]}
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).
Source: Custom serialization formats.
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.
| Technique | Runs | Best for |
|---|---|---|
Data migration ( |
Once per database, as part of |
Reference rows the app cannot work without (statuses, countries, default tags). |
|
After every |
Rows derived from other apps' state, such as groups and their permissions, which exist only after those apps' migrations. |
Fixture + |
When someone runs it (or from |
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.
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.
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'].
Source: post_migrate and AppConfig.ready().
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.
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.
Source: Writing custom django-admin commands and update_or_create().
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:
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).
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)
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.
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().
Source: Rollback emulation and Django 5.2 release notes: Tests.
Pitfalls
-
Dumping generated tables. Do not include
contenttypes,auth.Permissionorsessionsin a fixture; use--exclude, or dump named models. -
Excluding a dependency.
loaddata --exclude catalog.tag bookshelffailed withDeserializationError: Tag matching query does not existbecause 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.
loaddatareplaces 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.
dumpdatauses the default manager; add--allfor 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()raisingMultipleObjectsReturned(orDoesNotExistfor 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.
| In the books | Today |
|---|---|
An |
Not loaded for apps with migrations since Django 1.7 and removed in 1.9. Use a data migration, |
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 |
|
Serializers are not validators; validate input at the boundary with forms or API serializers. |
| Version | Change |
|---|---|
2.0 |
|
3.1 |
The JSON and YAML serializers, and therefore |
3.2 |
JSON Lines ( |
5.2 |
Each serialization format defines a |
6.0 |
The JSON serializer writes a trailing newline even without |
6.1 |
|
Source: Django 6.1 release notes and Django 5.2 release notes.
Related pages
-
Models & Fields for the models that get serialized, and Managers & Custom QuerySets for
get_by_natural_key()and default managers. -
Migrations for data migrations in depth.
-
Management Commands & Shell for
dumpdata,loaddataand custom commands. -
Testing for the test client, factories and
setUpTestData. -
The Admin Site for curating the data a fixture seeds.