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

  • ConditionalCheckFailedException is a modeled class, so except client.exceptions.… works. Most DynamoDB errors are not: ValidationException has no class at all and has to be matched on e.response["Error"]["Code"]. The modeled class still subclasses ClientError, so a broad except ClientError upstream will swallow it if you order your handlers carelessly.
  • The returned item is a top-level key of e.response, not of e.response["Error"]. That is why the fence reads e.response.get("Item"). It is easy to go looking under ["Error"] alongside Code and Message, 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.TypeDeserializer converts 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: Year

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

References

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

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.