Why are fields in ManagementForm dynamically populated?

I do a lot of static analysis on my Django codebase to produce TypeScript types. While doing so for formsets, I noticed ManagementForm creates its fields in the init method. But I can’t see any reason why these are declarative and static. Here is the relevant code:

# special field names
TOTAL_FORM_COUNT = 'TOTAL_FORMS'
INITIAL_FORM_COUNT = 'INITIAL_FORMS'
MIN_NUM_FORM_COUNT = 'MIN_NUM_FORMS'
MAX_NUM_FORM_COUNT = 'MAX_NUM_FORMS'
ORDERING_FIELD_NAME = 'ORDER'
DELETION_FIELD_NAME = 'DELETE'

# default minimum number of forms in a formset
DEFAULT_MIN_NUM = 0

# default maximum number of forms in a formset, to prevent memory exhaustion
DEFAULT_MAX_NUM = 1000


class ManagementForm(Form):
    """
    Keep track of how many form instances are displayed on the page. If adding
    new forms via JavaScript, you should increment the count field of this form
    as well.
    """
    def __init__(self, *args, **kwargs):
        self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
        self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput)
        # MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT are output with the rest of
        # the management form, but only for the convenience of client-side
        # code. The POST value of them returned from the client is not checked.
        self.base_fields[MIN_NUM_FORM_COUNT] = IntegerField(required=False, widget=HiddenInput)
        self.base_fields[MAX_NUM_FORM_COUNT] = IntegerField(required=False, widget=HiddenInput)
        super().__init__(*args, **kwargs)

This could very well just be:

class ManagementForm(Form):
    TOTAL_FORMS = IntegerField(widget=HiddenInput)
    INITIAL_FORMS = IntegerField(widget=HiddenInput)
    # MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT are output with the rest of
    # the management form, but only for the convenience of client-side
    # code. The POST value of them returned from the client is not checked.
    MIN_NUM_FORMS = IntegerField(required=False, widget=HiddenInput)
    MAX_NIM_FORMS = IntegerField(required=False, widget=HiddenInput)

It’s not a major issue, but it does prevent static analysis and eventually proper typing in Django stubs if auto.

If you can’t find a reason in Git blame or similar, feel free to open a ticket and a PR changing it. By changing it in Django you can also see if any tests fail, which might reveal why.

I was curious so I did what Adam suggested and it turns out making that change triggers a ton of test failures in Django’s test suite (176 to be exact).

It’s not immediately obvious to me why, but it probably has something to do with the fact that ManagementForm.__init__ adds fields to self.base_fields and not to self.fields.

EDIT Actually the reason for all these test failures is that I put the wrong name for the fields (I used TOTAL_FORM_COUNT insead of TOTAL_FORM for example).

I suspect the weirdness here is so that we can have the field names in module-level variables and reference them in other places.
It’s still weird to me that it’s modifying base_fields instead of fields: there’s a comment in BaseForm.__init__() saying not to do that.

Yes that is weird. I see that as reason enough to change.

I made a PR with this change if you’d like to review: Moved ManagementForm's fields to class attributes. by adamchainz · Pull Request #15171 · django/django · GitHub

@adamchainz just catching up here. Thanks Adam, this is very helpful.

I suspect it was originally used to be able to refer to the constants in other places and ensure the field names were the same.