Python(boto3)での DynamoDB PutItem

put_item はアイテムを丸ごと書き込み、同じプライマリキーを持つ既存のアイテムを置き換えます(update_item との違いはアイテム単位の操作で扱っています)。低レベルクライアント では、すべての属性が 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'>

ParamValidationErrorClientError ではなく BotoCoreError を継承しているので、上のスニペットのハンドラーはこれを素通りさせます。ビジネス上の結果ではなくバグなので、たいていはそれが望ましい挙動です。ただし、書き込みを try/except ClientError で囲んでも万能ではない、ということでもあります。良い面として、このエラーは Item.Year.N という正確なパスを名指しするので、デバッグではサーバー側の ValidationException に勝ります。詳しくは"Parameter validation failed"にあります。

条件が失敗したときの全体像。同じ条件付き put を 2 回実行して例外の中身をすべて表示すると、こうなりました。

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

ここから 2 つのことが言えます。botocore 1.43.58 では、このオブジェクトは モデル化されたサブクラス なので、スニペットが使っている err.response["Error"]["Code"] のチェックと並んで except client.exceptions.ConditionalCheckFailedException も機能します。どちらかを選んで一貫させましょう。そして str(e) は整形された文であってサービスのメッセージではないので、決してリテラルと比較しないでください。

条件が失敗しても書き込みは課金されます。AWS の記述はこうです。"if the expression evaluates to false, DynamoDB still consumes write capacity units from the table"(2026-07-28 取得)。作成専用のリトライループは、拒否された試行のすべてに支払います。規模感として、約 15 KB のアイテムの put が成功したとき、ReturnConsumedCapacity="TOTAL" の下では "CapacityUnits": 15 と報告されました。書き込みは読み取りの 4 KB ではなく 1 KB 単位で切り上げられます。

リソース API は別の契約であり、それに気づかされるのが float ですboto3.resource("dynamodb").Table("Music").put_item(Item={...}) は素の Python を受け取って代わりにマーシャルしますが、2 進浮動小数点はきっぱり拒否します。

TypeError: Float types are not supported. Use Decimal types instead.

値は decimal.Decimal("4.5") で包みましょう。float からではなく 文字列 からです。そうしないと、Decimal が見る前に誤差が既に焼き付いています。同じ API を通して読み戻すと、数値はすべて Decimal で返ります。これは書式の細部ではなく、コードにとって本物の変化です。"Float types are not supported"を参照してください。

2 つの API を混ぜることは、どちらも警告してくれない罠です。低レベルクライアントは {"N": "1.5"} を平気で受け取りますが、これはリソース API なら float として拒否した値です。片方で書いてもう片方で読むコードベースは、入り口で Decimal を通っていないデータから Decimal を受け取ることになります。

#cond0 の別名は飾りではありません。これらは ExpressionAttributeNames を通して Artist/SongTitle に解決されます。属性名の直書きは、どれかが予約語と衝突するまでは動き、衝突した瞬間、自分では変えていない名前のせいで式が失敗します。

ビジュアルに行う

手書きが最初に破綻するのは条件式です。間違った条件は構文エラーではなく、拒否された書き込みとして失敗するからです。無料の DynamoDB Expression BuilderConditionExpression を名前マップと値マップとともに組み立て、そのまま貼り付けられる boto3 の呼び出しを出力します。

自分のテーブルに対してアイテムを書き込み・編集するには — 属性ごとのフォーム、型ピッカー、結果の boto3 としてのコピー — DynoTable をダウンロードしてください。

関連ガイド

参考資料

2026-07-28 に、boto3 1.43.58 / botocore 1.43.58 で、ポート 9000 の DynamoDB Local(amazon/dynamodb-local)に対して再現しました。上記の例外テキスト、レスポンスのフィールド、キャパシティの数値は、取得した出力をそのまま逐語で写したものです。

Console なしで DynamoDB を扱う

DynamoDB では実行できない本物の SQL(JOINs、GROUP BY、集計)を実行する高速な DynamoDB デスクトップクライアント。ビジュアル編集と、あなた自身の Bedrock キーで動く AI エージェントを備えています。

30日間無料トライアル、クレジットカード不要 — その後は期限のない Free プラン。