add_task from last week completed the Calendar and Tasks loop. Now your research progress data lives in a shared Google Sheet — date, what you did, status. How do you read those rows in Python?
GOOGLESHEETS_BATCH_GET — I guessed the naming pattern. Takes a sheet ID and a range in A1 notation. Returns... a list of rows?
A 2D list — rows as lists, cells as strings. BATCH_GET takes spreadsheet_id and a ranges list. The response comes back nested:
result = toolset.execute_action(
Action.GOOGLESHEETS_BATCH_GET,
{"spreadsheet_id": "your-sheet-id", "ranges": ["Sheet1!A1:D10"]}
)
value_ranges = result.get("valueRanges", [])
if value_ranges:
rows = value_ranges[0].get("values", [])
for row in rows:
print(row) # ['2026-04-12', 'Reviewed 50 responses', 'On track']result.get('valueRanges', []) and then value_ranges[0].get('values', []) — why two levels of .get()?
The Sheets API nests the data: the outer dict has valueRanges (a list of range objects). Each range object has a values key with the actual rows. Two levels of nesting, two levels of safe access:
def read_range(sheet_id: str, range_a1: str) -> list:
result = toolset.execute_action(
Action.GOOGLESHEETS_BATCH_GET,
{"spreadsheet_id": sheet_id, "ranges": [range_a1]}
)
value_ranges = result.get("valueRanges", [])
rows = value_ranges[0].get("values", []) if value_ranges else []
print(f"Read {len(rows)} rows from {range_a1}")
return rowsA row in Sheets is just a list of strings. I can filter, sort, and summarise this with all the Python I learned in the Python Track. Everything connects.
Your research-progress Sheet is now a Python list. The same loops, comprehensions, and dict operations apply.
I'm thinking about the Sheets data as a list of lists before I even write the function. That's the systems thinking moment — I predicted the data structure.
Empty rows at the bottom of the sheet may appear as empty lists [] in the response. Filter them with [r for r in rows if r] before processing. Sheets also trims trailing empty cells in each row — a row with 4 columns may come back as a 2-element list if the last two cells are blank.
Params: spreadsheet_id (sheet URL ID), ranges (list of A1 strings)
{
"valueRanges": [
{
"range": "Sheet1!A1:D10",
"values": [["row1col1", "row1col2"], ["row2col1", ...]]
}
]
}"Sheet1!A1:D10" — sheet name, !, top-left cell : bottom-right cell. "A:D" reads the entire A–D column range without specifying the last row.
From the URL: docs.google.com/spreadsheets/d/**SPREADSHEET_ID**/edit. The ID is the long alphanumeric string between /d/ and /edit. Store it as a variable at the top of your script rather than embedding it in each function call.
add_task from last week completed the Calendar and Tasks loop. Now your research progress data lives in a shared Google Sheet — date, what you did, status. How do you read those rows in Python?
GOOGLESHEETS_BATCH_GET — I guessed the naming pattern. Takes a sheet ID and a range in A1 notation. Returns... a list of rows?
A 2D list — rows as lists, cells as strings. BATCH_GET takes spreadsheet_id and a ranges list. The response comes back nested:
result = toolset.execute_action(
Action.GOOGLESHEETS_BATCH_GET,
{"spreadsheet_id": "your-sheet-id", "ranges": ["Sheet1!A1:D10"]}
)
value_ranges = result.get("valueRanges", [])
if value_ranges:
rows = value_ranges[0].get("values", [])
for row in rows:
print(row) # ['2026-04-12', 'Reviewed 50 responses', 'On track']result.get('valueRanges', []) and then value_ranges[0].get('values', []) — why two levels of .get()?
The Sheets API nests the data: the outer dict has valueRanges (a list of range objects). Each range object has a values key with the actual rows. Two levels of nesting, two levels of safe access:
def read_range(sheet_id: str, range_a1: str) -> list:
result = toolset.execute_action(
Action.GOOGLESHEETS_BATCH_GET,
{"spreadsheet_id": sheet_id, "ranges": [range_a1]}
)
value_ranges = result.get("valueRanges", [])
rows = value_ranges[0].get("values", []) if value_ranges else []
print(f"Read {len(rows)} rows from {range_a1}")
return rowsA row in Sheets is just a list of strings. I can filter, sort, and summarise this with all the Python I learned in the Python Track. Everything connects.
Your research-progress Sheet is now a Python list. The same loops, comprehensions, and dict operations apply.
I'm thinking about the Sheets data as a list of lists before I even write the function. That's the systems thinking moment — I predicted the data structure.
Empty rows at the bottom of the sheet may appear as empty lists [] in the response. Filter them with [r for r in rows if r] before processing. Sheets also trims trailing empty cells in each row — a row with 4 columns may come back as a 2-element list if the last two cells are blank.
Params: spreadsheet_id (sheet URL ID), ranges (list of A1 strings)
{
"valueRanges": [
{
"range": "Sheet1!A1:D10",
"values": [["row1col1", "row1col2"], ["row2col1", ...]]
}
]
}"Sheet1!A1:D10" — sheet name, !, top-left cell : bottom-right cell. "A:D" reads the entire A–D column range without specifying the last row.
From the URL: docs.google.com/spreadsheets/d/**SPREADSHEET_ID**/edit. The ID is the long alphanumeric string between /d/ and /edit. Store it as a variable at the top of your script rather than embedding it in each function call.
Create a free account to get started. Paid plans unlock all tracks.