DynamoDB TransactWriteItems en Python (boto3)

Las transacciones son uno de los sitios donde las dos API de boto3 más divergen: transact_write_items solo existe en el cliente de bajo nivel, así que la comodidad de los tipos nativos de Python que te da Table aquí no está disponible. Y cuando la transacción falla, lo que necesitas está en un rincón de la excepción que casi ningún código de boto3 mira. (Lo que te aporta una transacción es igual en todos los SDK.)

Código

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']

Explicación

  • TransactItems — una lista de diccionarios Put, Update, Delete y ConditionCheck, cada valor en JSON de DynamoDB, sin excepciones. Esta es la única llamada de boto3 donde la forma tipada no es opcional, y por eso existe la sección del final de esta página. Los topes están en la página de la CLI.
  • CancellationReasons no está dentro de Error. botocore eleva los campos de error modelados al nivel superior del diccionario de respuesta, así que la excepción capturada lleva un e.response con las claves CancellationReasons, Error, Message y ResponseMetadata una al lado de otra. Buscarlo bajo e.response["Error"] no encuentra nada, y e.response["Error"] solo contiene el código y el mensaje de resumen.
  • Sin "Message" en las entradas None — el motivo de una acción correcta es el diccionario de una sola clave {"Code": "None"}, así que el natural [r["Message"] for r in reasons] lanza KeyError: 'Message' justo en las acciones que funcionaron. Usa r.get("Message").
  • Una clase de excepción generada — botocore construye client.exceptions.TransactionCanceledException a partir del modelo de servicio en tiempo de ejecución, y por eso cuelga de la instancia del cliente y por eso no puedes hacerle from botocore.exceptions import .... En una función auxiliar que no tenga el cliente a mano, captura botocore.exceptions.ClientError y ramifica según e.response["Error"]["Code"]; la clase generada es una subclase suya.
  • Los errores estructurales no llegan como cancelaciones, así que la cláusula except del fragmento nunca los ve. Dos acciones apuntando al mismo elemento lanzan un ClientError pelado cuyo código es ValidationException y cuyo e.response no tiene clave CancellationReasons, ya que la transacción se rechazó antes de ejecutar ninguna acción. Captura ClientError en el borde exterior si quieres registrarlos con el mismo contexto.
  • ReturnValuesOnConditionCheckFailure: "ALL_OLD" en una acción pone el elemento perdedor bajo una clave Item en el motivo de esa acción, en JSON de DynamoDB, ahorrándote el get_item de seguimiento cuando ya has perdido la carrera.
  • boto3 rellena ClientRequestToken por ti. Capturadas en la red, dos llamadas idénticas a transact_write_items salieron con dos UUID distintos, así que el token cubre una única llamada y no tu propio bucle de capturar y reintentar. Pasa uno estable tú mismo si el reintento puede sobrevivir al proceso.
  • Reintenta con TransactionConflict, nunca con ConditionalCheckFailed — el primero dice que otro retuvo el elemento un momento; el segundo dice que tu precondición es falsa y seguirá siéndolo la próxima vez. Esos son los dos únicos códigos que la mayoría de los handlers necesitan separar, y el conjunto completo está descifrado en la página de TransactionCanceledException.
  • Coste — una escritura transaccional factura aproximadamente el doble que la misma escritura fuera de una, medido en la página de la CLI. Si solo necesitas atomicidad sobre un único elemento, una escritura condicional te la da a mitad de precio.

No hay versión de esto en la API de recursos

boto3.resource("dynamodb").Table(...) no tiene el atributo transact_write_items; solo lo tiene resource.meta.client. Así que una base de código que se haya asentado en Table y tipos nativos de Python tiene que volver al JSON de DynamoDB tipado para sus transacciones, o serializar a mano con 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 aplica las mismas reglas que la API de recursos, lo que significa que rechaza float y espera decimal.Decimal para cualquier cosa fraccionaria. El conversor de JSON de DynamoDB hace la misma conversión en el navegador cuando solo necesitas pegar un literal en un script. Para editar los elementos que toca una transacción sin escribir ninguna de las dos formas a mano, descarga DynoTable.

Ejemplos relacionados

Referencias

Verificado por última vez el 2026-07-28 contra la documentación oficial de AWS enlazada arriba.

Trabaja con DynamoDB sin la Consola

Un cliente de escritorio rápido para DynamoDB que ejecuta el SQL real que DynamoDB no puede — JOINs, GROUP BY, agregaciones — con edición visual y un agente de IA con tus propias claves de Bedrock.

Prueba gratuita de 30 días, sin tarjeta — después, el plan Free sin límite de tiempo.