Introduction to the PsychoPy Coder (tutorial)#

At last, we’ll discuss the PsychoPy Coder! In this tutorial, we explain the basics of the Coder interface. It will be a somewhat more “dry” tutorial because we won’t actually create any stimuli or trials in this tutorial, because we’ll save that for the next tutorial. Like in the previous Builder tutorial, we will explain the concepts by walking you through the process of programming a real experiment. This time, we will create a variant of the classical color-word Stroop task, the emotion-word Stroop task, in which participants are presented with images of emotional facial expressions in combination with words describing emotions that are congruent with the images (e.g., an angry expression with the word “angry”) or incongruent with the images (e.g., a happy exression with the word “angry”).

The psychopy package#

When using the Builder interace, you’ve seen that, “under the hood”, PsychoPy converts your Builder experiment to a Python script, which is then executed to run your experiment. If you look at this generated Python script closely, you’ll see that most of the code is based on functions and classes from the psychopy Python package. Whereas the Builder interface generates such code from your graphical experiment, in the Coder interface you’ll write your experiment using functionality from the psychopy package directly!

Tip

If you plan on programming your PsychoPy experiment (so not use the Builder interface), you technically do not need the entire “standalone” PsychoPy package; installing the psychopy Python package would suffice and you could just write your experiments in your favorite editor (like Visual Studio Code). However, as mentioned on the Getting started page, getting the psychopy package to work is not easy, which is why we recommend the “batteries included” standalone version of PsychoPy.

The psychopy package contains different modules for different features. For example, the visual module contains a class to specify and create a window and a large set of visual components (like text, image, and movie components) and the event module contains code to work with “events” such as mouse clicks/movement and keyboard presses. Check out PsychoPy’s reference manual for a complete overview of the package’s modules.

As you will see, most of PsychoPy’s functionality (like the different components) is implemented in custom classes, so your experience with object-oriented programming as discussed in week 1 will be very useful!

Note

In this tutorial, you’ll notice that many of the properties of Builder elements (e.g., the Experiment settings and Builder components like text and image components) have the same name and can take the same values as the attributes of the corresponding classes in the Coder interface!

The Coder interface#

Now, let’s get started by opening the Coder interface.

ToDo

Open the Coder interface (ViewOpen Coder view). You may close the Builder interface for now.

Like the Builder interface, the Coder interface has several subwindows (panes). The panel on the left represents the Source assistant, which lists all files in the current working directory (in the File Browser tab) and information about the Python modules in the current working directory specifically (in the Structure tab).

ToDo

By default, PsychoPy’s current directory is its installation path. Although this is not strictly necessary for this tutorial, change it to the tutorials/week_2 directory by clicking on the right arrow (Jump to another folder).

At the bottom of the Coder interface in the Shelf pane, you’ll find a so-called “Python shell”. You can think of it as a type of command line (like we discussed in the first Jupyter notebook of week 1), but specifically for Python code. You can only run a single line at once, but it’ll show the result immediately.

ToDo

Try writing some valid Python code in the Python shell (and pressing enter to run it), e.g., 1 + 1. Note that the Python shell will remember variables if you define them, just like Jupyter notebooks, so you can also run multiple commands like this:

>>> a = 5
>>> b = a ** 2
>>> b - a

This Python shell is very useful to debug or try out short code snippets. For example, if you forgot what the function len returns, you can for example run the command len([1, 2]) in the Python shell to find out (spoiler: an integer).

Finally, the last pane in the middle is PsychoPy’s code editor. Here, you can open any plain-text file (not just Python files!) which you can modify and save. In practice, of course, you’ll probably mostly work with Python files in this editor. Like most code editors, the Psychopy code editor also does some code formatting and syntax highlighting. One thing you’ll notice is that it by default uses two spaces (in contrast to the more conventional four spaces) for indentation.

Right now, there are probably no active files in your Psychopy code editor, so let’s create one for our emotion-word Stroop task!

ToDo

Create a new Python file (FileNew) and save it as emo_stroop.py in the tutorials/week_2 directory.

Although you learned in week 1 that Python files should be run in a terminal on the command line (e.g., python emo_stroop.py), Python files within the PsychoPy coder are actually run the same way as Builder experiments: by clicking on the big green play (►) button!

ToDo

Add some code to your exp_stroop.py file, e.g., print("PsychoPy 4evah"), and run the file.

After clicking the Run experiment button, the Experiment runner window should pop up, displaying something like the following:

#### Running:/Path/to/your/file/emo_stroop.py ####
PsychoPy 4evah
##### Experiment ended. #####

As you can see, the Experiment runner shows your print statement; remember this, as it is a nice way to debug your experiments!

Dialog boxes#

Arguably the first step in many experiments is providing the experiment with some information about the current session, such as the participant number, condition the participant is in, etc. In Builder experiments, you can implement this in the Experiment settings (under the Basic tab). The equivalent functionality in the Coder interface is implemented in the psychopy.gui module.

The psychopy.gui module contains different classes to create dialog boxes, but arguably the easiest one to use is the DlgFromDict class. This class needs a single mandatory argument upon initialization: a dictionary in which the keys represent the dialog’s fields (e.g., “participant_id”) and the values represent the default for each field. (If you set a value to an empty string, '', there won’t be a default value.)

For example, if you’d like a dialog box in which the participant number can be filled in, you can do the following:

exp_info = {'participant_nr': ''}  # no default!
dlg = DlgFromDict(exp_info)

Note that it is quite important to create the dictionary before passing it to the DlgFromDict class instead of passing it directly, e.g., DlgFromDict({'participant_nr': ''}). This is because PsychoPy will modify the dictionary (i.e., the variable exp_info in the code snippet above) with the information filled in by the participant/experimenter.

ToDo

In your experiment, create a dialog box with the fields “participant_nr” and “age”, in which the participant number should have the default 99 (for testing purposes) but age should not get a default value. Don’t forget to import the DlgFromDict class first! Then, run your experiment.

As you can see, when you run the experiment, a dialog box appears! After you filled in the required information, you can click OK to start the experiment. Notably, after you fill in the fields of the dialog box (e.g., with participant number 01 and an age of 29), you can access this information from the original dictionary passed to the DlgFromDict class (i.e., example_info in the code snippet above). For example, exp_info['age'] will return the age that the participant filled in (e.g., 29).

Warning

The type of the values from the dictionary depend on the type of the variables you initialized the dictionary with. For example, if you initialize a particular value with an empty string (i.e., '', such as with “participant_nr” in the code snippet above), then the value representing that field will also be a string, even if you fill in a number (such as 1)!

Let’s do an exercise to see whether everything’s clear so far.

ToDo

Given the dialog box configuration from the previous ToDo, include a print statement to your script that will print out the following: “Started the experiment for participant … with age …”, where the triple dots are replaced by whatever you filled in when running the experiment.

Click here to show the solution (but try it yourself first!)
# I like F-strings, but other string formatting methods are fine, too!
print(f"Started the experiment for participant {exp_info['participant_id']} with age {exp_info['age']}!")

Currently, it doesn’t matter whether the user clicked on OK or Cancel in the dialog box — in both cases, PsychoPy would have continued with the script. It would be nice to actually quit the experiment when the user pressed Cancel. To do so, we can use the attribute OK from the dialog box object, which is set to True when the user clicks on OK and is set to False when the user clicks on Cancel. To quit the experiment, we can use the function quit from the psychopy.core module. Then, we can implement something like the following to quit the experiment when the user clicked on Cancel instead of OK (assuming the dialog box object is named dlg and the quit function has already been imported):

if not dlg.OK:
    # Maybe add a nice print statement?
    print("User pressed 'Cancel'!")
    quit()

We recommend adding this snippet right after initialization of the dialog box in every experiment that actually uses a dialog box! Now, those that want a challenge, try the following (optional) ToDo.

ToDo (optional)

Add some code after the if not dlg.OK code block that also quits the experiment when the user fills in an invalid participant number (let’s say, anything higher than 99) or an invalid age (let’s say, below 18). Try running the experiment with different values for these two options to see whether your implementation works as expected!

Click here to show the solution (but try it yourself first!)
if not dlg.OK:
    quit()
else:
    # Quit when either the participant nr or age is not filled in
    if not exp_info['participant_nr'] or not exp_info['age']:
        quit()
        
    # Also quit in case of invalid participant nr or age
    if exp_info['participant_nr'] > 99 or int(exp_info['age']) < 18:
        quit()
    else:  # let's star the experiment!
        print(f"Started experiment for participant {exp_info['participant_nr']} "
                 f"with age {exp_info['age']}.")

Note that the DlgFromDict in fact can be customized much by using different arguments upon initialization. Check out the documentation to learn more.

Now, so far we haven’t really discussed how to actually create proper experiments, so let’s start with arguably the most important element of Coder exeriments: the Window.

The Window#

One of the most important classes from the psychopy package is the Window class, which defines the window in which you are going to run your experiment. It is quite a complex class, with many different attributes and methods; we’ll discuss the most important ones in this tutorial.

You can import the Window class from the visual module of the psychopy package (i.e., from psychopy.visual import Window). This, by itself, does nothing; for the experiment window to appear, we need to intialize an object with the Window class. There are a lot of arguments that can be used upon initialization (for an overview, see the docs), but all arguments have sensible defaults, so you can initialize a Window object as follows:

win = Window()

Note that you may use any variable name for your Window object, but we recommend naming it win like in the code snippet above, as it’s short but descriptive.

ToDo

Initialize a Window object as shown in the above code snippet and run your experiment! Don’t forget to also import the Window class!

When running the current experiment, you should briefly see a gray window pop up. This is the “default” experiment window. We can, of course, change the way it looks by passing it arguments upon initialization! We can, for example, change the size by passing a tuple with the width and height in pixels to the argument size. For example, if you’d want a window of (for some reason) 400 (width) by 800 (height) pixels, you’d initialize your Window as follows:

win = Window(size=(400, 800))

Most of the times, though, you’d probably want to run your experiment in “full-screen” mode. To do so, pass True to the argument fullscr (i.e., win = Window(fullscr=True)). Note that if you specify fullscr=True but leave the window’s size at its default (i.e., (800, 600)), you’ll see a warning in the Experiment running saying “User requested fullscreen with size [800, 600], but screen is actually … Using actual size”. This warning is harmless and can be ignored, but if you want to prevent this warning, you should specify the actual size of your monitor as well (e.g., size=(1920, 1080) for an HD monitor).

Warning

When you run the experiment in full-screen mode (i.e., fullscr=True), there is no easy way to quit the experiment unless you included this in your experiment (which we’ll discuss later). So if you, for example, accidentally create an infinite loop (!), you’d be “stuck” inside the PsychoPy window. As such, we recommend not running your experiments in full-screen mode until you’re completely done and ready to run it “for real”. Then, swithc on full-screen mode, because that actually may improve timing precision quite a bit!

You can also change the window’s background color by passing a list or tuple with three numbers, corresponding to the desired RGB values, to the color argument.

ToDo

Although we don’t recommend doing so in a real experiment, try making the window’s background color bright blue. Want a more challenging exercise? Try to set the background to bright orange. Hint: note that PsychoPy assumes that RGB values range from -1 (minimum) to 1 (maximum), not from 0 to 255!

Another important argument of the Window class is the monitor, to which you can pass the name of the monitor, as defined in the monitor center, you want to use for this experiment. For example, if you defined a monitor in the monitor center with the name “laptop”, you can pass this configuration to the Window class as follows:

win = Window(monitor='laptop')

ToDo (optional/difficult!)

If you don’t want to use the monitor center at all (e.g., when you’re programming your experiments in an external code editor), you can also programmatically using the Monitor class from the psychopy.monitor module. Try creating a monitor configuration for your own laptop/desktop monitor. Make sure you set the monitor’s size (in pixels), width (in cm), and distance between you to the monitor (in cm). The documentation of the monitor module contains all info you need to do this!

Note

If you initialize a Window object with a Monitor object (instead of a string pointing to a monitor previously specified in the monitor center), the Experiment Runner will always show the warning "Monitor specification not found. Creating a temporary one...". It is, in fact, using your own Monitor object, so you can safely ignore this warning.

Finally, the last important argument of the Window class is the type of units that should be used by default for your components (which we’ll discuss later), such as “norm” (for normalized units), “deg” (for visual degree angle), and “pix” (for pixels). As said before, there are in fact many more arguments to pass to the Window class (see the documentation for an overview), but we believe that the ones we discussed here are most important to know and that the other arguments all have sensible defaults.

ToDo

Let’s create a Window object that we’ll use for the rest of our emotion-word Stroop experiment! Make sure it is shown in full-screen mode, uses normalized units, uses the monitor specification of your own laptop/desktop monitor, and has a black background. Run your experiment to see whether it looks like expected!

Note that you can also change Window attributes after creating the object by directly editing the attributes. For example, if you’d want to change the units after initialization to pixels, you can do the following:

win.units = 'pix'

Warning

It’s quite likely that, at some point in this or the next tutorial, you’ll run the experiment, but no window opens! This is likely caused by a (syntax) error somewhere in your script. If this happens, check the Experiment runner window for the error and its traceback.

Timing & clocks#

When you run the current experiment, you’ll only see a black screen for like a second or so before it disappears again. The reason why the window doesn’t stay open is because we don’t tell it to! It is important to realize that PsychoPy will run the experiment script from top to bottom (like any Python file is run, actually) and will close the window once the script ends. Put differently, you can interpret the script as a chronological chain of events from the top of the script (beginning) to the bottom of the script (end).

So, if you want to keep the window open for a little longer, we can simply tell PsychoPy so! There are, in fact, different ways to do this, but arguably the easiest way is using the wait function from the psychopy.core module. We can pass this function a number corresponding to the amount of time (in seconds) PsychoPy should wait before continuing with the rest of the script.

ToDo

Import the wait function from the psychopy.core module and, after initializing the window, use it to make PsychoPy wait 2 seconds. Then, run the experiment.

When you ran the experiment after adding the call to the wait function, you may have noticed that the window was active for more than 2 seconds. This because, as we mentioned earlier, it takes a while to close the window.

Another important timing-related concept in PsychoPy is “clocks”. Clocks allow you to precisely keep track of the timing of events, which is especially important in studies that need strict control of stimulus onsets and duration, such as psychophysics and neuroimaging experiments. This functionality in PsychoPy is implemented in, you guessed it, the Clock class (from the psychopy.core module). The Clock class does not require any arguments upon initialization, so you can simply create a Clock object as follows:

clock = Clock()

The variable name, clock, is of course arbitrary; name it whatever you like! Upon initialization, the clock’s time is set to 0. If you, at some point, want to know how much time has passed since the clock’s initialization, you can call the getTime method:

t_since_init = clock.getTime()
print(t_since_init)  # prints time (in sec.)

Now, with our knowledge about clocks, let’s check whether the wait function actually makes PsychoPy wait as long as we tell it. (Of course it does, but it’s a nice way to practice how to use a Clock.)

ToDo

Import the Clock class and, after creating a Window object, initialize it. Then, query the time using getTime and store it in a variable (e.g., t_before_wait). Aftewards, make PsychoPy wait for 2 seconds (using the wait function), and finally, query the time again and store in another variable (e.g., t_after_wait). Make your script print the time before wait call, after the wait call, and the difference between those two times. Then, run the experiment.

Click here to show the solution (but try it yourself first!)
clock = Clock()
t_before = clock.getTime()
print(f"Time before wait: {t_before:.3f}")
wait(2)
t_after = clock.getTime()
print(f"Time after wait: {t_after:.3f}")
t_diff = t_after - t_before
print("Difference: {t_diff:.3f}")

If you implemented the ToDo correctly, you should see that the time just after initialization of the clock is very close to zero (e.g., 7.83892915e-06) and that both the time after the wait call and the difference before and after the wait call is approximately 2 seconds — just like we expected!

Another important methods of the Clock class is reset, which sets the clock’s time back to zero. This feature becomes useful when you want to reuse a clock for multiple routines, i.e., just reset the clock just before you want to use it again!

Responses#

Another important aspect of experiments is handling and interacting with participant responses. Here, we limit ourselves to two ways of interaction: with a keyboard and with a mouse.

Keyboard responses#

To interact with keyboard responses, the psychopy package contains a — guess what — Keyboard class in the psychopy.hardware.keyboard module. This class records all keypresses since its initialization, which you can save or use in your experiment. Although there are several arguments upon initialization (see the documentation), these are all optional and have sensible defaults.

One important attribute that Keyboard objects have is clock: a Clock object to keep track of keypress onsets and reaction times (relative to the initialization of the Clock object).

ToDo

Import the Keyboard class (at the start of your script), initialize a Keyboard object (use the variable kb), wait for 1 second (use the wait function), and print the time since the initialization of the Keyboard option using the clock attribute! Then, run the experiment and check the Experiment runner for the printed output.

Click here to show the solution (but try it yourself first!)
# At the start of your script
from psychopy.hardware.keyboard import Keyboard

# At the end of your script
kb = Keyboard()
wait(2)
t_since_init = kb.clock.getTime()
print(f"Time since initialization of Keyboard: {t_since_init:.3f}")

Arguably the most important method of the Keyboard class is the getKeys method. This returns a list of keypresses pressed since the previous call to getKeys or, if it is the first time the method is called, since the initialization of the Keyboard object.

ToDo

After the initialization of the Keyboard object and the call to wait (from the previous ToDo), call the getKeys function and store the result in a variable (e.g., keys) and print this variable. Then, run the experiment and check the Experiment runner to see the printed output.

As you can see, the getKeys method specifically returns a list of KeyPress objects! These objects are not just strings corresponding to the pressed keys (e.g., “a”, “b”, “return”, “left”, etc.), as you might expect. In fact, these KeyPress objects contain much more information that is contained in its attributes:

  • name: a string with the name of the pressed key (e.g., "a", "b", "return", "left", etc.);

  • rt: a float with the time (in seconds) from start of the Keyboard clock (i.e., the clock attribute);

  • tDown: a float with the abolute time (in seconds; unlikely you ever need this);

  • duration: a float with the time (in seconds) the key was pressed in (or None if still pressed in)

Importantly, one common thing in working with keypresses is that you want to check whether a particular key was pressed. For example, suppose you want to check whether the particpant pressed the spacebar. To do so, you could write the following:

keys = kb.getKeys()
spacebar_pressed = False
for key in keys:
    if key.name == "space":
        spacebar_pressed = True

As you can see, this requires quite a lot of code. That’s why PsychoPy added some “syntactic sugar” such that you can also do the following:

keys = kb.getKeys()
# Checks if "space" in the list of keypresses and returns
# a boolean (True / False)
spacebar_pressed = "space" in keys

ToDo

One routine that is common in (PsychoPy) experiments, and one which we also discussed in the Builder tutorials, is the “press-a-key-to-continue” routine. This can be implemented in PsychoPy using a while loop in combination with the keyGets method of a Keyboard object. Try implementing this in your script such that you only advance with the experiment when you press the enter key (“return”). You may remove the code from the last two ToDos (to clean up your script a bit).

Click here to show the solution (but try it yourself first!)
while True:
    keys = kb.getKeys()
    if "return" in keys:
        break

If you want to learn a little more about keyboard interaction, try the next (optional and more difficult) ToDo!

ToDo (optional/difficult)

As mentioned before, the getKeys method returns a list of KeyPress objects with several attributes with information about the keypress. For a period of two seconds, for each detected key press, print in a single statement the name of the key, its reaction time, and duration (e.g., “The ‘a’ key was pressed within 2.156 seconds for a total of 0.255 seconds”). F-strings would we nice here! (You may remove the code from the previous ToDo.)

Click here to show the solution (but try it yourself first!)
# We need to reset the clock!
kb.clock.reset()
while kb.clock.getTime() < 2:
    keys = kb.getKeys()
    for key in keys:
        # The `:.3f` part in the F-string makes sure the float is only displayed with 3 decimals!
        print(f"The '{key.name}' key was pressed within {key.rt:.3f} seconds for a total of {key.duration:.3f} seconds")

Mouse responses (optional)#

Instead of interacting through the keyboard, you can interact with mouse responses of the participant. Whether you have participants respond with keyboard presses or with the mouse is of course up to you (and depends on your experiment)! For the sake of explaining how to implement interaction with mouse responses, let’s add another screen to our experiments with a big, red button, which the participant has to click (with the mouse) in order to start the experiment.

Just like with keyboard responses, the psychopy package contains a class, Mouse (from psychopy.event), which implements interaction with the mouse. As can been seen in the documentation, a Mouse object can be initialized with several optional arguments (visible, newPos, and win) and contains various methods to query information about the mouse position (getPos) and mouse clicks/presses (getPressed). You can even set the position of the mouse (setPos) and make the mouse (temporarily) (in)visible (setVisible)!

Warning

In the documentation of the psychopy.event module, you can also see several functions for keyboard interaction, such as waitKeys and getKeys, which overlap in functionality with the previously discussed keyboard class. It is recommended to use the keyboard class instead of the functions from the event module!

Now, let’s practice a bit with the Mouse class!

ToDo

Import the Mouse class and initialize a Mouse object. Then, wait to continue with the experiment until the participant presses the left mouse button (you need to use the getPressed method). Check the documentation to see what the getPressed method returns exactly.

Click here to show the solution (but try it yourself first!)
# At the top of the script
from psychopy.event import Mouse

mouse = Mouse()
while True:
    buttons = mouse.getPressed()
    if buttons[0]:
        break

And another one for those who want a challenge.

ToDo

Import the Mouse class and initialize a Mouse object. Then, wait to continue with the experiment until the participant presses the left mouse button (you need to use the getPressed method). Check the documentation to see what the getPressed method returns exactly.

Click here to show the solution (but try it yourself first!)
# At the top of the script
from psychopy.event import Mouse

mouse = Mouse()
while True:
    buttons = mouse.getPressed()
    if buttons[0]:
        break

And those who want a challenge, try the next one.

ToDo

With the setPos method, you can control the position of the cursor! Try moving the mouse to the upper left corner of the screen and then going to each other corner in a clockwise fashion, stopping for 0.5 seconds at each corner.

Click here to show the solution (but try it yourself first!)
# This is just one solution! THere are many different implementations possible!
# These numbers assume "norm" units
x = [-.9, .9, .9, -.9]
y = [.9, .9, -.9, -.9]
for i in range(4):
    mouse.setPos((x[i],y[i]))
    wait(0.5)

Quitting the experiment#

As you’ve seen so far, when the Python interpreter arrives at the end of your script, the PsychoPy window automatically closes and the Python process finishes. In the context of PsychoPy experiments, however, it is good practice to end the experiment by explicitly closing the window using the window’s close method and then calling the quit function from the core.psychopy module (as we did before in the section on dialog boxes). Although not strictly necessary, calling the close method and the quit function perform a bit of bookkeeping that may prevent issues, so we recommend always including this at the very end of your script!

ToDo

At the end of your script, add the code to close your window and to quit the experiment.

This tutorial features some of the most important, but arguably boring, aspects of programming experiments in the PsychoPy Coder, so let’s continue with the next (and final) tutorial which discusses creating components and other fun stuff!