DynamoDB Scan in Python (boto3)
scan reads every item in a table or index, page by page, then optionally drops rows with a filter. With the boto3 resource API you express the filter with the Attr helper from boto3.dynamodb.conditions.
Code
import boto3
from boto3.dynamodb.conditions import Attr
dynamodb = boto3.resource("dynamodb", region_name="us-east-1")
table = dynamodb.Table("Music")
def scan_recent_songs():
items = []
kwargs = {"FilterExpression": Attr("Year").gte(2010)}
while True:
response = table.scan(**kwargs)
items.extend(response["Items"])
# Keep going until every 1 MB page is read
if "LastEvaluatedKey" not in response:
break
kwargs["ExclusiveStartKey"] = response["LastEvaluatedKey"]
print(f"Matched {len(items)} songs")
return items
if __name__ == "__main__":
scan_recent_songs()Explanation
Attr("Year").gte(2010)— theAttrhelper builds the filter expression, names, and values. Combine conditions with&/|(e.g.Attr("Genre").eq("Jazz") & Attr("Year").gte(2010)).Yearbeing a reserved word is handled for you.- Filtering is post-read — it shrinks the response, not the cost. You pay to read every item scanned.
- Pagination is mandatory — each page reads at most 1 MB before filtering, so a filtered page can be empty while
LastEvaluatedKeyis still set. Loop until it's absent or you'll miss matches. - For a large full-table scan, parallelize with
Segment/TotalSegments. - Numeric attributes return as
decimal.Decimal— compare againstDecimal/int, notfloat. - Prefer
query.scanreads the whole table; on anything but a tiny table it's slow and expensive.
Do it visually
The DynamoDB Expression Builder assembles the Attr(...) filter and copies runnable code.
To explore tables in a GUI — filtered, paginated result grids and copy-as-code — download DynoTable, instead of scanning blind from a script.
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.