DynamoDB Scan in Python (boto3)
A scan in boto3 is two decisions: the request, and how you page it. The snippet below uses the built-in paginator, which is not a wrapper someone wrote around your loop. It is five lines of botocore config, and those five lines decide whether your scan is correct and what it costs. (Whether you should be scanning at all is a different question.)
Code
import boto3
client = boto3.client("dynamodb")
paginator = client.get_paginator("scan")
items = []
for page in paginator.paginate(
TableName="Music",
FilterExpression="#filter0 >= :filterValue0",
ExpressionAttributeNames={"#filter0": "Year"},
ExpressionAttributeValues={":filterValue0": {"N": "2010"}},
):
items.extend(page["Items"])
print(f"Matched {len(items)} items")Explanation
- The paginator — data, not code. botocore ships one entry per operation in
paginators-1.json;Scan's reads{"input_token": "ExclusiveStartKey", "output_token": "LastEvaluatedKey", "limit_key": "Limit", "result_key": ["Items", "Count", "ScannedCount"], "non_aggregate_keys": ["ConsumedCapacity"]}. Everything below falls out of those keys. PaginationConfig={"PageSize": n}setsLimit, becauseLimitis thelimit_key.Limitbounds items read, never items returned, so with aFilterExpressiona page can be empty and still have cost.MaxItemscountsresult_keyitems and hands back aNextTokenyou can pass asStartingTokenin a later process. It does not stop the request from reading past your cutoff.build_full_result()aggregates only theresult_keyfields.Items,CountandScannedCountare summed;ConsumedCapacityis anon_aggregate_key, so the merged result reports one page's capacity as though it were the whole scan's. Sum it yourself, per page, or you will under-report by a factor of the page count.FilterExpressionruns server-side after the read, so you are billed onScannedCount, notCount.#filter0aliasesYearbecause it is a reserved word; without the alias the request fails before it reads anything.- Errors all arrive as
botocore.exceptions.ClientError. Branch one.response["Error"]["Code"]; the per-error classes exist only as attributes generated on the client (client.exceptions.ProvisionedThroughputExceededException), never as importable symbols. - The resource API — the other ergonomics:
Table.scantakes native Python types, returns numbers asdecimal.Decimal, and builds filters withAttr("Year").gte(2010)instead of placeholder maps.
What one filtered page actually costs
60 items of roughly 2 KB each, Year = 2024 matching two of them, PageSize=10, run against DynamoDB Local:
page 1: Count=0 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 2: Count=1 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 3: Count=0 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 4: Count=1 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 5: Count=0 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 6: Count=0 ScannedCount=10 CU=2.5 LastEvaluatedKey=yes
page 7: Count=0 ScannedCount=0 CU=0.0 LastEvaluatedKey=no
total CU across pages: 15.0Four of the six real pages returned nothing, at full price. That is the shape of the bug the paginator exists to prevent: a hand-rolled loop that breaks when Items is empty quits on page 1 and reports two matching songs as zero.
Page 7 is the other half. Page 6 hit its Limit on the last item in the table, so DynamoDB returned a LastEvaluatedKey anyway and the paginator spent one more round trip to learn there was nothing left. A LastEvaluatedKey means "I stopped", not "there is more".
Calling build_full_result() on the same scan reports CapacityUnits: 2.5. The six pages consumed 15.0.
Paging without writing the loop
The DynamoDB Query Builder assembles the filter, the alias map and the pagination loop as one runnable program, so the Limit-versus-Count trap above is handled before you paste it. To page a real table interactively rather than from a script, download DynoTable.
Related guides
- Query vs. Scan — when (rarely) a
scanis justified. - Why is my DynamoDB Scan slow and expensive? — the cost model and how to avoid it.
- Parallel scans — splitting the table with
Segment/TotalSegments, one paginator per segment. - DynamoDB ProvisionedThroughputExceededException — what a full-table scan does to a provisioned table's capacity.
References
- Scan — Amazon DynamoDB API Reference
- scan — Boto3 DynamoDB.Client Reference
- Scan paginator — Boto3 DynamoDB Reference
- Scanning tables — Amazon DynamoDB Developer Guide
Last verified 2026-07-28 against the official AWS documentation linked above.