Each page in oTree can contain a form, which the player should fill out and submit by clicking the “Next” button. To create a form, first you need fields on the player model, for example:

class Player(BasePlayer):
    name = models.StringField(label="Your name:")
    age = models.IntegerField(label="Your age:")

Then, in your Page class, set form_model and form_fields:

class Page1(Page):
    form_model = 'player'
    form_fields = ['name', 'age'] # this means, player.age

When the user submits the form, the submitted data is automatically saved to the corresponding fields on the player model.

Forms in templates

In your template, you can display the form with:

{% formfields %}

Simple form field validation

min and max

To require an integer to be between 12 and 24:

offer = models.IntegerField(min=12, max=24)

If the max/min are not fixed, you should use {field_name}_max()


If you want a field to be a dropdown menu with a list of choices, set choices=:

level = models.IntegerField(
    choices=[1, 2, 3],

To use radio buttons instead of a dropdown menu, you should set the widget to RadioSelect or RadioSelectHorizontal:

level = models.IntegerField(
    choices=[1, 2, 3],

If the list of choices needs to be determined dynamically, use {field_name}_choices()

You can also set display names for each choice by making a list of [value, display] pairs:

level = models.IntegerField(
        [1, 'Low'],
        [2, 'Medium'],
        [3, 'High'],

If you do this, users will just see a menu with “Low”, “Medium”, “High”, but their responses will be recorded as 1, 2, or 3.

You can do this for BooleanField, StringField, etc.:

cooperated = models.BooleanField(
        [False, 'Defect'],
        [True, 'Cooperate'],

After the field has been set, you can access the human-readable name using get_FIELD_display, like this: player.get_level_display() # returns e.g. 'Medium'.

Optional fields

If a field is optional, you can use blank=True like this:

offer = models.IntegerField(blank=True)

Dynamic form field validation

The min, max, and choices described above are only for fixed (constant) values.

If you want them to be determined dynamically (e.g. different from player to player), then you can instead define one of the below methods.

Note: if you have apps written before May 2019, the recommended format for these validation methods has changed. See New format for form validation.


Like setting choices=, this will set the choices for the form field (e.g. the dropdown menu or radio buttons).


class Player(BasePlayer):

    fruit = models.StringField()

    def fruit_choices(self):
        import random
        choices = ['apple', 'kiwi', 'mango']
        return choices


The dynamic alternative to setting max= in the model field. For example:

class Player(BasePlayer):

    offer = models.CurrencyField()

    def offer_max(self):
        return self.budget

    budget = models.CurrencyField()


The dynamic alternative to setting min= on the model field.


This is the most flexible method for validating a field.

class Player(BasePlayer):

    offer = models.CurrencyField()

    def offer_error_message(self, value):
        print('value is', value)
        if value > self.budget / 2:
            return 'Cannot offer more than half your remaining budget'

    budget = models.CurrencyField()

Validating multiple fields together

Let’s say your form has 3 number fields whose values must sum to 100. You can enforce this with the error_message method, which goes on the page (not the Player model):

class MyPage(Page):

    form_model = 'player'
    form_fields = ['int1', 'int2', 'int3']

    def error_message(self, values):
        print('values is', values)
        if values['int1'] + values['int2'] + values['int3'] != 100:
            return 'The numbers must add up to 100'


  • If a field was left blank (and you set blank=True), its value here will be None.
  • This method is only executed if there are no other errors in the form.
  • You can also return a dict that maps field names to error messages. This way, you don’t need to write many repetitive FIELD_error_message methods (see here).

Determining form fields dynamically

If you need the list of form fields to be dynamic, instead of form_fields you can define a method get_form_fields:

def get_form_fields(self):
    player = self.player
    if player.num_bids == 3:
        return ['bid_1', 'bid_2', 'bid_3']
        return ['bid_1', 'bid_2']

But if you do this, you have to be sure to also include the same {% formfield %} elements in your template. The easiest way is to use {% formfields %}.


You can set a model field’s widget to RadioSelect or RadioSelectHorizontal if you want choices to be displayed with radio buttons, instead of a dropdown menu.

{% formfield %}

If you want to position the fields individually, instead of {% formfields %} you can use {% formfield %}:

{% formfield player.contribution %}

You can also put the label in directly in the template:

{% formfield player.contribution label="How much do you want to contribute?" %}


You can also do {% formfield 'xyz' %}, starting with oTree 3.1 (October 2020). This allows you to provide the field name as a variable and loop over form fields in different ways.

Customizing a field’s appearance

{% formfields %} and {% formfield %} are easy to use because they automatically output all necessary parts of a form field (the input, the label, and any error messages), with Bootstrap styling.

However, if you want more control over the appearance and layout, you can use manual field rendering. Instead of {% formfield 'my_field' %}, do {{ form.my_field }}, to get just the input element. Just remember to also include {% if form.my_field.errors %}{{ form.my_field.errors.0 }}{% endif %}.

Example: Radio buttons in tables and other custom layouts

Let’s say you have a set of IntegerField in your model:

class Player(BasePlayer):

    offer_1 = models.IntegerField(widget=widgets.RadioSelect, choices=[1,2,3])
    offer_2 = models.IntegerField(widget=widgets.RadioSelect, choices=[1,2,3])
    offer_3 = models.IntegerField(widget=widgets.RadioSelect, choices=[1,2,3])
    offer_4 = models.IntegerField(widget=widgets.RadioSelect, choices=[1,2,3])
    offer_5 = models.IntegerField(widget=widgets.RadioSelect, choices=[1,2,3])

And you’d like to present them as a likert scale, where each option is in a separate column.

(First, try to reduce the code duplication in by following the instructions in How to make many fields.)

Because the options must be in separate table cells, the ordinary RadioSelectHorizontal widget will not work here.

Instead, you should simply loop over the choices in the field as follows:

    <td>{{ form.offer_1.label }}</td>
    {% for choice in form.offer_1 %}
        <td>{{ choice }}</td>
    {% endfor %}

If you have many fields with the same number of choices, you can arrange them in a table:

<table class="table">
    {% for field in form %}
            <th>{{ field.label }}</th>
            {% for choice in field %}
                <td>{{ choice }}</td>
            {% endfor %}
    {% endfor %}

Raw HTML widgets

If {% formfields %} and manual field rendering don’t give you the appearance you want, you can write your own widget in raw HTML. However, you will lose the convenient features handled automatically by oTree. For example, if the form has an error and the page re-loads, all entries by the user may be wiped out.

First, add an <input> element. For example, if your form_fields includes my_field, you can do <input name="my_field" type="checkbox" /> (some other common types are radio, text, number, and range).

Second, you should usually include {% if form.my_field.errors %}{{ form.my_field.errors.0 }}{% endif %}, so that if the participant submits an incorrect or missing value), they can see the error message.

Raw HTML example: slider

If you want a slider, instead of {% formfields %}, put HTML like this in your template:

<label class="col-form-label">
    Pizza is the best food:

<div class="input-group">
    <div class="input-group-prepend">
        <span class="input-group-text">Disagree</span>

    <input type="range" name="pizza" min="-2" max="2" step="1" class="form-range">

    <div class="input-group-append">
        <span class="input-group-text">Agree</span>

If you want to show the current numeric value, or hide the knob until the slider is clicked, you could do that with JavaScript, but consider using the RadioSelectHorizontal widget instead.

Raw HTML example: custom user interface with JavaScript

Let’s say you don’t want users to fill out form fields, but instead interact with some sort of visual app, like a clicking on a chart or playing a graphical game. Or, you want to record extra data like how long they spent on part of the page, how many times they clicked, etc.

First, build your interface using HTML and JavaScript. Then use JavaScript to write the results into a hidden form field. For example:

# Player class
num_clicks = models.IntegerField()

# page
form_fields = ['num_clicks']

<input type="hidden" name="num_clicks" id="num_clicks" />

# JavaScript
document.getElementById('num_clicks').value = 42;

When the page is submitted, the value of your hidden input will be recorded in oTree like any other form field.


Button that submits the form

If your page only contains 1 decision, you could omit the {% next_button %} and instead have the user click on one of several buttons to go to the next page.

For example, let’s say your Player model has offer_accepted = models.BooleanField(), and rather than a radio button you’d like to present it as a button like this:


First, put offer_accepted in your Page’s form_fields as usual. Then put this code in the template:

<p>Do you wish to accept the offer?</p>
    <button name="offer_accepted" value="True">Yes</button>
    <button name="offer_accepted" value="False">No</button>

You can use this technique for any type of field, not just BooleanField.

Button that doesn’t submit the form

If the button has some purpose other than submitting the form, add type="button":

    Clicking this will submit the form

<button type="button">
    Clicking this will not submit the form

Miscellaneous & advanced

Form fields with dynamic labels

If the label should contain a variable, you can construct the string in your page:

class Contribute(Page):
    form_model = 'player'
    form_fields = ['contribution']

    def vars_for_template(self):
        player = self.player
        return dict(
            contribution_label='How much of your {} do you want to contribute?'.format(player.endowment)

Then, in the template:

{% formfield player.contribution label=contribution_label %}

If you use this technique, you may also want to use Dynamic form field validation.