DynamoDB UpdateItem in Python (boto3)

boto3 gives you two clients for this call and they disagree about what a number is. The low-level client below sends and receives DynamoDB JSON, where every number is a quoted string. resource("dynamodb").Table(...) takes native Python objects, refuses float outright, and hands numbers back as decimal.Decimal. Picking one is the real decision on this page.

Code

import boto3

client = boto3.client("dynamodb")

response = client.update_item(
    TableName="Music",
    Key={"Artist": {"S": "Arturo Sandoval"}, "SongTitle": {"S": "Cubano Chant"}},
    UpdateExpression="SET #upd0 = :updValue0, #upd1 = :updValue1 ADD #upd2 :updValue2",
    ExpressionAttributeNames={"#upd0": "Genre", "#upd1": "Year", "#upd2": "Awards"},
    ExpressionAttributeValues={":updValue0": {"S": "Latin Jazz"}, ":updValue1": {"N": "1994"}, ":updValue2": {"N": "1"}},
    ReturnValues="ALL_NEW",
)

print(response["Attributes"])  # the item after the update

Explanation

  • Clause grammar is not boto3's business. The UpdateExpression is an opaque string it forwards; only DynamoDB parses it, so mistakes cost a round trip. ADD here is the atomic increment that removes the read-modify-write race, attribute_exists(Artist) in a ConditionExpression turns the upsert into an update-only, and the rest is in update expressions.
  • The response has exactly two top-level keys: Attributes and ResponseMetadata. There is no status field to check and no row count. If the call returned, it worked; ResponseMetadata carries the RequestId and HTTPStatusCode you want in a log line.
  • ReturnValues="UPDATED_NEW" is the frugal option. It returns only the attributes the expression touched, which on a large item is the difference between reading one counter and shipping the whole record back.
  • Errors arrive as botocore.exceptions.ClientError, and you branch on e.response["Error"]["Code"]. A missing alias produces ValidationException with the message Invalid UpdateExpression: Attribute name is a reserved keyword; reserved keyword: Year. The typed subclasses do exist, but only as attributes botocore generates on the client instance (client.exceptions.ConditionalCheckFailedException), never as importable symbols, so a helper function without the client in scope has to use the code string.

Decimal or DynamoDB JSON, pick one

The resource API rejects float before the request is built, with a message that tells you exactly what it wants:

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

That is boto3's own type checking, not DynamoDB's. Store Decimal("4.5") through the resource API and read the same attribute back through both clients, and you get:

resource Rating: Decimal('4.5') Awards: Decimal('2')
client Rating: {'N': '4.5'} Awards: {'N': '2'}

Neither is wrong; they are different contracts. Decimal keeps the precision DynamoDB actually stores and forces you to think about arithmetic, at the cost of Decimal("1") * 2 showing up in code that expected an int. The low-level client hands you strings and leaves the parsing to you, which is what the snippet above does.

The rule that follows: do not mix them in one code path. An item written through Table.put_item and read through client.get_item comes back in a different shape, and the bug surfaces in whichever branch you tested less.

A note on TTL attributes

The most common numeric SET in a Python codebase is a TTL: SET expires_at = :t with a Unix epoch. DynamoDB reads that attribute as seconds. Write int(time.time() * 1000) instead and the value is 1785269450912, which as seconds lands in the year 58542, so the item is never deleted and nothing complains. The DynamoDB TTL converter reads an epoch back in both units and tells you which one you wrote. To read the stored value back from a real table afterwards, 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.