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} sets Limit, because Limit is the limit_key. Limit bounds items read, never items returned, so with a FilterExpression a page can be empty and still have cost.
  • MaxItems counts result_key items and hands back a NextToken you can pass as StartingToken in a later process. It does not stop the request from reading past your cutoff.
  • build_full_result() aggregates only the result_key fields. Items, Count and ScannedCount are summed; ConsumedCapacity is a non_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.
  • FilterExpression runs server-side after the read, so you are billed on ScannedCount, not Count. #filter0 aliases Year because it is a reserved word; without the alias the request fails before it reads anything.
  • Errors all arrive as botocore.exceptions.ClientError. Branch on e.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.scan takes native Python types, returns numbers as decimal.Decimal, and builds filters with Attr("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.0

Four 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.

References

Last verified 2026-07-28 against the official AWS documentation linked above.

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.