Personal blog of Miguel Araujo

Be careful how you use static variables in forms

Working on django-uni-form I came across with a weird situation and it took me some time to figure out what was going on. I wouldn’t say it was a bug, it was more a misuse or a documentation problem. After that, I realized other coders make the same mistake that was affecting us, so I thought about writing an article for warning Python/Django programmers.

If you are building a Django application in which you add attributes to a form, that are not form fields, like in this example:

class ExampleForm(forms.Form):
    description = forms.CharField(
        label = u"Short description",
        max_length = 40,
    )
    
    type_of_form = 'dangerous'

You should know that every attribute that is not a form field, such as type_of_form, is static or a class-wide variable. Let’s see what this means and please stay with me till the end, don’t switch channel. If you handle a form instance in one of your views and change type_of_form value:

form = ExampleForm(request.POST)
form.type_of_form = "secure"

Then type_of_form turned into an instance-level variable. Which means only that specific form instance has a type_of_form set to ‘secure’. You can still access the class wide variable, using the class name:

>>> ExampleForm.type_of_form
'dangerous'

Things get complicated if we turn type_of_form into a list. For example:

class ExampleForm(forms.Form):
    description = forms.CharField(
        label = u"Short description",
        max_length = 40,
    )
    
    type_of_form = []

If we work with the list without instantiating a new one, the list behaves like a Singleton, there is only one instance of that object. Now imagine someone posts the form once and we do:

form = ExampleForm(request.POST)

>>> id(form.type_of_form)
140281019424640

form.type_of_form.append(1)

type_of_form is now the list [1] for this and every future object we instantiate. So if a second person posts to the same view:

form = ExampleForm(request.POST)

>>> id(form.type_of_form)
140281019424640

>>> form.type_of_form
[1]

If you see type_of_form is not an empty list, it’s not instantiated again, we share the same object than before in memory, it persists. If we manually create a new list:

form.type_of_form = []

In that case we have created a class-level variable, but the class wide variable still exists and hasn’t changed:

>>> ExampleForm.type_of_form
[1]

Remember that next time we instantiate a form, unless we manually change the variable for a new object, type_of_form will be [1], It persists

form = ExampleForm(request.POST)

>>> id(form.type_of_form)
140281019424640

>>> form.type_of_form
[1]

Some of you, might be thinking, but everything in Python is an object, true. The only difference here is that if type_of_form is a string, as strings are immutable in Python, every time we manipulate it we create a new string, a new instance-level variable. But with a list or any other object, it’s very easy to get tempted to manipulate the class-wide variable, without instantiating a new one, if done without knowing what we do, it will provoke side effects.

Of course, this not a Django bug, this is how Python classes like Django forms work. The problem is many coders use Django forms this way without being aware of what they do. Therefore when doing this in your open source application, try to be clear in your docs and use static variables where due.

Real example

This is the way Django-uni-form advised to add helpers to a form:

class ExampleForm(forms.Form):
    [...]

    helper = FormHelper()
    helper.form_id = 'publishTransportSecondStep'

That made it easy for someone to manipulate the helper the wrong way, doing:

def view(request):
    # Create the form
    form = MyForm()
    helper.add_input(Reset('reset','reset button'))

See the problem? Now we have added to helper, which behaves like a Singleton, a new button. That button will be shown in every form of MyForm type we create. Every time we go into the view, a new button will be added, so the form will end up having tons of buttons. Thus we changed the advised way of adding helpers to forms.

This way, if we manipulate the helper in a form, we do it for that specific form helper. But sometimes people don’t manipulate helpers in views, or if they do, they do it the right way, thus, some people might want to have helpers as static variables, for improving performance and memory footprint.

Solution

Conclusion is that you should use static variables carefully and in the situations they should be used, always understanding what you do. If you don’t want a variable to be static, wrap it in the constructor and turn it into an instance-level variable:

class ExampleForm(forms.Form):
    description = forms.CharField(
        label = u"Short description",
        max_length = 40,
    )
    
    def __init__(self, *args, **kwargs):
        self.type_of_form = []
        super(ExampleForm, self).__init__(*args, **kwargs)

Note that turning type_of_form into a property will not solve the problem:

class ExampleForm(forms.Form):
    description = forms.CharField(
        label = u"Short description",
        max_length = 40,
    )
    
    @property
    def type_of_form(self):
        return []

If you do something like this:

form.helper.append(1)
form.helper.append(2)
>>> form.helper
[]

In this example, every time you call form.helper you get a new list, a new object in memory, so changes don’t persist. So wrapping a variable in a property should not be used as an alternative to instance-level variables.

Thanks to Jonathan Stoppani for his corrections and help.

blog comments powered by Disqus