setUpTestData deep copies and expensive copy.deepcopy
Django deep-copies attributes set in setUpTestData for every test that reads them. Big lists, prefetched instances and querysets make it slow. What to store instead.
Moving test data into setUpTestData creates it once per class instead of once per test. Since Django 3.2 there's a cost on the other side: every attribute you assign to cls there is deep-copied for each test that reads it. For a couple of model instances that's nothing. For a list of 500 objects, an instance with large prefetched relations, or a big dict of expected results, the copying can cost more than the queries you saved.
copy.deepcopy shows up in other places in test suites too, and the fix is the same idea: copy less.
Why setUpTestData copies
TestCase.setUpClass() runs setUpTestData(), then wraps every class attribute it added in a descriptor. The first time a test reads self.author, the descriptor deep-copies the original and stores the copy on the test instance, so later reads in the same test are free. All attributes in one test share a copy memo, so relationships survive: self.book.author is self.author stays true for the copies.
That's what lets a test change self.author.name without affecting the next test: database changes are rolled back by the transaction, in-memory changes are thrown away with the copy. The price is a deepcopy per attribute per test, and deep copies walk everything an object references:
- a model instance is copied with everything cached on it: related objects loaded with
select_related,prefetch_relatedresults, annotated values - a list or dict of instances is copied element by element
- a queryset is copied without its results, so a test that iterates
self.booksruns the query again, every test - an object holding a reference to something big (a client, a parsed document, a large config) copies that too
The fix: store less, and only what tests change
Keep what's assigned to cls small, and keep read-only data where Django doesn't copy it.
# Before: 500 instances with prefetched tags, copied for every test that reads them
from django.test import TestCase
from model_bakery import baker
from catalog.models import Product
from catalog.pricing import price_table
class PricingTests(TestCase):
@classmethod
def setUpTestData(cls):
baker.make(Product, _quantity=500, _bulk_create=True)
cls.products = list(Product.objects.prefetch_related("tags"))
cls.expected = price_table() # a large dict
# After: tests get what they need without copying the world
from django.test import TestCase
from model_bakery import baker
from catalog.models import Product
from catalog.pricing import price_table
EXPECTED_PRICES = price_table() # read-only, module level: never copied
class PricingTests(TestCase):
@classmethod
def setUpTestData(cls):
baker.make(Product, _quantity=500, _bulk_create=True)
cls.product = Product.objects.first()
cls.product_ids = list(Product.objects.values_list("pk", flat=True))
def test_cheapest(self):
cheapest = Product.objects.filter(pk__in=self.product_ids).order_by("price").first()
self.assertEqual(cheapest.price, EXPECTED_PRICES["min"])
Rules of thumb:
- Store model instances tests actually use, not whole collections. If a test needs all of them, query in the test or store primary keys (a list of ints is cheap to copy).
- Don't store querysets: they're re-run per test anyway. Store the instances or the ids.
- Don't store instances with big
prefetch_relatedcaches; prefetch in the test that needs it. - Put large read-only data (expected results, parsed fixture files, lookup tables) in module-level constants, or in the class body. Only attributes added by
setUpTestDataare wrapped; constants defined in the class body aren't copied.
Objects that can't be copied
Since Django 4.1, an object that doesn't support deepcopy (a lock, an open file, many network clients) raises TypeError when a test reads it. Create those in setUp, or at module level if they're safe to share.
Other expensive deepcopy in tests
When the time is in copy.deepcopy but not under setUpTestData, look at the lines flakytest points to. Common sources:
- tests that start from a big "base" payload or config and
deepcopyit in every test - Django forms: every form instance deep-copies its class's
base_fields, so forms with many fields or long staticchoiceslists cost something each time they're built, in tests and in production - pydantic's
model_copy(deep=True)and similar deep-copy helpers in the code under test
The fixes:
# Before: copy a large template for every test
import copy
from orders.events import handle
BASE_EVENT = {"type": "order.created", "items": [{"sku": f"SKU-{n}", "qty": 1} for n in range(1000)]}
def test_order_created():
event = copy.deepcopy(BASE_EVENT)
event["items"] = event["items"][:2]
assert handle(event) == "ok"
# After: build a small fresh object per test
from orders.events import handle
def make_event(**overrides):
return {"type": "order.created", "items": [{"sku": "SKU-1", "qty": 1}], **overrides}
def test_order_created():
assert handle(make_event()) == "ok"
If the data is never modified, don't copy it at all: share it, and make it immutable (tuples, frozen dataclasses, types.MappingProxyType) so a test that tries to modify it fails loudly instead of leaking state.
Keep the tests correct
The copy is what isolates tests from each other's in-memory changes, so only move data out of setUpTestData attributes if tests don't modify it. A test that changes a module-level constant changes it for every test that runs after it in that process. Run the suite in random order (pytest-randomly) after the change to catch that.
How flakytest finds it
flakytest puts time in copy.deepcopy in a copying category. When the time under Django's setUpTestData descriptor is at least 0.3 seconds and 1% of test time, it suggests setUpTestData attributes are deep-copied on every access, and lists the tests and lines that read the attributes. Otherwise, when deep copying in general is at least a second and 3% of test time, it suggests copy.deepcopy is expensive with the lines that call it.
Further reading
Related guides
- Create shared test data in setUpTestData instead of setUp: Django runs setUp before every test, so its rows are inserted again each time. setUpTestData creates them once per TestCase class, and every test still starts clean.
- Faster factory_boy and model_bakery factories: Slow factory_boy and model_bakery factories? Create shared data once, use build() or baker.prepare() instead of saving, bulk create rows, reuse one Faker instance and tune Hypothesis.
- Expensive function-scoped pytest fixtures: A pytest fixture without a scope runs again for every test that uses it. Give expensive fixtures module or session scope, or split them into a shared part and a cheap reset.
- JSON, YAML and pydantic serialization in tests: Tests that parse the same JSON or YAML files again and again, or validate pydantic models in loops, burn CPU. Parse once, use the C YAML loader, reuse TypeAdapters.
Updated October 18, 2026 · Suggest a change