DynamoDB TransactWriteItems in Python (boto3)
Transactions are one of the places where boto3's two APIs diverge hardest: transact_write_items exists only on the low-level client, so the native-Python-types convenience you get from Table is off the table here. And when the transaction fails, what you need is in a corner of the exception most boto3 code never looks at. (What a transaction buys you is the same in every SDK.)
Code
import boto3
client = boto3.client("dynamodb")
# Move one award between two songs — atomically. If the first song has no
# award to give, NEITHER update happens.
try:
client.transact_write_items(
TransactItems=[
{
"Update": {
"TableName": "Music",
"Key": {"Artist": {"S": "Arturo Sandoval"}, "SongTitle": {"S": "Cubano Chant"}},
"UpdateExpression": "SET #upd0 = #upd0 - :one",
"ConditionExpression": "#upd0 >= :one",
"ExpressionAttributeNames": {"#upd0": "Awards"},
"ExpressionAttributeValues": {":one": {"N": "1"}},
}
},
{
"Update": {
"TableName": "Music",
"Key": {"Artist": {"S": "Arturo Sandoval"}, "SongTitle": {"S": "A Mis Abuelos"}},
"UpdateExpression": "SET #upd0 = if_not_exists(#upd0, :zero) + :one",
"ExpressionAttributeNames": {"#upd0": "Awards"},
"ExpressionAttributeValues": {":one": {"N": "1"}, ":zero": {"N": "0"}},
}
},
]
)
print("Transaction committed")
except client.exceptions.TransactionCanceledException as e:
# One reason per action, in TransactItems order. Code "None" means that
# action was fine — some OTHER action sank the transaction.
codes = [reason["Code"] for reason in e.response["CancellationReasons"]]
print(f"Transaction canceled: {codes}") # e.g. ['ConditionalCheckFailed', 'None']Explanation
TransactItems— a list ofPut,Update,DeleteandConditionCheckdicts, every value in DynamoDB JSON, no exceptions. This is the one boto3 call where the typed form is not optional, and it is why the section at the end of this page exists. The caps are on the CLI page.CancellationReasonsis not insideError. botocore lifts modeled error fields to the top of the response dict, so the caught exception carriese.responsewith the keysCancellationReasons,Error,MessageandResponseMetadataside by side. Looking for it undere.response["Error"]finds nothing, ande.response["Error"]holds only the summary code and message.- No
"Message"on theNoneentries — a successful action's reason is the single-key dict{"Code": "None"}, so the natural[r["Message"] for r in reasons]raisesKeyError: 'Message'on exactly the actions that worked. User.get("Message"). - A generated exception class — botocore builds
client.exceptions.TransactionCanceledExceptionfrom the service model at runtime, which is why it hangs off the client instance and why you cannotfrom botocore.exceptions import ...it. In a helper that does not have the client in scope, catchbotocore.exceptions.ClientErrorand branch one.response["Error"]["Code"]; the generated class is a subclass of it. - Structural mistakes do not arrive as cancellations, so the
exceptclause in the snippet never sees them. Two actions aimed at the same item raise a bareClientErrorwhose code isValidationExceptionand whosee.responsehas noCancellationReasonskey, since the transaction was rejected before any action ran. CatchClientErrorat the outer edge if you want those logged with the same context. ReturnValuesOnConditionCheckFailure: "ALL_OLD"on an action puts the losing item under anItemkey in that action's reason, in DynamoDB JSON, saving you the follow-upget_itemafter you have already lost the race.- boto3 fills
ClientRequestTokenfor you. Captured on the wire, two identicaltransact_write_itemscalls left with two different UUIDs, so the token covers a single call and not your own catch-and-retry loop. Pass a stable one yourself if the retry can outlive the process. - Retry on
TransactionConflict, never onConditionalCheckFailed— the first says someone else held the item for a moment; the second says your precondition is false and will still be false next time. Those are the only two codes most handlers need to separate, and the full set is decoded on the TransactionCanceledException page. - Cost — a transactional write bills about twice what the same write costs outside one, measured on the CLI page. If you only need atomicity on a single item, a conditional write buys it at half the price.
There is no resource-API version of this
boto3.resource("dynamodb").Table(...) has no transact_write_items attribute; only resource.meta.client does. So a codebase that has settled on Table and native Python types has to drop back to typed DynamoDB JSON for its transactions, or serialize by hand with boto3.dynamodb.types.TypeSerializer:
from boto3.dynamodb.types import TypeSerializer
serialize = TypeSerializer().serialize
values = {k: serialize(v) for k, v in {":one": 1, ":zero": 0}.items()}TypeSerializer applies the same rules as the resource API, which means it rejects float and expects decimal.Decimal for anything fractional. The DynamoDB JSON converter does the same conversion in the browser when you just need to paste a literal into a script. To edit the items a transaction touches without writing either form by hand, download DynoTable.
Related examples
- DynamoDB TransactWriteItems in Node.js — the same transaction with AWS SDK v3.
- DynamoDB TransactWriteItems with the AWS CLI — the same transaction from the shell.
- DynamoDB conditional write in Python — single-item atomicity without the 2× cost.
- DynamoDB transactions — isolation, idempotency, and when transactions are worth it.
- DynamoDB TransactionCanceledException — every cancellation-reason code, decoded.
- "Too many actions in a TransactWriteItems call" — the 100-action and 4 MB transaction limits.
- "Transaction request cannot include multiple operations on one item" — one action per item, per transaction.
References
- TransactWriteItems — Amazon DynamoDB API Reference
- DynamoDB.Client.transact_write_items — Boto3 documentation
- Amazon DynamoDB transactions: how it works — Amazon DynamoDB Developer Guide
- DynamoDB read and write operations (capacity unit consumption) — Amazon DynamoDB Developer Guide
Last verified 2026-07-28 against the official AWS documentation linked above.