Repository: django/django
Issue category: Bug Report

Issue:
ErrorDict always uses default renderer
Description
	 
When BaseForm.full_clean() instantiates ErrorDict, it doesn't pass it the renderer:
​https://github.com/django/django/blob/1860a1afc9ac20750f932e8e0a94b32d096f2848/django/forms/forms.py#L319
Despite ErrorDict being ready to receive it and use it for rendering:
​https://github.com/django/django/blob/1860a1afc9ac20750f932e8e0a94b32d096f2848/django/forms/utils.py#L124
Practically, this means customizations to the renderer are ignored when rendering the form errors using {{ errors }} in the template.
Building on top of the example and fix from #35987, I have a custom renderer that swaps out some templates:
from django import forms
from django.forms.renderers import TemplatesSetting
from django.forms.utils import ErrorList
from django.template.exceptions import TemplateDoesNotExist
class CustomRenderer(TemplatesSetting):
	def get_template(self, template_name):
		if template_name.startswith("django/forms/"):
			# Load our custom version from "custom/forms/" if it exists
			our_template = f"custom/{template_name.removeprefix('django/')}"
			try:
				return super().get_template(our_template)
			except TemplateDoesNotExist:
				pass
		return super().get_template(template_name)
class CustomErrorList(ErrorList):
	def copy(self):
		# Copying the fix from Django Ticket #35987
		copy = super().copy()
		copy.renderer = self.renderer
		return copy
class MyForm(forms.Form):
	default_renderer = CustomRenderer()
	def __init__(self, *args, error_class=CustomErrorList, **kwargs):
		super().__init__(*args, error_class=error_class, **kwargs)
The custom error list template uses some CSS utility classes from Tailwind, like text-red-600:
{% if errors %}<ul class="text-red-600">{% for field, error in errors %}<li>{{ field }}{{ error }}</li>{% endfor %}</ul>{% endif %}
But creating a form with a non-field error and rendering the error dict uses the default renderer and its template:
In [1]: from example.forms import MyForm
 ...:
 ...: form = MyForm({})
 ...: form.full_clean()
 ...: form.add_error(None, "Test error")
In [2]: form.errors.render()
Out[2]: '<ul class="errorlist"><li>__all__<ul class="text-red-600"><li>Test error</li></ul></li></ul>'
I need to override full_clean() to set the renderer:
class MyForm(forms.Form):
	...
	
	def full_clean(self):
		super().full_clean()
		# Fix a bug in Django where self._errors = ErrorDict is not passed the
		# renderer argument when initialized.
		self._errors.renderer = self.renderer
Then form errors use my custom template:
In [1]: from example.forms import MyForm
 ...:
 ...: form = MyForm({})
 ...: form.full_clean()
 ...: form.add_error(None, "Test error")
In [2]: form.errors.render()
Out[2]: '<ul class="text-red-600"><li>__all__<ul class="text-red-600"><li>Test error</li></ul></li></ul>'
I think this has probably been an issue ever since a custom renderer became possible in #31026. The argument was added to ErrorDict but missed in BaseForm.full_clean(), the only place where the class is instantiated.


Hints:

Patch:
diff --git a/django/forms/forms.py b/django/forms/forms.py
index 549a3adf6fa6..614f99039585 100644
--- a/django/forms/forms.py
+++ b/django/forms/forms.py
@@ -316,7 +316,7 @@ def full_clean(self):
         """
         Clean all of self.data and populate self._errors and self.cleaned_data.
         """
-        self._errors = ErrorDict()
+        self._errors = ErrorDict(renderer=self.renderer)
         if not self.is_bound:  # Stop further processing.
             return
         self.cleaned_data = {}


Test patch:
diff --git a/tests/forms_tests/tests/test_forms.py b/tests/forms_tests/tests/test_forms.py
index cd909628cb03..d88ac33f24f0 100644
--- a/tests/forms_tests/tests/test_forms.py
+++ b/tests/forms_tests/tests/test_forms.py
@@ -5313,6 +5313,22 @@ class CommentForm(Form):
             "required></p>",
         )
 
+    def test_custom_renderer_error_dict(self):
+        class CustomRenderer(DjangoTemplates):
+            def render(self, template_name, context, request=None):
+                if template_name == "django/forms/errors/dict/default.html":
+                    return "<strong>So many errors!</strong>"
+                return super().render(template_name, context, request)
+
+        form = Form({}, renderer=CustomRenderer())
+        form.full_clean()
+        form.add_error(None, "Test error")
+
+        self.assertHTMLEqual(
+            form.errors.render(),
+            "<strong>So many errors!</strong>",
+        )
+
     def test_cyclic_context_boundfield_render(self):
         class FirstNameForm(Form):
             first_name = CharField()