Speed up your test suite

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_related results, 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.books runs 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_related caches; 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 setUpTestData are 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 deepcopy it in every test
  • Django forms: every form instance deep-copies its class's base_fields, so forms with many fields or long static choices lists 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

Updated October 18, 2026 · Suggest a change