Python(boto3)中的 DynamoDB PutItem
put_item 写入一整个项目,并替换掉任何具有相同主键的已有项目(基于项目的操作讲了它和 update_item 的区别)。用低层 client 时,每个属性都以 DynamoDB JSON 传入,而 boto3 会在发送任何东西之前先在本地检查这个形状。
代码
import boto3
from botocore.exceptions import ClientError
client = boto3.client("dynamodb")
try:
client.put_item(
TableName="Music",
Item={"Artist": {"S": "Arturo Sandoval"}, "SongTitle": {"S": "Cubano Chant"}, "AlbumTitle": {"S": "Danzon"}, "Year": {"N": "1994"}, "Awards": {"N": "0"}},
ConditionExpression="attribute_not_exists(#cond0) AND attribute_not_exists(#cond1)",
ExpressionAttributeNames={"#cond0": "Artist", "#cond1": "SongTitle"},
)
print("Song written")
except ClientError as err:
if err.response["Error"]["Code"] == "ConditionalCheckFailedException":
print("A song with that key already exists — not overwritten")
else:
raise说明
{"N": 1994} 根本到不了 AWS,而 except ClientError 也捕获不到它。botocore 会先拿自己的服务模型校验请求,而在 N 类型要字符串的地方塞一个 Python int,就会在那里失败:
ParamValidationError: Parameter validation failed:
Invalid type for parameter Item.Year.N, value: 1994, type: <class 'int'>, valid types: <class 'str'>ParamValidationError 派生自 BotoCoreError,而不是 ClientError,所以上面代码片段里的处理分支会放它过去。这通常正是你想要的,因为它是 bug 而不是业务结果,但也意味着在写入外面包一层 try/except ClientError 并不能兜住一切。好处是错误会指名道姓地给出确切路径 Item.Year.N,调试起来胜过一个服务端的 ValidationException。更多内容见 "Parameter validation failed"。
一次失败条件的完整面貌。把同一个条件写入捕获两次,并把异常上的所有东西都打印出来,得到:
type(e).__name__ ConditionalCheckFailedException
e.response["Error"]["Code"] ConditionalCheckFailedException
e.response["Error"]["Message"] The conditional request failed
e.response["ResponseMetadata"]["HTTPStatusCode"] 400
str(e) An error occurred (ConditionalCheckFailedException) when calling the PutItem operation: The conditional request failed由此有两点。在 botocore 1.43.58 上,这个对象是一个建模出来的子类,所以 except client.exceptions.ConditionalCheckFailedException 和代码片段里那个 err.response["Error"]["Code"] 判断可以并行使用;挑一种,然后保持一致。另外 str(e) 是一个格式化过的句子,不是服务的消息,所以永远不要拿它去比对字面量。
条件失败照样要计一次写入的费。AWS 原话:"if the expression evaluates to false, DynamoDB still consumes write capacity units from the table"(2026-07-28 取回)。一个「只创建」的重试循环,每一次被拒绝的尝试都要付钱。给个量级:一个约 15 KB 的项目成功写入后,在 ReturnConsumedCapacity="TOTAL" 下报告了 "CapacityUnits": 15;写入按每 1 KB 向上取整,不是读取用的那 4 KB。
资源 API 是另一份契约,而 float 就是你发现这一点的地方。boto3.resource("dynamodb").Table("Music").put_item(Item={...}) 接受普通 Python 并替你做 marshalling,但它彻底拒绝二进制浮点数:
TypeError: Float types are not supported. Use Decimal types instead.把值包进 decimal.Decimal("4.5"),而且要从字符串而不是 float 出发,否则在 Decimal 看到它之前误差就已经烙进去了。用同一套 API 读回来时每个数字都是 Decimal,这是对你代码的一次真实改动,不是格式细节。见 "Float types are not supported"。
混用这两套 API 是谁都不会提醒你的陷阱。低层 client 会欣然接受 {"N": "1.5"},而这个值资源 API 本来会当成 float 拒绝掉。一个用其中一套写、用另一套读的代码库,会从从来没经过 Decimal 的数据里拿回 Decimal。
那些 #cond0 别名不是装饰。它们通过 ExpressionAttributeNames 解析成 Artist/SongTitle。内联写属性名一直都好用,直到其中一个撞上保留字,然后表达式就在一个你根本没改过的名字上失败了。
用可视化的方式来做
条件表达式是手写最先出错的地方,因为写错的那一个是以「写入被拒绝」而不是语法错误的形式失败的。免费的 DynamoDB Expression Builder 会连同名称和值映射一起拼出 ConditionExpression,并给出可直接粘贴的 boto3 调用。
要针对你自己的表写入和编辑项目——每个属性一个表单、类型选择器、把结果复制成 boto3 代码——请下载 DynoTable。
相关指南
- DynamoDB 条件表达式——
attribute_not_exists、乐观锁等等。 - DynamoDB 数据类型——每种属性类型在 DynamoDB JSON 里怎么写。
- DynamoDB ConditionalCheckFailedException——项目已存在时,那个「只创建」条件会抛出什么。
- DynamoDB ValidationException——项目或表达式写坏时的兜底错误。
参考资料
- PutItem — Amazon DynamoDB API Reference
- put_item — Boto3 DynamoDB.Client Reference
- Error handling — Boto3 Developer Guide
- Capacity unit consumption — Amazon DynamoDB Developer Guide
- Condition expressions — Amazon DynamoDB Developer Guide
2026-07-28 以 boto3 1.43.58 / botocore 1.43.58 针对 DynamoDB Local(amazon/dynamodb-local,端口 9000)复现。上方的异常文本、响应字段和容量读数都是捕获到的输出,原样照录。