Storing and accessing data¶
uproot uses an append-only log for all data storage. Every change is permanently recorded with a timestamp, creating a complete audit trail of your experiment. This architecture ensures data integrity, enables temporal analysis, and makes your research fully reproducible.
The append-only log¶
Unlike traditional databases that overwrite data, uproot appends every change to a permanent log. This means:
- Complete history: Every value a field ever held is recorded
- Timestamps: Each change includes when it happened
- Audit trail: The code location that made each change is tracked
- No data loss: Even deleted fields are preserved (as "tombstones")
Why this matters for research¶
The append-only log provides:
- Reproducibility — Analyze the exact sequence of participant decisions
- Debugging — Trace when and where unexpected values appeared
- Temporal analysis — Study how responses evolved over time
- Data integrity — No accidental overwrites or race conditions
See Exporting data for more information about when this data is exported.
Player data¶
Store data on individual participants using simple attribute assignment:
class Decision(Page):
fields = dict(
choice=RadioField(label="Your choice", choices=[(1, "A"), (2, "B")]),
)
@classmethod
def before_next(page, player):
# Form fields are saved automatically, but you can add computed fields:
player.made_choice = True
player.choice_time = time()
Read data back the same way:
class Results(Page):
@classmethod
def context(page, player):
return dict(
their_choice=player.choice,
choice_time=player.choice_time,
)
Form field values are saved automatically when participants submit. You can store any additional data you need.
Session data¶
Store data shared across all participants in a session using player.session:
class Setup(NoshowPage):
@classmethod
def after_always_once(page, player):
if not hasattr(player.session, "initialized"):
player.session.initialized = True
player.session.start_time = time()
player.session.total_contributions = 0
Access session data from any player:
@classmethod
def context(page, player):
return dict(
session_start=player.session.start_time,
total=player.session.total_contributions,
)
Session data is visible to all participants and persists for the duration of the session.
Group data¶
For multiplayer experiments, store data at the group level using player.group:
class GroupPlease(GroupCreatingWait):
group_size = 2
@classmethod
def after_grouping(page, group):
group.round = 1
group.total_payoff = 0
Access group data from any group member:
@classmethod
def context(page, player):
return dict(
current_round=player.group.round,
group_total=player.group.total_payoff,
)
Working with mutable types¶
Lists and dictionaries may in some circumstances require a context manager to track changes:
# This can raise an error:
player.scores.append(100)
# Use a context manager instead:
with player as p:
p.scores.append(100)
p.responses["q1"] = "yes"
# Changes are saved when exiting the with block
However, in all simple standard experiments, uproot will not require you to do this.
Specifically, if you are working with player or session and you are writing code within one of the standard page methods provided by uproot, you do not need to use a context manager. (However, it is always allowed to use a context manager.)
The use of context managers is only require in advanced expert cases.
Supported data types¶
uproot supports all common Python types:
| Type | Example |
|---|---|
| Numbers | player.score = 100, player.rate = 0.75 |
| Strings | player.name = "Alice" |
| Booleans | player.consented = True |
| Lists | player.choices = [1, 2, 3] |
| Dictionaries | player.responses = {"q1": "yes"} |
| None | player.partner = None |
| Decimals | player.payment = Decimal("10.50") |
| Tuples | player.coordinates = (10, 20) |
Nested structures work too:
Several other types are also supported, such as set and random.Random. A full list of permitted types is available here.
Accessing other players¶
Get other players in the same group or session:
# Other player in a 2-person group
partner = other_in_group(player)
partner_choice = partner.choice
# All players in a group
for p in players(group):
total += p.contribution
# All players in a session
for p in players(player.session):
print(p.name, p.payoff)
Accessing history (advanced)¶
View the complete history of a player's data:
history = player.__history__()
# Returns: {"choice": [Value(...), Value(...)], "payoff": [...], ...}
Each historical value includes:
- time — Unix timestamp of the change
- data — The value that was stored
- context — The file and line that made the change
- unavailable — Whether this represents a deletion of a field
Quick reference¶
| Storage level | Access | Use for |
|---|---|---|
player.field |
Individual | Participant responses, computed values |
player.session.field |
Shared | Session config, aggregate statistics |
player.group.field |
Group only | Group state, shared resources |
| Pattern | When to use |
|---|---|
player.x = value |
Storing immutable data (numbers, strings, bools) |
with player as p: |
Modifying lists or dicts |
hasattr(player, "x") |
Checking if a field exists |
player.__history__() |
Accessing the complete audit trail |