DynamoDB Query in Python (boto3)

boto3's query paginator is the reason this page is short: it hides LastEvaluatedKey entirely. It also hides one number you probably wanted, which is the part worth knowing before you trust it. For when to reach for query at all, see Query vs. Scan.

Code

import boto3

client = boto3.client("dynamodb")

paginator = client.get_paginator("query")

items = []
for page in paginator.paginate(
    TableName="Music",
    KeyConditionExpression="#hashKey = :hashKeyValue AND begins_with(#rangeKey, :rangeKeyValue)",
    ExpressionAttributeNames={"#hashKey": "Artist", "#rangeKey": "SongTitle"},
    ExpressionAttributeValues={":hashKeyValue": {"S": "Arturo Sandoval"}, ":rangeKeyValue": {"S": "C"}},
):
    items.extend(page["Items"])

print(f"Found {len(items)} items")

The paginator does not add up your bill

Against a 600-song fixture, every song ~3.9 KB and all under Artist = "Arturo Sandoval", the loop yields three pages: 271, 271 and 58 items, costing 128.5, 128.5 and 27.5 read units. Ask the same paginator for one merged result and you get this:

build_full_result() -> Items 600  Count 600  ScannedCount 600
                       ConsumedCapacity.CapacityUnits 128.5

Count and ScannedCount were summed. ConsumedCapacity was not — it is the first page's figure, and the real total was 284.5. botocore's DynamoDB paginator config is explicit about why: Count and ScannedCount are listed as result keys, ConsumedCapacity as a non-aggregate key. If you are logging capacity from build_full_result(), you are under-reporting a full-partition read by more than half.

The per-page dicts in the for page in paginator.paginate(...) loop above are raw responses, so summing page["ConsumedCapacity"]["CapacityUnits"] yourself gives the honest 284.5.

The Limit that costs you 58 extra round trips

Limit is a valid query parameter, so paginate() accepts it, and it is not the parameter Python users expect:

paginate(..., Limit=10)  ->  61 pages, 10 items each
paginate(...)            ->   3 pages

It caps items per request, not overall, so the paginator dutifully makes 61 HTTP calls to fetch the same 600 items. To cap the total, use PaginationConfig={"MaxItems": 10}; PaginationConfig["PageSize"] is the knob that maps to Limit.

Measured 2026-07-28 against DynamoDB Local (amazon/dynamodb-local) with boto3 1.43.58 on CPython 3.14.6.

Explanation

  • The client speaks DynamoDB JSON in both directions. Values go in as {"S": "Arturo Sandoval"} and Year comes back as {"N": "1994"}. The resource API (boto3.resource("dynamodb").Table(...).query) converts both ways and hands you Decimal('1994') — which is right for money and surprising the first time it refuses to add to a float.
  • Key("Artist").eq(...) belongs to the resource API only. Passing it to the client raises before the request leaves: ParamValidationError: Invalid type for parameter KeyConditionExpression ... valid types: <class 'str'>. The client wants the expression string this page builds.
  • The key condition is one equality plus at most one sort-key comparison (=, <, <=, >, >=, BETWEEN, begins_with). Put anything else in a FilterExpression, which boto3 passes straight through and DynamoDB applies after the read. ScanIndexForward=False reverses the order, IndexName="..." retargets an index.

Do it visually

The DynamoDB Expression Builder writes the key condition and the typed ExpressionAttributeValues map as boto3-ready Python, which is the part that goes wrong when you type {"N": 2010} instead of {"N": "2010"}.

To point the same query at your own tables from a key-condition 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.