DynamoDB Conditional Write in Python (boto3)
boto3 is the one SDK where a conditional write has a named exception class to catch, and it is also the one where the returned item hides somewhere you would not guess. The expression itself works the same everywhere; DynamoDB condition expressions covers the functions and the optimistic-locking pattern.
Code
import boto3
client = boto3.client("dynamodb")
# Update the item only if nobody changed it since we read version 7.
try:
client.update_item(
TableName="Music",
Key={"Artist": {"S": "Arturo Sandoval"}, "SongTitle": {"S": "Cubano Chant"}},
UpdateExpression="SET #upd0 = :updValue0, #version = :newVersion",
ConditionExpression="attribute_exists(#cond0) AND #version = :expectedVersion",
ExpressionAttributeNames={"#upd0": "Genre", "#version": "Version", "#cond0": "Artist"},
ExpressionAttributeValues={
":updValue0": {"S": "Latin Jazz"},
":expectedVersion": {"N": "7"},
":newVersion": {"N": "8"},
},
ReturnValuesOnConditionCheckFailure="ALL_OLD",
)
print("Updated to version 8")
except client.exceptions.ConditionalCheckFailedException as e:
# With ReturnValuesOnConditionCheckFailure="ALL_OLD", the current item
# rides back on the exception — no extra read to see what beat you.
print("Lost the race — item is now:", e.response.get("Item"))Explanation
ConditionalCheckFailedExceptionis a modelled class, soexcept client.exceptions.…works. Most DynamoDB errors are not:ValidationExceptionhas no class at all and has to be matched one.response["Error"]["Code"]. The modelled class still subclassesClientError, so a broadexcept ClientErrorupstream will swallow it if you order your handlers carelessly.- The returned item is a top-level key of
e.response, not ofe.response["Error"]. That is why the fence readse.response.get("Item"). It is easy to go looking under["Error"]alongsideCodeandMessage, find nothing, and conclude the parameter did not work. - The item comes back in DynamoDB JSON even though you may be used to native values, because this is the low-level client.
boto3.dynamodb.types.TypeDeserializerconverts it if you want plain Python. - The resource API expresses the same guard as objects,
ConditionExpression=Attr("Version").eq(7) & Attr("Artist").exists(), with native values and no placeholder maps. It raises the identical exception, so the handling below is unchanged. - A failed check still bills a write. The Developer Guide is explicit that a false condition consumes write capacity, sized on the larger of the old and new item, so an unbounded retry on a contended key costs real money while making no progress.
Where boto3 puts the returned item
Run the fence against a stored Version of 9 and print the exception's response keys. DynamoDB Local 3.3.0, boto3 1.43.58:
sorted(e.response.keys()) -> ['Error', 'Item', 'ResponseMetadata']
e.response["Item"] -> {'Artist': {'S': 'Arturo Sandoval'},
'Year': {'N': '1994'},
'Version': {'N': '9'},
'SongTitle': {'S': 'Cubano Chant'},
'AlbumTitle': {'S': 'Danzon'}}Drop ReturnValuesOnConditionCheckFailure and the same failure gives ['Error', 'ResponseMetadata']. The Item key is absent, and e.response.get("Item") returns None rather than raising. That is the version of this bug that survives code review and starts logging None in production.
Why every name in the expression is aliased
The fence writes #version and #cond0 instead of Version and Artist, which looks like overkill for two ordinary words. It is, for these two. Version is not a DynamoDB reserved word, and used bare it passes name validation.
Year is reserved, and the same table has one. Guard on it directly and you get:
ValidationException: Invalid ConditionExpression: Attribute name is a reserved keyword;
reserved keyword: Year573 words are on that list, including Name, Status, Size, Count, Data, Owner, Timestamp and Items. Aliasing everything is how generated code avoids ever having to know which is which. Paste your attribute names into the reserved words checker and it returns the ExpressionAttributeNames map for the ones that need it.
To write these guards against your own tables with the aliasing handled for you, download DynoTable.
Related examples
- DynamoDB conditional write in Node.js — the same optimistic lock with AWS SDK v3.
- DynamoDB conditional write with the AWS CLI — the same optimistic lock from the shell.
- DynamoDB PutItem in Python — the create-only
attribute_not_existsput. - DynamoDB condition expressions — every function, with patterns.
- Enforcing uniqueness on multiple attributes — conditions + transactions combined.
- DynamoDB ConditionalCheckFailedException — when the failed check is expected, and how to handle it cheaply.
References
- UpdateItem — Amazon DynamoDB API Reference
- DynamoDB.Client.update_item — Boto3 documentation
- Condition expressions — Amazon DynamoDB Developer Guide
- DynamoDB read and write operations (capacity unit consumption) — Amazon DynamoDB Developer Guide
- Reserved words in DynamoDB — Amazon DynamoDB Developer Guide
Last verified 2026-07-28 against the official AWS documentation linked above.