Documentation

Differences from standard Python

The MX8 Labs Research Platform uses Python for survey logic. Familiar constructs such as variables, lists, dictionaries, loops, conditions, functions, and comprehensions are available. The main differences come from what a survey needs: translatable question text, answer labels that stay in sync with routing, reproducible randomization, and a restricted execution environment.

Some of these differences affect how you write the code; others are applied automatically when you save. The sections below explain each behavior, why it exists, and what it means when editing a survey.

Question text uses templates and tags

In ordinary Python, f"How do you rate {brand}?" immediately substitutes the value of the Python variable brand. For a survey, that would give each brand different question text before translation and reporting can work with it.

Survey questions keep the template separate from the brand being rated. The platform translates the template before filling its placeholders, and reporting can group responses under the same question while using the brand tag as a comparison dimension. Write a plain string with placeholders and supply their values through tags:

from survey import Survey

s = Survey(**globals())

for brand in ["Acme", "Example"]:
    s.rating_question(
        "How do you rate {brand}?",
        number_of_points=5,
        tags=s.tag(brand=brand),
    )

s.complete()

To preserve these templates, saving removes f-string prefixes: f"How do you rate {brand}?" becomes "How do you rate {brand}?". This does not create a tag or evaluate the expression inside the braces. A Python variable named brand alone is not enough: supply it through tags=s.tag(brand=brand) or a surrounding with s.tag(brand=brand): block. See Adding metadata to questions.

The prefix removal applies throughout the survey source, including strings used outside questions. Do not rely on f-string expressions such as {i + 1} being evaluated. Compute the value first and pass it as a tag for question text. For ordinary internal string construction, use .format(), for example pair = "{} vs {}".format(first_brand, second_brand).

Repeated strings become shared constants

In ordinary Python, two identical string literals are independent pieces of source code. Editing one does not update the other. In a survey, an answer label often appears both in a question and in the logic that checks its response. Changing only the label can silently break screening or routing.

The platform keeps these references together by extracting repeated strings into shared constants when you save. For example, you might write:

service = s.select_question(
    "Which service do you use most?",
    options=["Streaming services", "Cable TV"],
)
s.terminate_if(
    service != "Streaming services",
    reason="This survey is for streaming users.",
)

On save, both uses of "Streaming services" are replaced with the same constant:

STREAMING_SERVICES = "Streaming services"

service = s.select_question(
    "Which service do you use most?",
    options=[STREAMING_SERVICES, "Cable TV"],
)
s.terminate_if(
    service != STREAMING_SERVICES,
    reason="This survey is for streaming users.",
)

If you later rename the option to "Video streaming services", edit the value of STREAMING_SERVICES. Both the displayed option and the screening comparison then use the new wording. This is why constants may appear in your saved code even if you did not write them yourself.

The transform finds repeated, nonblank string values across the source, not just answer options. It creates uppercase names derived from their text, inserts definitions before use, and adds numeric suffixes when names would collide. It can reuse an earlier uppercase string constant with the same value. Single-use strings and docstrings remain inline.

When changing wording shared across the survey, edit the constant's value. If one use needs different wording, give that use a separate value and review any logic that depends on it. A constant name is a Python variable, not a question's reporting ID; see Stable question identifiers.

Libraries and language restrictions

Survey code runs in a restricted environment with limits on imports, dynamic evaluation, file access, and introspection. The supported features and restrictions below determine which Python constructs you can use in a survey.

The allowed standard-library imports are collections, datetime, itertools, json, math, random, re, statistics, string, and time. The survey module is also available. Other imports, including pandas, are rejected by the standard survey runner.

Some differences you may encounter are:

Standard Python featureSurvey programming approach
Classes and decorated functionsUse ordinary functions and data structures; class definitions and function decorators are rejected.
eval(), exec(), compile(), and open()Dynamic execution and direct file access through these builtins are blocked.
getattr(), hasattr(), type(), and isinstance()These introspection builtins are blocked. Use the documented return values and properties of survey methods.
map(), filter(), iter(), and next()These builtins are blocked. Use comprehensions, loops, or indexing as appropriate.
set()The constructor is blocked. For distinct values in encounter order, use list(dict.fromkeys(values)).
print() and input()These console builtins are blocked. Ask respondents questions through the survey methods.
is comparisonsThe validator rejects is; use value comparisons with == where that is the intended check.
General use of globals() or locals()Blocked, except for the supported initialization pattern such as s = Survey(**globals()).
Names and attributes starting with __Access is restricted; use the documented survey API.

These are common restrictions, not an exhaustive list of blocked names. Keep Survey and Recoder reserved for their platform classes rather than reusing them as variables, arguments, or function names. Validation reports unsupported features; it does not automatically convert them to supported equivalents.

For examples using the allowed libraries, see Using Advanced Python Features.

Use respondent-stable randomization

Although the random module can be imported, prefer s.random() and s.randomize(items) for survey routing and ordering. The platform helpers use the respondent's seed or ID to make randomization reproducible for the same respondent context. This keeps assignments and ordering consistent when the same survey logic is run again for that respondent.

s.random() returns a float between 0 and 1. s.randomize(items) returns a shuffled list; nested groups stay together, and flatten=True flattens those groups after shuffling. Reproducibility depends on the survey logic and sequence of random calls remaining consistent; it does not mean every call returns the same value.

Saved code uses consistent formatting

Ordinary Python lets you choose your formatting style. The platform uses Black to standardize spacing, indentation, quotes, and line wrapping whenever survey code is saved with validation. This keeps scripts readable and gives the generated constants described above the same style as the surrounding code.

Formatting runs after f-string removal and constant extraction, before validation. The resulting source is saved and returned to the editor, so you continue editing the version the platform checks and runs. Formatting does not repair invalid Python; check the validation errors after saving. See Validating Complex Surveys on the MX8 Labs Research Platform.

For API integrations, saves that skip survey-code validation also skip these source changes. The standalone format tool applies all three changes; the standalone validation tool checks the supplied source without rewriting it.