Issue with parallel test runner on Python 3.14

I’m not sure if I’ve hit a bug or I’m holding it wrong. In my project I use a custom test runner which overrides a few Django settings (adds MD5 password hasher to PASSWORD_HASHERS for speed, stuff like that).

The test suite runs fine both sequentially and in parallel on Python 3.12. It also runs fine sequentially on Python 3.14. But when running in parallel on Python 3.14 it appears as if Django doesn’t see the settings I’ve set in the custom test runner.

A minimal reproducer:

  • create a new project with django-admin startproject fooproject
  • create two files with identical contents, fooproject/tests.py and fooproject/tests_more.py

Contents for both files:

from django.conf import settings
from django.test import TestCase
from django.test.runner import DiscoverRunner


class MyRunner(DiscoverRunner):
    def setup_test_environment(self):
        settings.WORLD = "World"
        super().setup_test_environment()


class MyTestCase(TestCase):
    def test_it_works(self):
        print(f"Hello {settings.WORLD}")

Run tests like so:

./manage.py test --parallel --testrunner fooproject.tests.MyRunner

This works on Python 3.12 (prints out “Hello World” twice), but on Python 3.14, produces an error: AttributeError: 'Settings' object has no attribute 'WORLD'

I am aware of #36531 (Add "forkserver" support to parallel test runner.) – Django but am unfamiliar with anything-multiprocessing and so not sure how to proceed.

Not sure exactly of the change there. It looks like a doozy.

But I’m going to point you here: Django settings | Django documentation | Django

Directly setting attributes isn’t a supported pattern. (c.f. Override settings and friends.)

As Carlton points out, what you’re trying to do is not supported.

But fwiw, DiscoverTestRunner.setup_test_environment() modifies settings in almost exactly the same way you are, so I wouldn’t expect the behavior you’re seeing. The one difference is it reads a setting before trying to write any. My best guess would be that LazySettings.__setattr__() doesn’t bootstrap settings like __getattr__() does, and that’s somehow causing confusion. You might try swapping the order of the super call and your settings override. (Also, if you’re modifying settings in setup_test_environment(), it’s good practice to restore them in teardown_test_environment(). Again, though, all of this is undocumented internals and likely to break unexpectedly.)

Also fwiw, here’s what I use to do similar test overrides, either directly in settings.py or in manage.py to swap in a test settings file. (And this approach is supported, or at least is recommended by a knowledgable Django developer.):

if sys.argv[1:2] == ["test"]:
    # Test-only overrides
    PASSWORD_HASHERS = [
        'django.contrib.auth.hashers.MD5PasswordHasher',
    ]
    # ...