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 indexes

The 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 modelled 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

  • IndexName does not replace TableName. Both go in the same call, and the KeyConditionExpression names 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 the INCLUDE list); per the API reference "global secondary index queries cannot fetch attributes from the parent table". A missing attribute means a follow-up get_item on 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 and Decimal out.
  • 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.

References

Build this request visually

Compose this operation in the free DynamoDB Query Builder — key condition, filter, index, Limit, sort order, and a pagination loop — and copy it back as a runnable SDK v3, CLI, or boto3 program.

Open the DynamoDB Query Builder

Work with DynamoDB without the Console

A fast DynamoDB desktop client that runs the real SQL DynamoDB can’t — JOINs, GROUP BY, aggregates — with visual editing and an AI agent on your own Bedrock keys.

Free 30-day trial, no credit card — then the Free plan with no time limit.