boto3
DynamoDB
Python
dictionary conversion
AWS

How to convert a boto3 Dynamo DB item to a regular dictionary in Python?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

When you use the low-level boto3 DynamoDB client, returned items are wrapped in DynamoDB AttributeValue objects rather than ordinary Python values. The clean solution is to deserialize them as soon as they cross the repository boundary so the rest of the application can work with normal dictionaries.

Know Which boto3 API You Are Using

This problem applies mainly to the low-level client API.

  • 'boto3.client("dynamodb") returns AttributeValue maps'
  • 'boto3.resource("dynamodb") usually gives you normal Python-style values'

A low-level item looks like this:

python
1raw_item = {
2    "pk": {"S": "USER#42"},
3    "age": {"N": "31"},
4    "active": {"BOOL": True},
5    "profile": {
6        "M": {
7            "country": {"S": "CA"},
8            "score": {"N": "98.5"}
9        }
10    },
11    "tags": {"L": [{"S": "pro"}, {"S": "beta"}]}
12}

That format is correct for the wire protocol, but it is awkward in normal business logic.

Use TypeDeserializer

boto3 already includes the right conversion tool: TypeDeserializer.

python
1from boto3.dynamodb.types import TypeDeserializer
2
3
4def dynamodb_item_to_dict(item: dict) -> dict:
5    deserializer = TypeDeserializer()
6    return {key: deserializer.deserialize(value) for key, value in item.items()}
7
8plain = dynamodb_item_to_dict(raw_item)
9print(plain)
10print(type(plain["age"]))

This correctly handles nested maps and lists, so there is little reason to hand-roll your own recursive parser.

Decode Query and Scan Results the Same Way

Most real code works with multiple items from query or scan, not just one record. Wrap the same logic in a helper for result sets.

python
1from boto3.dynamodb.types import TypeDeserializer
2
3
4def decode_items(items: list[dict]) -> list[dict]:
5    deserializer = TypeDeserializer()
6    return [
7        {key: deserializer.deserialize(value) for key, value in item.items()}
8        for item in items
9    ]
10
11items = [
12    {"pk": {"S": "USER#1"}, "age": {"N": "20"}},
13    {"pk": {"S": "USER#2"}, "age": {"N": "30"}},
14]
15
16print(decode_items(items))

That keeps all DynamoDB deserialization behavior consistent across your data-access layer.

Understand Decimal Values

DynamoDB numbers deserialize to Decimal, not plain int or float. That preserves precision, but it can surprise code that expects JSON-serializable primitives.

python
1import json
2from decimal import Decimal
3
4
5def decimal_to_json(value):
6    if isinstance(value, Decimal):
7        if value % 1 == 0:
8            return int(value)
9        return float(value)
10    raise TypeError(f"Unsupported type: {type(value).__name__}")
11
12payload = dynamodb_item_to_dict(raw_item)
13print(json.dumps(payload, default=decimal_to_json))

If the values represent money or other precision-sensitive quantities, converting to float may be the wrong choice. In those cases, strings or domain-specific serializers are often safer.

Deserialize at the Repository Boundary

A good pattern is to convert the item immediately after reading it from DynamoDB and keep the rest of the application isolated from AttributeValue syntax.

python
1import boto3
2from boto3.dynamodb.types import TypeDeserializer
3
4
5class UserRepository:
6    def __init__(self, table_name: str):
7        self.client = boto3.client("dynamodb")
8        self.table_name = table_name
9        self.deserializer = TypeDeserializer()
10
11    def get_user(self, user_id: str) -> dict | None:
12        response = self.client.get_item(
13            TableName=self.table_name,
14            Key={"pk": {"S": f"USER#{user_id}"}}
15        )
16        item = response.get("Item")
17        if not item:
18            return None
19        return {key: self.deserializer.deserialize(value) for key, value in item.items()}

That keeps services, handlers, and tests free from low-level DynamoDB wire-format details.

Common Pitfalls

The biggest mistake is mixing the low-level client and higher-level resource API without noticing which one already performs deserialization.

Another common issue is reimplementing recursive conversion logic by hand when boto3 already provides the correct tool. Developers also often convert every Decimal to float automatically and then discover precision loss later in analytics or billing code.

Summary

  • Low-level boto3 client items need explicit deserialization.
  • 'TypeDeserializer is the built-in tool for converting DynamoDB AttributeValue maps.'
  • Use a shared helper for query and scan result sets.
  • Treat Decimal conversion as a domain decision, not an afterthought.
  • Deserialize at the repository boundary so the rest of the app can use normal dictionaries.

Course illustration
Course illustration

All Rights Reserved.