Documentation

Survey Programming Cookbook

Here are some ways to set up some of the more advanced features you might need to add to your survey.

Adding in recodes

Recodes can be added to a survey using the recodes parameter of various question types such as numeric_question, select_question, multi_select_question. Recodes allow you to map the response to a different value for easy analysis. For example, you might ask a respondent about their age, but then want to group their response into different age ranges. In this case, you can specify a dictionary of recodes where the keys are the values the respondent actually selected and the values are the new values you want to map them to. To use recodes in a survey question, you simply pass the recodes dictionary to the question type's recodes parameter.

from survey import Survey

s = Survey(**globals())

s.multi_select_question(
     "Which of the following fruits do you like?",
     options=["Apple", "Banana", "Cherry"],
     recodes={"Apple": "Fruit",
              "Banana": "Fruit",
              "Cherry": "Berry"})
Using tags to pipe values into questions

In addition to a question's main parameters, tags can be attached to a single question with tags=s.tag(...), or to every question inside a with s.tag(...): block. These tags are used to group questions together for reporting purposes and can be useful in analyzing survey results.

For example, if you have a series of questions about different brands of cars, you could specify the brand as a tag for each question. This would make it easier to analyze the results and better understand what people like or dislike about different brands.

Tags also pipe values into the question text. For example, if you have a tag of "brand" set to "Ford" and a question of "What do you like about {brand} cars?", then respondents will see "What do you like about Ford cars?":

s.text_question("What do you like about {brand} cars?", tags=s.tag(brand="Ford"))
Handling Repeated Questions

Each question in your survey should have a unique text that exactly matches the survey brief.

If the same question appears multiple times, check if it should:

  • Be asked only once to each respondent (possibly using conditional logic).
  • Be asked in different contexts (use unique tags to differentiate contexts).
  • Be repeated in the same context (use a repeat-count tag to differentiate questions).

If you need to repeat a question, then add an additional tag so it can be uniquely identified for reporting:

Example
s.text_question("Why do you say that?", tags=s.tag(brand=brand, opinion=opinion))
s.text_question("Why do you say that?", tags=s.tag(repeat=1))
Managing Quotas

Quotas ensure that specific demographics or respondent criteria are met. Use the s.set_quota() method to create quota groups and the s.quota() method to define individual quotas.

Example of setting a quota for console ownership distribution:
console = s.select_question(question="Which of the following game consoles do you own?",
                            options=["Xbox", "Playstation", "Nintendo Switch"],
                            randomize=True,
                            other_options=["None of these"])

s.set_quota(
    name="Console Ownership",
    quotas=[
        s.quota("Xbox", criteria=console == "Xbox", quota=0.20),
        s.quota("Playstation", criteria=console == "Playstation", quota=0.50),
        s.quota("Nintendo Switch", criteria=console == "Nintendo Switch", quota=0.3)
    ]
)

Important: Quotas should sum to 100% and must be placed early in the survey after asking relevant questions.

Handling Lists

When working with lists (e.g., brands), you might want to limit the number of items shown to each respondent. Use the s.get_least_filled() method to dynamically select items from a list.

Example
brand_options = ["Dove", "Pantene", "Head & Shoulders", "Herbal Essences", "Garnier", "Tresemme"]

selected_brands = s.get_least_filled(3, brand_options, "selected_brands")

for brand in s.randomize(selected_brands):
    # Ask questions about each selected brand here
    s.select_question(
        "How often do you buy {brand} products?",
        options=["Never", "Occasionally", "Regularly"],
        tags=s.tag(brand=brand),
    )
Randomization

For questions involving choice or ranking, it's crucial to randomize the order of items to avoid bias. Use the randomize attribute or the s.randomize() method as needed.

Example:
brands = ["Heinz", "Campbell's", "Progresso", "Amy's", "Healthy Choice"]

for brand in s.randomize(brands):
    familiar = s.select_question(
        question="Have you bought any {brand} products in the last week?",
        options=["Yes", "No"],
        tags=s.tag(brand=brand)
    )

s.randomize() keeps nested lists together, so you can shuffle groups of related items without breaking them apart. Pass flatten=True to flatten the groups into a single list after shuffling:

genres = [
    "Jazz",
    ["Punk Rock", "Classic Rock", "Indie Rock"],  # this group stays together
    "Hip-Hop",
]

for genre in s.randomize(genres, flatten=True):
    s.select_question(
        "What do you think of {genre} music?",
        options=["Like", "Dislike", "Neutral"],
        tags=s.tag(genre=genre),
    )

If you need a random number rather than a shuffled list — for example, to split respondents into cells — use s.random(). It returns a float between 0 and 1 that is stable for the same respondent, so each respondent is always assigned to the same cell:

cell = "Test" if s.random() < 0.5 else "Control"
s.store_value("cell", cell)
Custom Response Validation

Responses can be validated using the custom_validator parameter, which typically involves a lambda function to check specific conditions.

Example
s.text_question("Please enter the word 'apple'", custom_validator=lambda x: "Please enter the word 'apple'" if x.lower() != "apple" else None)
Dynamic Question Text

Use placeholders in question text, which will be replaced by the values of tags passed via tags=s.tag(...).

Example
brand = "TheBrand"
s.text_question("Why do you like {brand}", tags=s.tag(brand=brand))
Storing Calculated Variables

You might need to store calculated values that are not directly asked in the survey. Use s.store_value() for this purpose and the variables will be available for reporting.

Example
from datetime import datetime

birth_year = s.numeric_question("What year were you born in?", min_max=(1920, 2020))
age = datetime.now().year - birth_year
s.store_value("age", age)

s.store_value() returns the value it stored, but there is no function to read stored values back — for survey logic, keep using normal Python variables (like age above) rather than the stored reporting value.

Storing a list of values

When the derived value is naturally a list — multiple brands, segments, or flags — use s.store_values() instead. It produces a multi-select calculated variable in reporting, the same shape as a multi-select question.

from survey import Survey
s = Survey(**globals())

known = s.multi_select_question(
    "Which brands have you heard of?",
    options=["Acme", "Globex", "Initech", "Umbrella"],
)
considering = s.multi_select_question(
    "Which of those would you consider buying?",
    options=known,
)

# Save the intersection as a derived multi-select variable.
s.store_values("aware_and_considering",
               list(set(known) & set(considering)),
               tags=s.tag(source="derived"))
Catching speeders with response timing

s.time_between(first_response, second_response) returns the elapsed time between two responses in seconds (as a float). Use it to mark respondents who tabbed through important content too fast — for example, the body of an ad or a long set of instructions. It returns None when either response has not been recorded or its timestamp is unavailable, so check for None before comparing.

from survey import Survey
s = Survey(**globals())

ad_start = s.show_message(
    "Please read the following advertisement carefully.",
    number_seconds=5,
)
ad_end = s.rating_question(
    "How likely are you to consider this brand?",
    number_of_points=5,
)

# Flag respondents who spent less than the expected dwell time on the ad.
minimum_dwell_seconds = ...  # set this per study
ad_dwell = s.time_between(ad_start, ad_end)
s.store_value("ad_speeder",
              bool(ad_dwell is not None and ad_dwell < minimum_dwell_seconds))

There is no universal threshold. What counts as too fast depends on the question — how much content there is to read, watch, or think about — so set minimum_dwell_seconds for each study rather than reusing a fixed number.

Pair this with Per-question timers when you want to enforce a minimum dwell as well as record it.

Allowed Python libraries

You may import and use the following standard-library modules — this is the complete allowlist:

  • collections
  • datetime
  • itertools
  • json
  • math
  • random
  • re
  • statistics
  • string
  • time

The survey module itself is always available — every survey starts with from survey import Survey. Other standard-library modules and third-party packages such as pandas are not available; if you would like to import non-standard Python libraries, these are available on an Enterprise plan.