Django's Triage and Review Team
Published November 11, 2025
This video features David Smith at DjangoCon Europe 2023 in Edinburgh, Scotland.
Good form: How Django’s form rendering improved during the 4.x series
by David Smith
If you render forms server side then this talk is for you! How to make best use of Django’s form rendering features introduced during the 4.x series to help you simplify your form logic.
Historically Django has used concatenation of strings to render its forms with various as_* methods to render forms in different styles. While great to get a project up and running many folk will have used a third party library to help ease customisation of forms.
During the 4.x series a number of changes have been made including a switch to use the template engine to render forms, the ways in which the template can be set has grown and a new default style will replace as_table from Django 5.0.
We’ll cover:
⁃ What is a form, a field and a widget? What common attributes can I set to avoid logic in my template?
⁃ The switch to template based rendering, and that a template can now be used to render your form. We’ll work through a concrete example how this can be used simplify logic in your templates.
⁃ How the rendering of your form can be set on a per-project, per-form and per-instance basis.
⁃ Introduction of a new as_div template style, and why the other styles are no longer recommended.
⁃ Future ideas on how Django can ease form rendering further.
⁃ So with all this change, do I still need 3rd-party package such as crispy-forms?
May your form be good!
Django’s form rendering became more flexible and accessible during the 4.x series. Django 4.1 introduced the recommended `as_div` rendering style, with fieldsets and legends for grouped inputs, while Django 4.0 and 4.1 added per-form, per-instance, and site-wide templates, plus helpers such as `legend_tag` and `use_fieldset`. David Smith shows how to reduce template repetition by configuring labels and help text on fields, looping over bound fields, and choosing custom renderers, while explaining that Crispy Forms and similar packages still provide Python-based layouts, template packs, and conditional widget classes; Django 5.0 further adds field-level rendering templates and `as_field_group`.
Summarised automatically from the transcript.
Automatically transcribed, so expect mistakes in names and technical terms.
David.
Speaker 1: Thank you very much for all being here. So a bit of introduction. So yeah, I'm David Smith. I'm a maintainer of Django Crispy Forms and I've been contributing to Django itself since uh 2020 And through that work I've become a member of Django's triage and review team. This is the team that helps progress PRs to Django itself. And just during this time it's just wonderful the community, really supportive. I just think it's amazing that so many people can gather around a tool and organise events like this. It's um yeah, really great. I'm Smith DC1 at GitHub and David Smith at fosterdone. org.
Speaker 1: So today what are we going to cover? We'll start about talking about the anatomy of form, the different parts that make up a Django form, and the different words that Django uses to describe those elements. We 'll then take an example form and see how we can ease the form rendering of that using the new features that we added during the Django 4 series. And one of the questions that we've been asked off the back of this is so with all these new features, do I still need my third-party library? Do I still need to use Crispy Forms? So we'll have a look at that. And then towards the end, we'll have a look towards the future and see
Speaker 1: what features might be added in the future and what else we could think about. So let's go So we'll start off with a small sample form. So maybe it's a contact form where we want to collect the first name and last name, and maybe the day that you want to book an appointment for. So we create our um form um by subclassing uh Django 's forms. form And there we can define our fields. So we've got a first name and last name field, which are both character fields, and our day field, which is a choice field there. And if we uh just call string
Speaker 1: on that form, it'll be rendered in a template. And we'll get the whole form, uh including all the fields and all the inputs and all the widgets. Diving down to the next layer down, we have fields, so a form can have many fields And just looking at that day field here, we have a choice field which has choices. And we've also defined a custom widget here. So we want it to be a checkbox um field so people can uh select different data that they may want to um book an appointment for. So when we render um that in our template
Speaker 1: That field becomes the the label, the label suffix, which is by default the the colon, and the widgets of those input elements. And one thing to be aware of is that the field that we define on our form becomes a bound field when we uh render it in our template. This is something that took me a little while to um to realize and uh that When the penny dropped, it really opened things up for me. So Django's definition of a bound field is a field plus data. So a good example of this is maybe a user submits their form and it's invalid. So we want to render that form again with um the errors in those previously submitted values.
Speaker 1: We don't know those when we're defining the um the field on our form, we only know about those at um render time. And then finally we have our widget. So a widget is Django's representation of a HTML input element. Each field has a default widget, which you can customize per field. So here we've customized the widget for the choice field to be that checkbox input that we saw. So the widget is just those input elements. And for choice widgets,
Speaker 1: you have a concept of subwidgets to give you each option within that overall widget. So our first name and last name, the widget is just the input box, but for this choice widget, it's the collection of the pods. So by default, um when we call string on our form, we'll have something that that looks like this. And this is a form that's rendered in uh table tags. That's the the default that Django currently um ships with. That may not be what your your project uh needs. So historically Django's shipped another couple of options, so as UL and as P for um rendering the form as a unstructured unordered list or um as paragraph
Speaker 1: tags. Probably the as p is probably the most popular of these. And the accessibility team reviewed the uh the styles that um Django ships with a little while ago. Um, mainly Thibaut, who's running the um the workshop next door And the view was from an accessibility perspective, none of these styles are really that suitable and accessible for screen reader users. And the recommendation from the accessibility team was to introduce a new template that renders the forms in div tags. In Django 4. 1, we introduced asDiv template with the associated um methods.
Speaker 1: So from Django 4. 1 you can call AsDiv um and that's got the improved accessibility features. So it renders um It implements field sets and legends to relate grouped um to group related inputs. So here we've got the that those checkboxes grouped so um they're easier for screen reader users to navigate. At this point there was some discussion about these older methods. Should we keep those around? Should we remove them because they're not really recommended? And after a bit of discussion, we came to the conclusion that while they're not recommended, we won't remove them from the core Django library. But in the documentation we will recommend the
Speaker 1: asDiv. So all the docs in Django will now use AsDiv for its examples. and it will recommend those over the traditional styles. Um so Django uh wants to ship sensible defaults. So as div will become the the default um in Django 5. 0. Um and we've um deprecated the um the old um style. So to ease um transition, there's a transitional renderer that got introduced in Django
Speaker 1: 4. 1. And you can implement this by in your settings. py file, setting the form renderer searching to the that transitional renderer. And if you need to retain the previous behavior of it being rendered as a table, you can create a custom form renderer. setting the the form template name and the for form set template name settings um to those um those um those template names uh directly So by default, Django will ship as div template, and maybe you can uh stall those um directly and just use the out-of-the-box template.
Speaker 1: So yesterday Carlton talked about um Neapolitan. So I think that project is using the AsDiv templates that are straight out of the box and just styling those using the Tailwind classes. But maybe you need a bit more customizability. Maybe for example the requirement is to have a bit of margin between each of those fields. Django gives us lots of flexibility to unpack each of the elements that make up our forms. So here we've got an example where we do exactly this, we've got a bit of margin between each of our elements And we can access each
Speaker 1: bound field on our form through a dotted name attribute. So form. lastname gives us the bound field for the last name field on that form. And that bound field knows everything that you need to know about rendering a uh a field in Django. So we're able to call the the label tag to give us the label, errors for errors, and if we call string directly on the field itself, that actually gives us the the field's widget, so the input element there So we can do that for the last name and the first name. But for that day, we need to do something a little bit different because we need to implement field set and uh legend tags.
Speaker 1: So we can write those out ourselves in our template, and maybe we want to add a bit of help text as well at this point to give a prompt to the users of what they should be doing. So this is great. We can uh really customize the the form layout in our project, but it's quite verbose. I think um folk would just want to call form. to render their their their form. So let's see how we can we can improve this. So the first thing I would recommend folk do is use all the field arguments. So you can set help text. and labels and and uh label suffixes um on your field in your form definition.
Speaker 1: So here we can take that custom logic that's currently sitting in our template. and define it on the field instead. And then in our template we can access that through the accessing the help text attribute on that bound field instead. So we're starting to um generalize the the the template logic. So this has been around for for for some time. This is um not not new capability, but um what is new In Django 4. 1, we introduce the legend tag. So traditionally there's a label tag which will render your um your field uh label in in label tags but for um field sets we need to do the same thing but in in legend tags.
Speaker 1: So um this got added in in for one Those that are kind of eagle-eyed and see what go, that's that 's great, but what about that that field set? What can we do about that? So we introduced a useField set um method in in in Django four one And that's available on the bound field. And what this does, it looks at the the widget that's defined on that field. and works out if that's a widget that needs to implement field and and legend to relate um those um inputs together. So if you need to use the field set, then render it with uh a field set and a legend tag. Otherwise, we can just use the
Speaker 1: additional label tag. All of the default widgets that come with Django itself have this setting set. where required, but maybe if you've got a custom widget you may need to just review that attribute on your widget to make sure people can make use of that in their in their forms So now we're starting to get somewhere where we can um we've generalized the the template logic. And rather than writing out each field individually and each element on each field, we can instead loop over each field in our form and render each field in exactly the same way.
Speaker 1: So suddenly we can um shrink the the amount of um logic that we've got in our templates to uh render our form. Um and one tip here is that um Django will loop over those fields in your form in the same order that they're defined on your your form or your model. But maybe in your um in your HTML you want them ordered in a different way. So you can use the the field order attribute on your form and be able to customize the um the order that that those have rendered in um so you can still make use of um looping over uh over the fields
Speaker 1: So this is great. We've now got a a shorter form. shorter form logic, but we've still got to write that everywhere. And maybe we've got the same form in multiple pages in our project. We just want to get back to writing form. So what can we do? We can extract that form logic into its own template and Django 4. 0 switch form rendering to use the template engine. So all we need to do is define the template name on our form. And now when we call string on our form, it'll use that template name to render that that form.
Speaker 1: Also in in Django four zero You're able to customize that um that template on a per instance. So maybe you have a view. And depending on some logic that's in your view, you may want to render that form with a different template So you're able to call the render method directly on the form and provide it with a with a template name directly. So at this point, you're able to define a template for your form and use that per instance. Or per form. But maybe you've got a
Speaker 1: form template now that you can use for every form in your project. You would still have to define that template name on every um. form in your in your project. So in Django 4. 1 we added the ability to set a site-wide form template. And the way this works is you create a custom form renderer, and you can define the form template name site-wide on this form renderer. And it also works for form sets as well. So here we create a custom form renderer by subclassing the template setting and set the um the location of those templates on those settings and in our
Speaker 1: settings. py file, give it the location of that custom form renderer So now we can set a site-wide template on the form renderer in our settings. We can override that on a per form basis by say defining the template name on the form or we can define it per instance in our view. So just to recap the first part of the talk , step one, move as much logic as you can out of the template onto your form. Step two, use new features to help uh ease writing of the template logic, so uh the the legend tag and the use field sets Loop over those fields to reduce repetition and then use those templates and throughout your project.
Speaker 1: One final thing is that the um the switch to template rendering um goes even deeper. There's a an ability to customize the the template for the labels using the the template name attribute on your form and also the customize how errors are rendered by defining a custom error class and using that on your your form as well. So now we can set Templates on my form. Do I still need CRISPRs? Do I still need my my third-party package? I think it depends. So
Speaker 1: um I'll talk about Crispy Forms because I know uh a bit more about Crispy Forms. So Crispy Forms comes with some uh additional features that may be useful So you can define your form layout in Python code. It's a little bit crazy, but but but folks seem to like it. So rather than um writing out all your um template logic in in HTML. Here we're able to have two um two inputs side by side, so maybe um name and email in two columns and they're on the same row. And we can write that in in in um Python code. And CRISPRM will render that using the the templates that are assigned to each of those those classes.
Speaker 1: You can do dynamic form layouts, so you can pick part of a form and you can update the attributes on part of that form. You could select all of the widgets of a certain type so here we can select all of the password widgets on our form and just add certain classes. on that form. And finally the template packs. These are really powerful. So if the bootstrap template packs they come with uh lots of additional custom layer top jucks, so you can define modals, bootstrap floating fields, bootstrap alerts, accordions, and so on. And you can build out
Speaker 1: your form using these bootstrap components in the using that layout structure that we saw above. So you can get quite a lot of template logic. A lot easier using these um components. One other feature that I find quite useful and this is shared both by Django widget tweaks and CRISPYforms is the ability to add CSS classes to your widget in template logic. So of course you can define the classes for your widget. So by defining a atters dict on your widget that's on your field, that's on your form.
Speaker 1: But maybe you don't know what class you need to add at that point. So in this example at the bottom we might have a bootstrap form and we need to add the is invalid class so we get the red box around the input field and highlights the um the error text um in red as well. We only know that once the validation logic has been run in that form. So once that field is bound. So here we can say Yeah, if the fields has an error, then add the um the classes required to um style that field as as an error. Otherwise we can just render it the default styles.
Speaker 1: This is something that's available in these third-party packages, but I'm not aware of a way in Django itself to be able to add CSS classes on the fly. Maybe you could do it in your view. Again, Carlton was talking yesterday about having kind of similar concepts grouped together. I'm not sure that adding CSS classes in views. Is a good answer. I'd rather see CSS classes in the template because I see CSS classes and um HTML as kind of being the the same thing. And finally, some of the template logic can get quite complex. You won't be able to read that, but that's kind of deliberate. This is just the
Speaker 1: field template from the Bootstrap template pack. And by the way, this doesn't cover checkboxes or radios or file fields. These are all there at their own templates as well. So Some of the bootstrap logic can get quite complex because depending on what type you have, it's not just the classes that change, it's also the structure of the HTML itself as well. So, what about the future? So far we've talked about treating each field in the form exactly the same, looping over them one by one. But maybe somebody comes along and goes, that's great, I really love this form.
Speaker 1: But could you just um put the first name and last name next to each other rather than underneath each other each underneath each other? Suddenly all that hard work will be undone. You'll have to maybe write something like this, which unpacks each of those elements so you can um put maybe the the email and the password fields um inside um two dibs for for the column and those nested within a row This is much more um more difficult to maintain. Maybe what we would like is um make that bound field renderable. So rather than the former able to um render the field.
Speaker 1: So here we have an asField group method on the bound field, and that renders the bound field with its label, its errors, its help text, and and so on. And this is I believe much more manageable of laying out the the fields on your form. I'm going to refer to Carlton's talk again yesterday. He was talking about using HTMX and maybe we just want to render a field on its own rather than the whole form. This would be a template just for a field So we've added this feature in Django 5. 0. So you'll be able to define this in a similar sort of way as the other template.
Speaker 1: So field template name on your form renderer for site-wide. and using the template name argument when you define your your field on your form for per um per field um templates as well Anything else? This one's a little bit uh bit bit out there, but something that we discussed is calling string on on field actually um gives you the the widget rather than the the field. But this has literally been like it forever. So it would be quite a breaking change and we probably need a really strong consensus among the community to change this at this point in time. Thank you.
Speaker 2: Thank you, David. Great. So I think we have time for um a few questions. Anyone has got a question? There's two microphones to the left and to the right. Oh the other way around and there's one walking around. Um
Speaker 3: thanks for the talk. I'm wondering if you change something project wide. Um what will happen with the admin interface? Are we overriding everything there or are there still will we get then a bootstrap template in the admin all of a sudden or how does this work
Speaker 1: Uh yeah, good question. So the um the admin templates um are um quite bespoke and custom to use the the admin um I guess classes in the admin site. I've looked at can we use some of these features in the admin? I'm not quite sure it's so so so simple given the the complexity of the the admin templates.
Speaker 2: There's more one more question again.
Speaker 4: Thank you. Um I'm just wondering from a talk I saw the other day about uh excessive use of area labels in Django forms by default, is that getting better now?
Speaker 1: Uh yeah, good question. Um I can't quite remember where we we we got to, but um there's definitely a couple of tickets open to um add those for things like errors and and help text and we've made some improvements um to the use of the um the four labels to link the um the inputs and the the labels together
Speaker 4: So yeah they're available but they're not like overused by default. That was the concern I think expressed. Okay. I'll look into it so thank you.
Speaker 2: Okay. Thank you everybody. Um thank you David especially. Great talk.
A bound field is a form field combined with submitted data. It lets Django render values and validation errors that are only known when the form is being rendered, such as after an invalid submission.
Discussed at 3:13Django 4.1 introduced the `as_div` rendering style, which uses divs along with fieldsets and legends to group related inputs more accessibly for screen-reader users. It is recommended over the older table, list, and paragraph styles and became the default in Django 5.0.
Discussed at 6:20Access each field as a bound field and render its label, errors, help text, and widget separately; for grouped controls, use the fieldset and legend helpers. You can then loop over the form’s fields to avoid repeating the same rendering logic, and use `field_order` when the HTML order should differ.
Discussed at 9:26Since Django 4.0, a form can specify a template name, or a view can pass one to the form’s render method for per-instance customization. Since Django 4.1, a custom form renderer can define a site-wide form template, with per-form and per-instance templates still able to override it.
Discussed at 14:06It depends on the project: Django now handles much more of the basic form rendering, but Crispy Forms still adds Python-defined and dynamic layouts, template packs such as Bootstrap components, and convenient conditional CSS classes. Those features can make a third-party package worthwhile for complex layouts or framework-specific styling.
Discussed at 18:05Django 5.0 adds a field-group rendering feature that can render a bound field together with its label, errors, help text, and other surrounding markup. This makes it easier to arrange fields into custom rows and columns or render one field independently, such as for an HTMX update.
Discussed at 23:33Note: We understand that names change, people change, and bodies change. We respect each individual's journey and privacy. If you have any concerns about a video or need us to remove content, please don't hesitate to contact us. We will handle your request with care and promptly address any issues.
Published June 13, 2025
Published June 13, 2025
Published June 13, 2025
Published June 13, 2025
Published June 13, 2025
Published June 13, 2025