11 KiB
Lecture: Introduction to oTree
Goal: Understand the architecture of an oTree application and build a basic Bayesian game.
0. Preliminaries:
- Learn Python in X Minutes
- oTree documentation
- oTree Tutorials
- Deploying oTree on Heroku via command line
1. oTree
What is oTree?
oTree is an open-source, Starlette-based framework for implementing interactive experiments in the browser.
- Logic: Written in Python.
- Interface: Written in HTML/Starlette template tags.
- Data Storage: Automatic integration with PostgreSQL or SQLite.
- Deployment: Easy to host on Heroku or local servers.
Installation + project creation
Install
python -m pip install otree
otree --version
Create a project
otree startproject myproject
cd myproject
otree devserver
Then open: http://localhost:8000/
Create an app inside the project
otree startapp my_app
File layout (important ones)
Inside the project:
settings.py— session configs, installed apps, currency settings, etc._static/— CSS/JS (optional)_templates/— global templates (optional)my_app/— your app code
Inside an app:
__init__.py— main app logic (models + pages + constants)templates/my_app/*.html— participant-facing pagestests.py— automated tests/bots (optional but recommended)
The Core Structure
An oTree project is organised into Apps. Each app contains:
__init__.py: The "brain." Defines your models, variables, and logic.Pages.py: Controls the sequence of what the participant sees.- Templates (
.html): The visual interface for each page.
The Hierarchy
- Session: The entire group of participants.
- Subsession: A specific round within an app.
- Group: A collection of players interacting (e.g., in a prisoner's dilemma; we won't do this today).
- Player: The individual unit.
- Pages: The screens participants see (forms, instructions, results).
Data live in the database. Each Player has fields that get saved automatically.
Defining Variables
Variables are defined in __init__.py under three classes: Constants, Subsession, Group, and Player.
class Constants(BaseConstants):
NAME_IN_URL = 'Decision'
PLAYERS_PER_GROUP = None
NUM_PRACTICE_ROUNDS = 2
NUM_PAYMENT_ROUNDS = 3
NUM_ROUNDS = NUM_PRACTICE_ROUNDS + NUM_PAYMENT_ROUNDS
class Player(BasePlayer):
decision = models.IntegerField()
belief_1 = models.FloatField(min=0, max=100)
captcha_attempt_1 = models.StringField(label="", initial="")
More types of fields here https://otree.readthedocs.io/en/latest/models.html#fields.
Forms and validation
Basic:
class Decision(Page):
form_model = "player"
form_fields = ["choice"]
Cross-field validation:
class Player(BasePlayer):
a = models.IntegerField()
b = models.IntegerField()
class SomePage(Page):
form_model = "player"
form_fields = ["a", "b"]
@staticmethod
def error_message(player, values):
if values["a"] + values["b"] != 10:
return "a + b must equal 10."
Where to put logic: common hooks
-
Subsession.creating_session()
Initialise per-session/per-round variables, assign treatments, form groups. -
Page.is_displayed(player)
Conditional page display (by round/treatment/role). -
Page.vars_for_template(player)
Pass computed values to the template. -
Page.error_message(player, values)
Validate across multiple fields. -
Page.before_next_page(player, timeout_happened)
Compute payoffs, set state, write vars. -
Group.set_payoffs()(your own method)
Common pattern: compute payoffs after everyone submits.
Game Logic & Functions
Logic is triggered by events.
Determining if page is displayed
class TaskFeedback(Page):
@staticmethod
def is_displayed(player):
return (
player.round_number <= C.NUM_PRACTICE_ROUNDS and
not player.participant.vars.get('failed_captcha', False)
)
Setting values in particular rounds
def creating_session(subsession: Subsession):
for player in subsession.get_players():
if player.round_number == 1:
treatments_index = list(range(len(C.payoffs_list)))
random.shuffle(treatments_index)
for r in range(C.NUM_PAYMENT_ROUNDS):
player.in_round(C.NUM_PRACTICE_ROUNDS+1+r).treatment_index = treatments_index[r]
Useful built-in methods: https://otree.readthedocs.io/en/latest/models.html#built-in-fields-and-methods.
The User Interface
Templates use HTML combined with oTree tags to display data dynamically.
Contribute.html
{{ block title }}
Round {{ payment_round_number }} out of {{ C.NUM_PAYMENT_ROUNDS }}
{{ endblock }}
Live pages (real-time interaction)
For truly interactive real-time tasks (e.g., chat, continuous-time trading), oTree supports live pages (WebSocket-style). Basic pattern:
class MyLivePage(Page):
live_method = "live_handler"
@staticmethod
def live_handler(player, data):
# data is a dict sent from JS
return {player.id_in_group: dict(message="received")}
You also write JS in the template to send/receive messages.
See more here: https://otree.readthedocs.io/en/latest/multiplayer/intro.html.
Running the Server
To see your work in the browser:
- Initialise:
otree devserver 8000 - Access: Open
http://localhost:8000 - Data: Go to the Admin tab to monitor real-time results and export CSV/Excel files.
Quick Reference Table
| Command | Purpose |
|---|---|
otree startproject <name> |
Create a new project folder |
otree startapp <name> |
Create a new app within the project |
otree devserver |
Run the local testing server |
otree zip |
Package the app for server upload |
Important: Always test your app using to ensure your payoff logic works before bringing in real participants.
2. Deployment
For testing/class projects, focus first on:
- local devserver (
otree devserver); - exporting data reliably.
Common option:
- oTree app on GitHub.
- Host on heroku.com.
- Recruit on UCL ELFE or Prolific.com.
Template already fitted to deal with this pipeline. The setting (in config; available when creating a session) completionlink is where you should put the prolific link. Prolific ID should be saved in participant labels.
On Heroku
- Create new app
- Deploy from GitHub; manual deploy; then turn on automatic deployments
- Add web dyno (GitHub education gives free dynos); worker dyno not needed
- Add-on: postgresql (cheapest for testing)
Running for real:
- Performance M dynos with auto-scalling with max latency of 500ms
- Postgresql Standard 2
- Config vars: OTREE_AUTH_LEVEL: STUDY; OTREE_PRODUCTION: 1; OTREE_ADMIN_PASSWORD: whateveryouwant
Downgrading postgresql is a real pain. Here's my cheatsheet:
Procedure to copy and remove previous database on heroku
# Step 0. Login
heroku login
# Step 1. Provision a new database
heroku addons:create heroku-postgresql:essential-0 --app example-app
# Step 2. Enter maintenance mode to prevent database writes
heroku maintenance:on --app appname
# Step 3. Transfer data to the new database
## Check name and colour; or on website; it takes a while to provision, give it 2-3 minutes for new database to to show up
heroku addons --all
heroku config --app example-app
## Copy
heroku pg:copy app_name_to_copy_from::HEROKU_POSTGRESQL_COLOUR_TO_COPY_FROM HEROKU_POSTGRESQL_COLOUR_TO_COPY_TO --app app_name_to_copy_to
# Step 4. Promote the new database
heroku pg:promote HEROKU_POSTGRESQL_COLOUR_TO_PROMOTE -a appname
# Step 5. Destroy previous database
heroku addons:destroy HEROKU_POSTGRESQL_COLOUR --confirm appname
# Step 6. Exit maintenance mode
heroku maintenance:off --app appname
Example
heroku login
heroku maintenance:on --app southern-path-2
heroku addons:create heroku-postgresql:essential-0 --app southern-path-2
heroku addons --all
# This gets you the colours of the database; note that it takes a bit to provision, so they won't show immediately
heroku config --app southern-path-2
heroku pg:copy southern-path-2::DATABASE HEROKU_POSTGRESQL_PINK_URL --app southern-path-2 --confirm southern-path-2
heroku pg:promote postgresql-clear-38905 -a southern-path-2
heroku addons:destroy postgresql-round-22954 --confirm southern-path-2
heroku maintenance:off --app southern-path-2
On Prolific
My typical study name: Survey on Decision-Making (up to £X.00/Nmin with bonus)
Internal name: Acronym YYYY.MM.DD
My typical description:
This survey seeks to understand how individuals make choices and is conducted by academic researchers.
You can earn up to £X.00 for Nmin.
You will earn a minimum of £Y.00.
Please keep in mind that how much you will receive will depend on your own choices and the choices of other participants. The payment scheme is designed to encourage you to think carefully about your choices.
- Study label: None
- Device requirements: Desktop only
- Labels: None
- Limit participant access: Depends on your budget
- What's the URL of your study? https://HEROKUAPPNAME.herokuapp.com/room/prolific?participant_label={{%PROLIFIC_PID%}}&STUDY_ID={{%STUDY_ID%}}&SESSION_ID={{%SESSION_ID%}}
- How do you want to record IDs? URL parameters
- Completion paths: Manually review
- Screeners:
- Current Country of Residence: United Kingdom (better sample)
- Approval Rate: 95–100
- Fluent languages: English
- Exclude participants from other studies: any pilot and other waves of the same study that you've ran
- Do participants require a login & password to access your survey tool? No
- Total times a participant can complete your study: once
- Reject exceptionally fast submissions: NO!
On ELFE
- Login to main terminal.
- Run the app in production mode, create ELFE session.
- Turn on all terminals and open the link.
- Test the Qualtrics questionnaire at the end.
- Test (elfe.lab.run)[elfe.lab.run].
- Schedule session on (elfe.lab.run)[elfe.lab.run] and invite participants.
On the day
- Login to main terminal.
- Run the app in production mode, create ELFE session.
- Turn on all terminals and open the link.
- Sit participants and allocate them on the app.
- Let them start after all seated.
- Take note of anything unusual.
- Show timer and remind participants of the time every half hour or so.
- Make sure everyone filled out the survey at the end before leaving (if they finished).
- Pay participants (lab manager, via Wise account).