In the past few months, oTree has many new features, because of oTree Lite, the new no-self format, and various other improvements that were made.

oTree Lite can still run apps written in the 3.x format. However, if you are starting a new project, it's recommended to use the newest syntax.

Old vs new format

Some of these changes may look arbitrary, but in each case the change addresses a specific issue, either enabling new use cases, preventing certain errors, or making the functionality clearer or more consistent.

This table is also useful for new learners of oTree who are trying to understand code written before 2021.

Description Old format (still works) New format (recommended) More info Comment
App folder
dictator/
    _builtin/
    templates/
        dictator/
            Decide.html
            Results.html
    __init__.py
    models.py
    pages.py
dictator/
    __init__.py
    Decide.html
    Results.html
More info
Import statements
# models.py
from otree.api import (
    models,
    widgets,
    BaseConstants,
    BaseSubsession,
    BaseGroup,
    BasePlayer,
    Currency as c,
    currency_range
)

# pages.py
from otree.api import (
    Currency as c, currency_range
)    
from ._builtin import Page, WaitPage
from .models import Constants
from otree.api import *
More info
Template preamble
{% extends "global/Page.html" %}
{% load otree %}
(not needed)
More info
Template tags
{% if XYZ %}…{% endif %}
{% for x in XYZ %}…{% endfor %}
{% formfields %}
{% next_button %}
{{ player.xyz }}
{{ if XYZ }}…{{ endif }}
{{ for x in XYZ }}…{{ endfor }}
{{ formfields }}
{{ next_button }}
{{ player.xyz }}
More info
Model methods
class Subsession(BaseSubsession):
    aaa = models.IntegerField()

    def creating_session(self):
        self.aaa = 1
        
        
class Player(BasePlayer):
    bbb = models.StringField()
    
    def bbb_choices(self):
        ...
class Subsession(BaseSubsession):
    xyz = models.IntegerField()

def creating_session(subsession: Subsession):
    subsession.xyz = 1


class Player(BasePlayer):
    bbb = models.StringField()
    
def bbb_choices(player: Player):
    ...
More info
User-defined functions
class Player(BasePlayer):
    bbb = models.IntegerField()
    
    def do_something(self):
        self.bbb = 10

# to use it:                
player.do_something()
class Player(BasePlayer):
    bbb = models.StringField()
    
def do_something(player: Player):
    player.bbb = 10

# to use it:
do_something(player)
More info
after_all_players_arrive (method definition style)
class MyPage(WaitPage):

    def after_all_players_arrive(self):
        for p in self.group.get_players():
            p.payoff = 10
class MyPage(WaitPage):
    @staticmethod
    def after_all_players_arrive(group: Group):
        for p in group.get_players():
            p.payoff = 10
after_all_players_arrive (method name style)
class MyPage(WaitPage):
    after_all_players_arrive = 'set_payoffs'
class MyPage(WaitPage):
    after_all_players_arrive = set_payoffs
The old format is not deprecated
before_next_page
class MyPage(Page):

    def before_next_page(self):
        ...
class MyPage(Page):
    @staticmethod
    def before_next_page(player: Player, timeout_happened):
        ...
More info
participant.vars
participant.vars['aaa'] = 1
participant.vars['bbb'] = 2
session.vars['ccc']
participant.aaa = 1
participant.bbb = 2
session.ccc = 3

# settings.py:
PARTICIPANT_FIELDS = ['aaa', 'bbb']
SESSION_FIELDS = ['ccc']
More info This is just for convenience. You can still use participant.vars.
Roles
class Player(BasePlayer):
    def role(self):
        if self.id_in_group == 1:
            return 'seller'
        else:
            return 'buyer'
class Constants(BaseConstants):
    seller_role = 'seller'
    buyer_role = 'buyer'
More info
Requirements files
requirements.txt
requirements_base.txt
requirements.txt
formfield
{% formfield player.xyz %}
{{ formfield 'xyz' }}
More info The old format is not deprecated
formfield errors
{{ form.xyz.errors }}
{{ formfield_errors 'xyz' }}
More info
Rounding numbers in templates
{{ pi|floatformat:2 }}
{{ pi|to2 }}
More info floatformat no longer works in oTree 5
Currency
c(42)
cu(42)
More info
Intentionally accessing a null (None) field
x = player.aaa
try:
    x = player.aaa
except TypeError:
    x = None

# OR set the field's 'initial='
# to something other than None:

aaa = models.IntegerField(initial=0)
More info

About @staticmethod and (player: Player) etc...

If you are using a text editor to write your oTree code, it's recommended to add @staticmethod before all functions inside a page class, like is_displayed, vars_for_template, before_next_page, etc. They are sometimes omitted from this documentation for brevity. You can also add type annotations on all your functions. They are not mandatory but will help your editor provide better autocompletion.

If documentation says... PyCharm users should type...
class MyPage(Page):

    def is_displayed(player):
        ...
class MyPage(Page):
    @staticmethod
    def is_displayed(player: Player):
        ...
def creating_session(subsession):
    ...
def creating_session(subsession: Subsession):
    ...