DynamoDB Query a GSI in Python (boto3)
A GSI query is a normal query plus IndexName, and AlbumTitle-index gives us songs by album, an access pattern the Artist + SongTitle table key cannot serve. What changes in Python is error handling: the two most common index mistakes fail in different layers of boto3, and only one of them can be caught by exception class.
Code
import boto3
client = boto3.client("dynamodb")
paginator = client.get_paginator("query")
items = []
for page in paginator.paginate(
TableName="Music",
IndexName="AlbumTitle-index",
KeyConditionExpression="#hashKey = :hashKeyValue",
ExpressionAttributeNames={"#hashKey": "AlbumTitle"},
ExpressionAttributeValues={":hashKeyValue": {"S": "Danzon"}},
):
items.extend(page["Items"])
print(f"Found {len(items)} songs on the album")except ValidationException will not compile, let alone catch
Add ConsistentRead=True to the query above and boto3 raises this, on the client and the resource API alike:
botocore.exceptions.ClientError: An error occurred (ValidationException) when
calling the Query operation: Consistent reads are not supported on global
secondary indexesThe obvious handler is except client.exceptions.ValidationException. It does not exist:
AttributeError: <botocore.errorfactory.DynamoDBExceptions object> has no
attribute ValidationException. Valid exceptions are: BackupInUseException,
... IndexNotFoundException, ... ProvisionedThroughputExceededException, ...botocore generates exception classes from the service model, and DynamoDB models 33 of them. ValidationException is a protocol-level error and is not one, so the only reliable branch is on the code:
except ClientError as exc:
if exc.response["Error"]["Code"] == "ValidationException":
...The asymmetry is real. Mistype the index name and you get IndexNotFoundException, which is modeled and catchable by class. Misuse the consistency flag and you get a string comparison. Both are index errors; only one has a type.
The cursor carries the table key too
The paginator hides LastEvaluatedKey, but it is worth knowing what it holds on an index. Over 300 songs on one album:
page 1: Count 271 capacity 128.5 LastEvaluatedKey ['AlbumTitle', 'Artist', 'SongTitle']
page 2: Count 29 capacity 14.0 LastEvaluatedKey []A GSI key is not unique, so the index key alone cannot resume the read; DynamoDB returns the index key and the base-table key together. Hand-rolled paging that stores only the index key repeats or drops items.
Reproduced 2026-07-28 against DynamoDB Local (amazon/dynamodb-local) with boto3 1.43.58 on CPython 3.14.6. The error text and key lists are the library's own output.
Explanation
IndexNamedoes not replaceTableName. Both go in the same call, and theKeyConditionExpressionnames the index's partition key (AlbumTitle) with the same operator set as a table query.- You get the projection and nothing else. The index returns what it projects (
ALL,KEYS_ONLY, or theINCLUDElist); per the API reference "global secondary index queries cannot fetch attributes from the parent table". A missing attribute means a follow-upget_itemon the base key, or a wider projection on a new index. - Items missing the index key never appear — the sparse-index pattern. It keeps an index over
status = "OPEN"small, and it is also why a GSI query can return less than you expect and raise nothing. - The resource API takes the same
IndexName:table.query(IndexName="AlbumTitle-index", KeyConditionExpression=Key("AlbumTitle").eq("Danzon")), with native Python values in andDecimalout. - A GSI write lands after the table write. Replication is asynchronous, so a read-after-write path against the index will occasionally miss. Retrying it in a tight loop burns capacity without making replication faster.
Do it visually
The DynamoDB Expression Builder writes the index key condition and the typed value map as boto3-ready Python, including the {"S": ...} wrappers the client insists on and the resource API forbids.
To point the same index query at your own tables from a form and read the results in a paginated grid, download DynoTable.
Related examples
- DynamoDB Query a GSI in Node.js — the same index query with AWS SDK v3.
- DynamoDB Query a GSI with the AWS CLI — the same index query from the shell.
- DynamoDB Query in Python — querying the base table.
- GSI vs. LSI — which index type fits the access pattern.
- Why GSIs are eventually consistent — the replication lag explained.
- "The table does not have the specified index" — the index name doesn't match (GSI names are case-sensitive).
- "Consistent reads are not supported on global secondary indexes" — why the consistent-read flag fails on a GSI.