# 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.

```python
# 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
```

```python
# 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:

```python
# 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"
```

```python
# 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

- [Django: `TestCase.setUpTestData()`](https://docs.djangoproject.com/en/stable/topics/testing/tools/#django.test.TestCase.setUpTestData)
- [Python: the copy module](https://docs.python.org/3/library/copy.html)
