DynamoDB GetItem in Python (boto3)
get_item fetches one item by its full primary key. The boto3 low-level client (boto3.client("dynamodb")) speaks DynamoDB JSON in both directions, so the key goes in wrapped with its type and the item comes back the same way. How it differs from query and scan is covered in item-based actions.
Code
import boto3
client = boto3.client("dynamodb")
response = client.get_item(
TableName="Music",
Key={"Artist": {"S": "Arturo Sandoval"}, "SongTitle": {"S": "Cubano Chant"}},
ProjectionExpression="#proj0, #proj1, #proj2, #proj3",
ExpressionAttributeNames={"#proj0": "Artist", "#proj1": "SongTitle", "#proj2": "AlbumTitle", "#proj3": "Year"},
)
item = response.get("Item")
if item is None:
print("Item not found")
else:
print(item)Explanation
A miss returns a response with no Item key at all. Not None, not an empty dict. Reading the same table for a key that does not exist, the response's top-level keys were exactly:
['ResponseMetadata']That is why the snippet uses response.get("Item"). response["Item"] raises KeyError on the ordinary not-found path, which is how a missing row turns into a 500 in a web handler. You are still billed for the read: AWS's read-capacity page states that "if you perform a read operation on an item that doesn't exist, DynamoDB will still consume read throughput as outlined above" (fetched 2026-07-28).
Year is a reserved word, which is why the generated snippet aliases every projected attribute. Drop the #proj aliases and pass ProjectionExpression="Year" and the engine rejects the read:
ValidationException: Invalid ProjectionExpression: Attribute name is a reserved keyword; reserved keyword: YearAliasing unconditionally costs nothing and removes the whole class of failure. The full list is 573 words long; see "Attribute name is a reserved keyword".
Four ways to get the Key wrong, three different messages. They are worth knowing apart, because none of them is the "provided key element does not match the schema" error people expect. Reproduced against a Music table keyed on Artist (partition) + SongTitle (sort):
| What you passed | Verbatim ValidationException message |
|---|---|
{"Artist": …} — sort key missing | The number of conditions on the keys is invalid |
{"Artist": …, "SongTitle": …, "Extra": …} | The number of conditions on the keys is invalid |
{"Artist": …, "Song": …} — wrong attribute name | One of the required keys was not given a value |
{"Artist": {"N": "1"}, …} — wrong type | One or more parameter values were invalid: Type mismatch for key |
Note that a missing key attribute and an extra one produce the same message, so "number of conditions" means "you did not hand me exactly the key schema", not "you passed too few".
ProjectionExpression cuts the payload, not the bill. Reading a ~15 KB item three ways with ReturnConsumedCapacity="TOTAL":
full item, eventually consistent CapacityUnits: 2.0
ProjectionExpression="#y" (Year only) CapacityUnits: 2.0
ProjectionExpression="#y" + ConsistentRead CapacityUnits: 4.0The projection changed the response from ~15 KB to a single number and changed the cost by nothing. AWS states it plainly: "The number of capacity units consumed will be the same whether you request all of the attributes (the default behavior) or just some of them (using a projection expression)" (Query API Reference, fetched 2026-07-28). ConsistentRead=True is the only flag on that list that moves the number, and it doubles it. See projection expressions for what projections are actually for.
The resource API is a different contract, not a nicer spelling. boto3.resource("dynamodb").Table("Music").get_item(...) returns plain Python and every number as decimal.Decimal:
{'Artist': 'Arturo Sandoval', 'AlbumTitle': 'Danzon', 'Awards': Decimal('0'), 'Year': Decimal('1994'), 'SongTitle': 'Cubano Chant'}That cuts both ways. Writing back through the same API with a float raises before the request leaves your machine:
TypeError: Float types are not supported. Use Decimal types instead.If that one bites you, "Float types are not supported" has the fix. Mixing the two APIs in one codebase is the real trap: the low-level client will happily accept {"N": "1.5"} that the resource API would have rejected.
Errors arrive as botocore exceptions, and boto3 gives them real classes. On 1.43.58 the object raised for a failed condition is ConditionalCheckFailedException, a ClientError subclass, so except ClientError plus a err.response["Error"]["Code"] check and except client.exceptions.ConditionalCheckFailedException both work. Prefer whichever your codebase already uses; do not match on str(e).
Do it visually
Before you alias by hand: the free DynamoDB reserved-words checker takes your attribute names, tells you which of the 573 reserved words you hit, and emits the ExpressionAttributeNames map ready to paste.
To browse tables and run GetItem against your own data — key form, results grid, copy the request back out as boto3 — download DynoTable.
Related guides
- Query vs. Scan — when a single
get_itembeats aquery. - DynamoDB data types — how each attribute type is represented in DynamoDB JSON.
- DynamoDB ResourceNotFoundException — the usual first error here: wrong table name or region.
- "The provided key element does not match the schema" — the key you pass doesn't match the table's key schema.
References
- GetItem — Amazon DynamoDB API Reference
- get_item — Boto3 DynamoDB.Client Reference
- Read consistency — Amazon DynamoDB Developer Guide
- Capacity unit consumption — Amazon DynamoDB Developer Guide
Reproduced 2026-07-28 against DynamoDB Local (amazon/dynamodb-local) on port 9000 with boto3 1.43.58 / botocore 1.43.58. Every message and capacity figure above is engine output, copied verbatim. DynamoDB Local is not the service; where the two are known to word an error differently we say so on the error page.