Customizing the HTML name of a Django form field

Django uses the name of the form field as the name attribute of the corresponding form element in HTML. If you ever need to change it, here's how (Django 5.2+).

Default behavior

Let's have a look at what happens behind the scenes. We're using this form as an example:

class UploadForm(forms.Form):
    action = forms.CharField(max_length=30)
    file = forms.FileField()

A Django form keeps track of its fields internally, as self.fields; an ordered dict mapping the field name to the field instance. This allows us to alter the form fields for a specific instance without impacting the form definition. For example to remove a field because the current user isn't an admin.

Now suppose we're rendering our form manually in our template:

<form action="." method="post" enctype="multipart/form-data">
    {{ form.action }}
    {{ form.file }}
    <input type="submit" value="Upload">
</form>

The form instance doesn't have an action or file property. It still works because Django's template engine will try to resolve form["action"] next. The form will look up the form field in its self.fields registry and subsequently return its corresponding BoundField instance.

BoundField is the class that's responsible for everything related to turning a form field into HTML and selecting the corresponding data for that field. It doesn't matter whether your form is bound or not.

To clarify: a bound form means the form has data attached to it, typically the contents of request.POST.

You can see where we're heading: BoundField determines the actual value for the name attribute of the form control (i.e. the input, select, textarea,...).

Overriding the class used for bound fields used to require quite some effort in earlier versions of Django. From the 5.2 release on it's as easy as overriding the bound_field_class attribute of either your form or your form field.

Overriding bound_field_class

In our case we need the action field to use :action as the HTML name of the corresponding control. Here's how:

class ColonPrefixedBoundField(BoundField):
    def __init__(self, form, field, name):
        super().__init__(form, field, name)
        html_name = f":{name}"
        self.html_name = form.add_prefix(html_name)
        self.html_initial_name = form.add_initial_prefix(html_name)

class UploadForm(forms.Form):
    action = forms.CharField(
        max_length=30, 
        bound_field_class=ColonPrefixedBoundField,
    )
    file = forms.FileField()

If we render the form we now get this:

<input type="text" name=":action" maxlength="30" required id="id_:action">

Note that the id derives from the HTML name as well. This is perfectly fine; you are allowed to use colons in the id of an HTML element as long as it's not the first character.

A browser submitting this form will send a POST request with the :action and file keys and values. Because we changed the html_name of our bound field, and it's its responsibility to grab its value from the data, there's nothing we need to change about our form. Everything works as intended.

Our form field is still called action. Need to validate the contents or tweak the field? Use action, not :action. The name of the form control isn't relevant to the form. BoundField takes care of that.

But... why?

Because I'm implementing a basic API that needs to conform to external specifications –which means the name :action is required– and I don't feel like dragging Django Rest Framework or Django Ninja into this.