How to have default/initial values in a Django form that is bound and rendered

10 January 2020   10 comments   Python, Web development, Django

Mind that age!

This blog post is 3 years old! Most likely, its content is outdated. Especially if it's technical.

Django's Form framework is excellent. It's intuitive and versatile and, best of all, easy to use. However, one little thing that is not so intuitive is how do you render a bound form with default/initial values when the form is never rendered unbound.

If you do this in Django:

class MyForm(forms.Form):
    name = forms.CharField(required=False)

def view(request):
    form = MyForm(initial={'name': 'Peter'})
    return render(request, 'page.html', form=form)

# Imagine, in 'page.html' that it does this:
#  <label>Name:</label>
#  {{ form.name }}

...it will render out this:

<label>Name:</label>
<input type="text" name="name" value="Peter">

The whole initial trick is something you can set on the whole form or individual fields. But it's only used in UN-bound forms when rendered.

If you change your view function to this:

def view(request):
    form = MyForm(request.GET, initial={'name': 'Peter'}) # data passed!
    if form.is_valid():  # makes it bound!
        print(form.cleaned_data['name'])
    return render(request, 'page.html', form=form)

Now, the form is bound and the initial stuff is essentially ignored.
Because name is not present in request.GET. And if it was present, but an empty string, it wouldn't be able to benefit for the default value.

My solution

I tried many suggestions and tricks (based on rapid Stackoverflow searching) and nothing worked.

I knew one thing: Only the view should know the actual initial values.

Here's what works:

import copy


class MyForm(forms.Form):
    name = forms.CharField(required=False)

    def __init__(self, data, **kwargs):
        initial = kwargs.get('initial', {})
        data = {**initial, **data}
        super().__init__(data, **kwargs)

Now, suppose you don't have ?name=something in request.GET the line print(form.cleaned_data['name']) will print Peter and the rendered form will look like this:

<label>Name:</label>
<input type="text" name="name" value="Peter">

And, as expected, if you have ?name=Ashley in request.GET it will print Ashley and produce this rendered HTML too:

<label>Name:</label>
<input type="text" name="name" value="Ashley">

UPDATE June 2020

If data is a QueryDict object (e.g. <QueryDict: {'days': ['90']}>), and initial is a plain dict (e.g. {'days': 30}),
then you can merge these with {**data, **initial} because it produces a plain dict of value {'days': [90]} which Django's form stuff doesn't know is supposed to be "flattened".

The solution is to use:

from django.utils.datastructures import MultiValueDict

...

    def __init__(self, data, **kwargs):
        initial = kwargs.get("initial", {})
        data = MultiValueDict({**{k: [v] for k, v in initial.items()}, **data})
        super().__init__(data, **kwargs)

(To be honest; this might work in the app I'm currently working on but I don't feel confident that this is covering all cases)

Comments

Haki Benita

Hey Peter, the behavior you implemented looks more like default than initial. You want to 1. Suggest a value for the user and 2. Select a value if the user did not provide it.

Peter Bengtsson

How do you implement `default`?

Haki Benita

I don't know why I thought there was a `default` attribute. You're right.. ;)

Anthony Ricaud

The solution comes down to merging the `data` and `initial` dicts, with a preference for the content of `data`. Using an idiomatic merge would outline this behaviour. https://treyhunner.com/2016/02/how-to-merge-dictionaries-in-python/ has a bunch of those.

```python
def __init__(self, data, **kwargs):
    initial = kwargs.get('initial', {})
    data = {**initial, **data}
    super().__init__(data, **kwargs)
```

Peter Bengtsson

That's neat! It's ultimately sugar syntax to my otherwise clunky loop where I merge `data` and `kwargs.get('initial')`. Thanks!

Ole Laursen

For a full solution, you need to query the value of each field. You can do this with .value() on an instantiated form. So you can instantiate a dummy form, and then go through the bound fields, calling .value() on them and then using that as data, something this:

    dummy_form = ...

    default_values = {}
    for bound_field in dummy_form:
        v = bound_field.value()
        if v is not None:
            default_values[bound_field.name] = v

Then

   SomeForm(default_values)

I think this works because the fields check the type of their input before cleaning. It's not exactly elegant, though, forms in Django are a bit rough around the edges.

Anonymous

This works, thx

Tonis

Thanks for the post, but all the solutions didn't sit well with me. Applying inital values in the __init__ worked, however on loading the form, it rendered with Errors where there were missing values. Which isn't acceptable, those errors should only show when it's been submitted at least once.

The solution was quite simple actually. The internal logic in the form is to ignore initial values if there is a data struct passed into the form. So just don't pass the request.POST or request.GET data. This can be done with just `form = MyForm(request.GET or None, initial={'name': 'Peter'})`. This way on inital load the form will have the inital values, but not think it's been submiteed so won't have errors about missing values. And will ignore the inital values on following submits as there is data there.

Peter Bengtsson

Thanks! I haven't looked back much since I implemented my solution. There's definitely a feeling that djangos form framework is very much assuming you'll render HTML and submit it regularly.

Mateusz Jastrzębski

I've learnt an interesting thing from developing with Django as a beginner a while ago - if the solution you think is obvious is not implemented then you're probably trying to work against a well developed and commonly agreed upon programming pattern, which is a good indication for me to perhaps do some more reading on the topic.

Best practices aside, I've run into a similar issue and while above philosophy and what Tonis said makes perfect sense, here's an alternative approach to merging initial and request data:

data = {
    **{
        name: field.initial
        for name, field in YourFormClass.declared_fields.items(
        )
    },
    **getattr(request, request.method).dict(),
}

The way it works is it merges initial values defined inside YourFormClass with data from the request. Downsides? Won't work with multiple valued keys. Probably others, too.

Your email will never ever be published.

Related posts