Repository: django/django
Issue category: Feature Request

Issue:
About the FormSet class name generated through the formset_factory.
Description
	 
When you define Django Form, what format do you usually name it?
In my case, I usually name it "xxxForm".
The names of the forms used as examples in official documents related to Django Form are the same as my name define format.
# Working with forms docs.
class ContactForm(forms.Form):
	subject = forms.CharField(max_length=100)
	message = forms.CharField(widget=forms.Textarea)
	sender = forms.EmailField()
	cc_myself = forms.BooleanField(required=False)
# Working with forms docs.
class ArticleForm(forms.Form):
	title = forms.CharField()
	pub_date = forms.DateField()
# FormField docs.
class CommentForm(forms.Form):
	name = forms.CharField()
	url = forms.URLField()
	comment = forms.CharField()
The way django people name it may be based on the developers' tendencies or tastes, but I think most of them are built in the format as xxxForm.
The FormSet class I created by passing xxxForm name format to the formset_factory is named as follows.
>>> ArticleFormSet = formset_factory(ArticleForm)
>>> ArticleFormSet.__name__
'ArticleFormFormSet'
The name of the FormSet class created through the formset_factory in the form xxxForm name format is a little strange.
This is because when formset_factory creates a class through type, it adds a static "FormSet" to the form name.
def formset_factory(
	form,
	formset=BaseFormSet,
	extra=1,
	can_order=False,
	can_delete=False,
	max_num=None,
	validate_max=False,
	min_num=None,
	validate_min=False,
	absolute_max=None,
	can_delete_extra=True,
	renderer=None,
):
	...
	return type(form.__name__ + "FormSet", (formset,), attrs)
I wonder if the format of these names is intended :)


Hints:
['I agree this is a bit odd. \u200bThe test for the Formset.__repr__() uses a form called Choice so the result is ChoiceFormSet as expected. As you pointed out, perhaps to match the documented convention of ending all form names with "Form" we could consider appending "Set" rather than "FormSet" if form.__name__ ends with Form. I can\'t think of any way this would break backward compatibility, and it might make debugging a little less confusing.', 1731782903.0]
['Thank you for your good opinion @Tim Graham ! It was not found because we tested it through Choice in the test. I agree to change "FormSet" to "Set".', 1731786303.0]
['\u200bPR', 1731789286.0]

Patch:
diff --git a/django/forms/formsets.py b/django/forms/formsets.py
index c8e5893f19b6..c2663154d4a5 100644
--- a/django/forms/formsets.py
+++ b/django/forms/formsets.py
@@ -570,7 +570,12 @@ def formset_factory(
         "validate_max": validate_max,
         "renderer": renderer,
     }
-    return type(form.__name__ + "FormSet", (formset,), attrs)
+    form_name = form.__name__
+    if form_name.endswith("Form"):
+        formset_name = form_name + "Set"
+    else:
+        formset_name = form_name + "FormSet"
+    return type(formset_name, (formset,), attrs)
 
 
 def all_valid(formsets):


Test patch:
diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py
index f80c1dc09e35..9f7012a11fd8 100644
--- a/tests/forms_tests/tests/test_formsets.py
+++ b/tests/forms_tests/tests/test_formsets.py
@@ -149,6 +149,12 @@ def test_basic_formset(self):
         self.assertFalse(formset.is_valid())
         self.assertFalse(formset.has_changed())
 
+    def test_formset_name(self):
+        ArticleFormSet = formset_factory(ArticleForm)
+        ChoiceFormSet = formset_factory(Choice)
+        self.assertEqual(ArticleFormSet.__name__, "ArticleFormSet")
+        self.assertEqual(ChoiceFormSet.__name__, "ChoiceFormSet")
+
     def test_form_kwargs_formset(self):
         """
         Custom kwargs set on the formset instance are passed to the