PsyCloud

Python SDK (psycloudpy)

psycloudpy lets you describe a behavioral experiment in Python and save it as the same PsyCloud bundle that Studio uses. Use it when your study starts from a stimulus table, a factorial design, or a notebook where you are already preparing materials and checking trial counts.

The main idea is simple: build the trial table, describe the screen shown for each row, and name the columns you want in the data file.

Pre-release install

The SDK ships inside the PsyCloud monorepo and is under active development (psycloudpy is not yet on PyPI). From a clone of the repo:

python -m pip install -e packages/psycloudpy

After that, import psycloudpy should work from Python, IPython, or a notebook.

Start With The Design

Before writing code, write the columns you expect to see in your final data:

ColumnWhat it means
conditioncongruent or incongruent
colorthe ink color on this trial
wordthe text printed on the screen
correct_keythe key that should count as correct
response fieldsresponse key, RT, correctness, timeout

The SDK code below builds exactly that table, then uses the table to draw each trial. Notice that correctness is declared inside keypress(...); you do not need a post-trial callback.

stroop.py
from psycloudpy import (
    experiment,
    fixation_cross,
    keypress,
    seed_per_participant,
    text,
    trial,
    write_bundle,
)
from psycloudpy.expr import case_when, expr_if, row, uppercase, when
 
instructions = """
# Color Stroop
 
Name the INK COLOR, not the word.
 
Keys:
- r = red
- g = green
- b = blue
 
Press any key to begin.
""".strip()
 
bundle = (
    experiment(id="demo.stroop", name="Color Stroop")
    .instructions_page(instructions)
    .phase("trials", label="Stroop trials")
    .factors(
        condition=["congruent", "incongruent"],
        color=["red", "green", "blue"],
    )
    .cross()
    .derive(
        "word",
        expr_if(
            row.condition == "congruent",
            uppercase(row.color),
            case_when(
                when(row.color == "red", "GREEN"),
                when(row.color == "green", "BLUE"),
                default="RED",
            ),
        ),
    )
    .derive(
        "correct_key",
        case_when(
            when(row.color == "red", "r"),
            when(row.color == "green", "g"),
            default="b",
        ),
    )
    .repeat(2)
    .shuffle(seed=seed_per_participant("stroop-demo"))
    .screen("trial")
    .present(fixation_cross(x="center", y="center", size=36), 500)
    .ask(
        text(trial.word, fill=trial.color, font_size=72, x="center", y="center"),
        keypress(["r", "g", "b"], correct=trial.correct_key, timeout_ms=2000),
    )
    .record("condition", "color", "word", "correct_key")
    .end_page("# Done\n\nThanks for participating.")
    .bundle(with_auto_ids=True)
)
 
write_bundle(bundle, dir="stroop_bundle", overwrite=True)

Run it:

python stroop.py

The result is a stroop_bundle/ directory. You can import that bundle into Studio, validate it with the CLI, or keep it as a reproducible study artifact.

Read The Chain

The fluent chain is meant to read like a methods section:

  1. Start the study

    experiment(id=..., name=...) gives the study a stable identity. instructions_page(...) adds a one-screen introduction that waits for a key.

  2. Build the trial table

    .phase("trials") opens a block of trials. .factors(...) names the independent variables, .cross() makes every combination, .derive(...) computes useful columns, .repeat(...) adds repetitions, and .shuffle(...) randomizes the rows.

  3. Draw one row

    .screen("trial") describes what a single row looks like. trial.word and trial.color mean "use the value from the current row."

  4. Collect the response

    keypress([...], correct=trial.correct_key, timeout_ms=2000) stores the response and lets PsyCloud score correctness at runtime.

  5. Choose output columns

    .record(...) names the condition columns you want copied into the output data, alongside the response fields that PsyCloud records automatically.

The Two Kinds Of Trial Values

You will see two pronouns in examples:

Use thisWhereMeaning
row.colorinside .derive(...), .filter(...), constraintsa row while the trial table is being built
trial.colorinside .screen(...) stimuli and responsesthe participant's current trial at runtime

In the Stroop example, row.color is used to compute word and correct_key before the study runs. Later, trial.word, trial.color, and trial.correct_key are filled in separately on every participant trial.

If You Have A Fixed Item List

Not every study is a full factorial crossing. For a lexical decision, recognition memory, or sentence task, you may already know the exact items. Define those rows directly in code and pass them to .trials(...). Each row becomes one trial.

lexical_decision.py
from psycloudpy import experiment, keypress, text, trial, write_bundle
 
stimuli = [
    {"item_id": "cat_01", "letter_string": "CAT", "condition": "word", "correct_key": "f"},
    {"item_id": "blit_01", "letter_string": "BLIT", "condition": "nonword", "correct_key": "j"},
]
 
bundle = (
    experiment(id="demo.lexical", name="Lexical Decision")
    .instructions_page("Press F for WORD and J for NONWORD.")
    .phase("main", label="Trials")
    .trials(stimuli)
    .screen("trial")
    .ask(
        text(trial.letter_string, font_size=64, x="center", y="center"),
        keypress(["f", "j"], correct=trial.correct_key, timeout_ms=2500),
    )
    .record("item_id", "condition", "correct_key")
    .end_page("# Done\n\nThank you.")
    .bundle(with_auto_ids=True)
)
 
write_bundle(bundle, dir="lexical_bundle", overwrite=True)

That is often the most ergonomic pattern for fixed-item experiments: keep the item list in the same script as the task, so the design is visible, versioned, and reproducible. If you already have a CSV from another source, import it only after checking the rows and column names explicitly.

Check Before You Run Participants

Use the workbench helpers before launching a study. check(...) and simulate(...) call the local psycloud CLI, while simulate_tables(...) turns a simulation report into tables you can inspect in Python.

from psycloudpy import check, simulate, simulate_tables, write_bundle
 
write_bundle(bundle, dir="stroop_bundle", overwrite=True)
 
# Compile and lint the bundle.
report = check("stroop_bundle")
 
# Dry-run fake sessions to catch design mistakes early.
simulation = simulate("stroop_bundle", participants=10)
tables = simulate_tables(simulation["report"])

For a quick manual check, import the bundle into Studio and preview it like any other study. The bundle files are ordinary JSON plus assets, so they can be kept with the scripts that generated them.

Check the table, not just the code

Before recruiting anyone, inspect the materialized trial table. Confirm the number of rows, the balance of conditions, the correct keys, and the columns you plan to analyze later.

Common Recipes

Study needUse
factorial design.factors(...).cross()
fixed item list.trials(rows)
fixed delay.present(stimulus, duration_ms)
response-gated screen.ask(stimulus, response)
keyboard responsekeypress(["f", "j"], correct=..., timeout_ms=...)
"press any key" pageinstructions_page(...), end_page(...), or anykey()
survey pagesurvey_page(...), form_response()
output condition columns.record("condition", "item_id", ...)

For survey-style work, the ergonomic path is one survey_page(...) with an item list, rather than one trial per questionnaire item:

from psycloudpy import experiment, form_response, survey_page, write_bundle
 
items = [
    {"name": "calm", "label": "Calm"},
    {"name": "alert", "label": "Alert"},
    {"name": "nervous", "label": "Nervous"},
]
 
bundle = (
    experiment(id="demo.mood", name="Mood Survey")
    .instructions_page("Please rate how you feel right now.")
    .phase("survey", label="Mood ratings")
    .factors({})
    .screen("survey")
    .ask(
        survey_page(
            items,
            response_type="likert",
            labels=["Not at all", "A little", "Moderately", "Very much"],
            required=True,
            submit_label="Continue",
        ),
        form_response(),
    )
    .bind(role="trial")
    .end_page("# Thanks")
    .bundle(with_auto_ids=True)
)
 
write_bundle(bundle, dir="mood_bundle", overwrite=True)

What To Learn First

Focus on these five verbs first: experiment, phase, factors or trials, screen, and record. Once those make sense, add derived columns, feedback, counterbalancing, and adaptive flow.

The lower-level pc_* constructors are still available for package authors and advanced migrations, but most study authors should start with the fluent helpers shown here.

Worked Examples

Runnable examples live under packages/psycloudpy/src/psycloudpy/examples/high_level/, including Stroop, simple reaction time, recognition memory, continuous recognition, PANAS, matrix reasoning, adaptive practice, Luck and Vogel change detection, and a design-first memory task.

Next