Patch dataclass default_factory bug

This test should pass, right?

# foo.py
from dataclasses import dataclass, field
from uuid import UUID, uuid4


@dataclass
class Foo:
    id: UUID = field(default_factory=uuid4)
# test_foo.py
from uuid import UUID
from unittest import TestCase, main
from unittest.mock import patch

import foo


class TestFoo(TestCase):
    @patch('foo.uuid4')
    def test_foo(self, uuid4):
        uuid = UUID('00000000-0000-0000-0000-000000000000')
        uuid4.return_value=uuid
        foo_ = foo.Foo()
        self.assertEqual(foo_.id, uuid)

if __name__ == "__main__":
    main()

Because I’m getting a ‘real’ UUID back (i.e. patch isn’t happening).

I also stumbled upon this from last year, unanswered and seem to be the same:

I can’t believe this is a bug, but?

Unfortunately its expected behavior, though a bit confusing in this context. The problem comes down to the time when you did the patch.

Consider this:

@dataclass
class Foo:
    id: UUID = field(default_factory=uuid4)

The issue is that when that class (not an instance of it) is created, it will take the reference to uuid4 and save it as the default_factory. This action happens at import time, before your patch would evaluate.

A way around this is to do something like this:

@dataclass
class Foo:
    id: UUID = field(default_factory=lambda: uuid4())

This works and passes the test case because the reference to the lambda is saved, but the uuid4 function is not evaluated until when the default_factory is called during instance creation… which happens to be after the patch occurs.

C:\Users\csm10495\Desktop\test>pytest -v
================================================= test session starts =================================================
platform win32 -- Python 3.11.0, pytest-7.2.0, pluggy-1.0.0 -- C:\Python311\python.exe
cachedir: .pytest_cache
rootdir: C:\Users\csm10495\Desktop\test
plugins: anyio-3.6.2, flaky-3.7.0, mock-3.10.0, repeat-0.9.1, xdist-3.3.1
collected 1 item

test_foo.py::TestFoo::test_foo PASSED                                                                            [100%]

================================================== 1 passed in 0.01s ==================================================

3 Likes

Argh, of course! Thank you for explaining, and providing a workaround. This will save me a lot of time :pray:

2 Likes