Learning Python for Research (Part II): Data Structures & Control Flow

If you followed along with Part I, you should have a working conda environment, a text editor, and some basic Python running. Now comes the part that actually starts to feel useful.

This post covers data structures, control flow, and functions. Not glamorous, but this is the stuff that makes up most real scripts. Before bringing in external libraries, it helps to know what Python already gives you. NumPy and Pandas build on these ideas.

With these tools, you can already:

  • Store participant IDs, trial results, and metadata
  • Loop through hundreds of trials without manual repetition
  • Flag bad trials automatically based on criteria
  • Write helper functions instead of copying the same block of code everywhere

Lists: Ordered Collections

Lists are one of the most common ways to store multiple items in Python. They are ordered, so items keep their position. You can add, remove, and change them. In research, you might keep a list of participant IDs, stimulus names, or data files to process. You use [] to create lists. For example my_data = [1, 5, 10]

A quick note on indexing

When we have to access data in a list (or other data structures when allowed), we can index into the list. Basically, get the data we need. To index into a list, we can use the convention list_name[index]. Where the index is between 0 and n-1, this is because Python indexes starting at 0. So if you have 10 items, your indexes have to be between 0-9. You can also get a slice of the data by using the convention list_name[index_1:index_2].

participants = ["P001", "P002", "P003"]

# access by index (count starts at 0)
print(participants[0])   # P001
# Negative indexing counts backwards from the end of a list,
# so -1 gives us the last item
print(participants[-1]) # P003

# add a new participant
participants.append("P004")

# remove a participant
participants.remove("P002")

# loop over them (will cover loops later)
for pid in participants:
    print(pid)
          

The key with lists is that they are flexible containers for data. They can hold numbers, booleans, or even other lists:

# list of trial accuracies
accuracies = [0.91, 0.85, 0.88]

# list of lists - participant ID and their accuracies
data = [
    ["P001", [0.91, 0.85, 0.88]],
    ["P002", [0.75, 0.80, 0.78]]
]
          

Try it yourself:

  • Create a list of your top 5 favourite research tools or software.
  • Add a new item to the list, then remove one.
  • Create a list that contains lists of fruits, vegetables, and desserts, with labels

Tuples: Lists That Can't Change

Tuples are like lists, but immutable. Once you make one, it can't be changed. They’re great for storing fixed information, like coordinates, or function return values that shouldn’t be modified by mistake. Unlike lists, we use () to create tuples, but would still use [] to index into the tuple.

origin = (0, 0)  # x, y coordinates
print(origin[0])  # 0

# tuple unpacking
x, y = origin
print(f"x: {x}, y: {y}")

Tuples can also be used when returning multiple values from a function (we haven't learned how to make functions yet):

def min_and_max(values):
    return (min(values), max(values))

result = min_and_max([3, 7, 1, 5])
print(result)       # (1, 7)
low, high = result
print(low, high)    # 1 7

Try it yourself:

  • Create a tuple that stores today’s date as (year, month, day).
  • Use tuple unpacking to print it in “day/month/year” format.

Dictionaries: Key/Value Mappings

Dictionaries store data as key/value pairs. The key is the label; the value is the thing stored under that label. They work well for mapping IDs to results, condition names to settings, or filenames to metadata.

results = {
    "P001": [0.52, 0.48, 0.56],
    "P002": [0.61, 0.59, 0.63],
}
print(results["P001"])  # [0.52, 0.48, 0.56]

# add a new participant
results["P003"] = [0.70, 0.65, 0.72]

# loop through dictionary
for pid, scores in results.items():
    print(pid, scores)

You can also check if a key exists before using it:

if "P004" not in results:
    results["P004"] = [0.80, 0.82, 0.78]

Try it yourself:

  • Create a dictionary mapping 3 country names to their capital cities.
  • Add a new country/capital pair.

Sets: Unique Values Only

Sets store unordered, unique items. If you need to remove duplicates from a dataset or just check whether something is present, sets can save you a lot of time.


conditions = {"A", "B", "C", "A"}
print(conditions)  # Contains A, B, and C (order may vary)

# membership test
print("A" in conditions)  # True
print("D" in conditions)  # False

Converting a list to a set is a quick way to deduplicate it:

participants = ["P001", "P002", "P002", "P003"]
unique_participants = set(participants)
print(unique_participants)  # {'P001', 'P002', 'P003'}

I use sets a lot to check whether I have the right data before running an analysis. For example, I can compare the columns in a dataset against the columns I expect:

# Columns required for the analysis
required = {"participant", "trial", "reaction_time", "accuracy"}

# Columns in the dataset
columns = {"participant", "trial", "reaction_time", "condition"}

# Find missing columns
missing = required - columns

if missing:
    print(f"Missing columns: {missing}")
else:
    print("All required columns are present!")

Try it yourself:

  • Create a list with repeated numbers.
  • Convert it to a set to remove duplicates.
  • Check if a specific number is in the set.

Control Flow

If / elif / else

Use these to branch your code based on conditions. In research, this might mean adjusting difficulty based on performance or flagging poor-quality data.

accuracy = 0.82

if accuracy > 0.9:
    print("Excellent performance!")
elif accuracy > 0.75:
    print("Good performance!")
else:
    print("Needs improvement.")

Let's walk through this code. Basically the first if statement checks if accuracy is greater than 0.9. Only if this evaluates to True will the indented line run. If this happens, the rest of the elif or else statements will be skipped. If it evalutes to False, then the next line tries. This continues down until a condition evaluates to True or it reaches an else statement which will always run if everything else is False .

Loops

Loops let you repeat actions without writing the same code over and over.

# for loop - iterates over a sequence of values
for trial in range(1, 6):
    print(f"Trial {trial}")

# while loop - runs until a condition is false
countdown = 3
while countdown > 0:
    print(countdown)
    countdown -= 1

For loops contain 3 main components:

  1. Iterator (loop variable): trial — holds the current value during each iteration.
  2. Iterable (range): range(1, 6) — provides the sequence of values (1 through 5) that the loop iterates over.
  3. Code block (loop body): print(f"Trial {trial}") — the indented code executed during each iteration.

Try it yourself:

  • Write an if/elif/else statement that prints different feedback based on a variable called score.
  • Write a for loop that prints all even numbers from 2 to 20.
  • Write a while loop that counts down from 5 to 1.

Functions: Reusable Code Blocks

Functions package code into named, reusable pieces. They keep scripts easier to read, test, and debug.

def mean(values):
    return sum(values) / len(values)

rt_list = [350, 420, 390]
print(mean(rt_list))  # 386.666...

A Python function has four main components:

  1. Function definition: def — keyword used to define a function.
  2. Function name: mean — identifies the function so it can be called.
  3. Parameter: values — receives the input data passed to the function.
  4. Function body: return sum(values) / len(values) — performs the calculation and returns the result.

You can also add default parameters. Basically values to default to if the user doesn't specify.

def greet(name, greeting="Hello"):
    print(f"{greeting}, {name}!")

greet("Gregg")           # Hello, Gregg!
greet("Gregg", "Hi")     # Hi, Gregg!

Try it yourself:

  • Write a function that returns the square of a number.
  • Write a function that takes a list of numbers and returns the largest one.
  • Write a function with a default parameter, then call it with and without providing that parameter.

Mini-Project: Participant Performance Tracker

Here is a small example that does not require any extra files.

def mean(values):
    return sum(values) / len(values)

# Accuracy for each participant across three blocks
results = {
    "P001": [0.9, 0.8, 0.85],
    "P002": [0.7, 0.75, 0.72],
    "P003": [0.95, 0.92, 0.94]
}

# loop through each participant and print their mean accuracy
for pid, accs in results.items():
    avg_acc = mean(accs)
    if avg_acc > 0.9:
        performance = "Excellent"
    elif avg_acc > 0.75:
        performance = "Good"
    else:
        performance = "Needs improvement"
    print(f"{pid}: {avg_acc:.2f} - {performance}")

Try it yourself:

  • Add another participant with their own accuracy scores.
  • Write a new function that returns the highest single accuracy for a participant.
  • Sort participants by their average accuracy and print them from highest to lowest.

That's Part II. If you went through the "try it yourself" sections, you know enough Python to automate some tedious research tasks. Part III brings in NumPy, Pandas, and Matplotlib.