Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 3

Processing and Validating Benchmark Records with Lists, Dictionaries, JSON, and CSV

Recommended resources

Welcome to the third lesson in our Python foundations module. In our previous lesson, you learned how to use conditionals and loops to control the flow of a program. These are the tools that allow your code to make decisions and repeat tasks.

Today, we'll put that control flow to work on a central task in any performance engineering role: processing data. To do that, we first need ways to store and organize collections of data. You'll learn about two of Python's most important data structures, lists and dictionaries. Then, we'll use them to read, transform, and write benchmark records using two standard file formats: CSV and JSON. A crucial part of this process is ensuring our program is robust, so we will also cover how to handle and reject malformed or invalid records.

By the end of this lesson, you will be able to write a script that can take a file of raw benchmark measurements, process it, and output a clean, structured summary.

Storing Collections: Lists and Dictionaries

In the last lesson, you saw a for loop iterate over a sequence. The most common sequence you'll work with is a Python list. A list is an ordered collection of items, which can be of any type. You create them with square brackets [].




# A list of model names (strings)
model_names = ["Llama-3-8B", "Mistral-7B", "Gemma-2B"]



# A list of latencies (floats)
latencies_ms = [12.5, 9.8, 12.7, 13.1]




# You can access items by their zero-based index
first_model = model_names[0]  # "Llama-3-8B"
second_latency = latencies_ms[1] # 9.8




# And loop over them
for model in model_names:
    print(f"Found model: {model}")

While lists are great for ordered sequences, benchmark records usually have more structure. For example, a latency measurement is meaningless without knowing which model and batch size it belongs to. For this, we use a dictionary. A dictionary stores a collection of key-value pairs, created with curly braces {}. This allows you to associate named pieces of data, which is perfect for representing a single, structured record.




# A dictionary representing a single benchmark record
benchmark_record = {
    "model_name": "Llama-3-8B",
    "batch_size": 8,
    "latency_ms": 123.4
}




# You access values by their key, not by a numeric index
model = benchmark_record["model_name"] # "Llama-3-8B"
latency = benchmark_record["latency_ms"] # 123.4




# You can easily combine lists and dictionaries
all_records = [
    {"model_name": "Llama-3-8B", "batch_size": 8, "latency_ms": 123.4},
    {"model_name": "Mistral-7B", "batch_size": 16, "latency_ms": 95.2}
]




# Now we can loop through records and access data by name
for record in all_records:
    print(f"Model {record['model_name']} at batch size {record['batch_size']} had a latency of {record['latency_ms']}ms.")

Using dictionaries for records makes your code far more readable and less error-prone than relying on list indices (e.g., record['latency_ms'] is much clearer than record[2]). We will use this pattern extensively.


Working with CSV Files

CSV (Comma-Separated Values) is a simple text format for storing tabular data, like you might find in a spreadsheet. It's a common way to log raw results from benchmark runs. Python's built-in csv module makes it easy to work with these files.

While you can read CSV rows as lists, it's far more effective to read them directly as dictionaries, using the file's header row as the keys. The following video from Corey Schafer provides an excellent introduction to the csv module and explains the benefits of using DictReader.

Python Tutorial: CSV Module - How to Read, Parse, and Write CSV Files

Watch the following segments of this tutorial on the CSV module. The first part introduces CSV files and the basic csv.reader. The second, more important part introduces DictReader and DictWriter, which are the tools you'll use most often for handling structured records.

Introduction to CSV: Watch from the beginning to understand the file format and the purpose of the csv module until the first code example. Reading with csv.reader: See how to read rows as lists in this section. Notice how you have to access data by index (e.g., line[2]). Reading with csv.DictReader: Now, watch the section on DictReader here. Pay close attention to how it automatically uses the header row and lets you access data by name (e.g., line['email']). Writing with csv.DictWriter: Finally, see how DictWriter lets you write a list of dictionaries back to a file in this segment.

As you saw, DictReader and DictWriter are powerful. They treat rows not as simple lists of strings, but as the structured records we need for benchmark analysis.

For a more formal description of these objects, you can refer to the official Python documentation.

CSV File Reading and Writing

This is the official documentation for the classes you just saw in the video. It provides a definitive reference for how they work.

Focus on the descriptions and short usage examples for csv.DictReader and csv.DictWriter. Note how DictWriter requires the fieldnames parameter to define the header and the order of columns.


Rejecting Malformed Records with try...except

Real-world data is rarely perfect. A benchmark log might contain corrupted lines, missing values, or text where a number should be. If your program tries to process this invalid data, it will crash.

To build robust programs, you need to anticipate and handle these errors. In Python, the standard way to do this is with a try...except block. The logic is: "Try to run this code, but if a specific error occurs, don't crash. Instead, run the code in the except block."

For example, what if we try to convert the string 'corrupted' to a number?

value_str = 'corrupted'
try:



    # This line will fail
    value_float = float(value_str)
    print(f"Successfully converted to {value_float}")
except ValueError:



    # The code jumps here when float() fails with a ValueError
    print(f"Could not convert '{value_str}' to a float. Skipping.")

This is essential for data processing. You can wrap your data conversion and processing logic in a for loop inside a try...except block to safely skip any record that can't be parsed correctly.

Different errors have different names. ValueError is common for type conversions. If you try to access a dictionary key that doesn't exist, you'll get a KeyError. If a CSV file itself is structured incorrectly (e.g., an unclosed quote), the csv module may raise a csv.Error.

This reading from the official documentation shows a practical example of catching csv.Error.

CSV File Reading and Writing

This section provides several code examples. We are interested in the one that demonstrates error handling.

Find the example under the heading "A slightly more advanced use of the reader". This shows how to wrap the loop over the CSV reader in a try...except csv.Error block to catch parsing errors and report them gracefully instead of crashing.


Working with JSON Files

Another ubiquitous data format is JSON (JavaScript Object Notation). While CSV is great for simple tables, JSON excels at representing more complex, nested data structures. It's the de facto standard for web APIs and is also widely used for configuration files. The syntax maps almost directly to Python dictionaries and lists.

Python's json module provides the tools to translate between Python objects and JSON-formatted text.

  • json.load(): Reads from a JSON file and converts it into a Python object (usually a dictionary or list).
  • json.dump(): Takes a Python object and writes it to a file in JSON format.
  • json.loads() / json.dumps(): The 's' stands for string. These work with JSON data in strings instead of files.

This video from Corey Schafer covers the essential file-based operations.

Python Tutorial: Working with JSON Data using the json Module

This video provides a complete guide to the json module. For our purposes, focus on the part that deals with reading from and writing to files.

Watch the segment on loading and dumping JSON files. Pay attention to the json.load() function for reading a file into a Python dictionary, and json.dump() for writing a dictionary back to a file. Notice the use of the indent argument to produce human-readable output, which is very helpful.


A Practical Example: Summarizing Benchmark Data

Let's tie all these concepts together. Imagine you have a file raw_results.csv with latency measurements from multiple benchmark runs. The data is messy and includes some invalid entries.

raw_results.csv:

model,batch_size,latency_ms
Llama-3-8B,8,123.4
Mistral-7B,16,95.2
Llama-3-8B,8,125.1
Mistral-7B,16,invalid
Llama-3-8B,8,124.8
Mistral-7B,16,97.0

Your goal is to calculate the average latency for each configuration (model and batch_size), ignoring any invalid rows, and save the result as a clean summary.json file.

Here's how you could write a script to do this:

import csv
import json




# Use a dictionary to store intermediate results.
# The key will be a (model, batch_size) tuple.
# The value will be a list of valid latencies for that config.
# e.g., {('Llama-3-8B', 8): [123.4, 125.1, 124.8]}
processed_data = {}




# Open the CSV file to read the raw results
with open('raw_results.csv', mode='r', newline='') as csv_file:



    # Use DictReader to treat each row as a dictionary
    reader = csv.DictReader(csv_file)
    
    print("Processing raw results...")
    for row in reader:
        try:



            # 1. Extract and validate data
            model_name = row['model']
            batch_size = int(row['batch_size'])
            latency = float(row['latency_ms'])
            



            # 2. Create a unique key for this configuration
            config_key = (model_name, batch_size)
            



            # 3. Store the valid latency
            if config_key not in processed_data:
                processed_data[config_key] = []
            processed_data[config_key].append(latency)

        except (ValueError, KeyError) as e:



            # Catch errors from failed int()/float() conversions or missing keys
            print(f"--> Skipping malformed row: {row}. Reason: {e}")




# Now, calculate the average for each configuration
summary_list = []
for config, latencies in processed_data.items():
    avg_latency = sum(latencies) / len(latencies)
    
    summary_list.append({
        "model": config[0],
        "batch_size": config[1],
        "num_runs": len(latencies),
        "avg_latency_ms": round(avg_latency, 2) # Round to 2 decimal places
    })

print("Writing summary to summary.json...")



# Write the final summary list to a JSON file
with open('summary.json', mode='w') as json_file:



    # Use indent for a nicely formatted, human-readable file
    json.dump(summary_list, json_file, indent=4)

print("Done.")

If you run this script, it will print messages about the skipped row and create a summary.json file that looks like this:

summary.json:

[
    {
        "model": "Llama-3-8B",
        "batch_size": 8,
        "num_runs": 3,
        "avg_latency_ms": 124.43
    },
    {
        "model": "Mistral-7B",
        "batch_size": 16,
        "num_runs": 2,
        "avg_latency_ms": 96.1
    }
]

This example demonstrates the entire workflow: reading structured data (DictReader), validating and transforming it using control flow and error handling (for, try...except), and writing a structured summary (json.dump).


Conclusion

In this lesson, you've acquired a powerful set of tools for handling data in Python, a fundamental skill for performance engineering. You can now take raw data from common file formats, process it robustly, and generate structured output.

Here are the key takeaways:

  • Lists ([]) store ordered sequences of items, accessed by index.
  • Dictionaries ({}) store unordered key-value pairs, perfect for representing structured records accessed by name.
  • The csv module's DictReader and DictWriter are the preferred tools for handling tabular data like benchmark logs.
  • The json module's load and dump functions allow you to easily read and write complex, nested data structures.
  • try...except blocks are essential for writing robust code that can handle errors and invalid data without crashing.

So far, we've been writing our logic directly in scripts. As our tasks become more complex, we'll need a way to organize our code into reusable and testable components. In the next lesson, you'll learn how to do just that by defining your own functions.