πŸš€ Quick Start GuideΒΆ

Transform Your boto3 Experience with Type-Safe DataclassesΒΆ

Welcome to boto3-dataclass! This library transforms boring boto3 dictionaries into beautiful, type-safe dataclasses with full autocomplete support. Say goodbye to response['Key']['SubKey'] and hello to response.Key.SubKey with full IDE support!

πŸ’‘ What You’ll GetΒΆ

  • βœ… Full autocomplete in your IDE

  • βœ… Type safety with mypy

  • βœ… Easy dot notation instead of dictionary access

  • βœ… Zero performance overhead with lazy loading

  • βœ… 100% compatible with existing boto3 code

πŸ“¦ InstallationΒΆ

Install the service you need (e.g., for IAM, S3):

pip install boto3-dataclass[iam,s3]

Or match with your boto3 version

pip install "boto3-dataclass[iam,s3]>=1.40.0,<1.41.0"

Or install everything at once:

pip install boto3-dataclass[all]  # Installs all AWS services

⚑ 30-Second Demo¢

Let’s see the magic in action with AWS IAM:

Before (Standard boto3)ΒΆ

import boto3

# ❌ No type hints, no autocomplete
client = boto3.client("iam")
response = client.get_role(RoleName="my-role")

# ❌ Dictionary access - error-prone and no autocomplete
role_name = response["Role"]["RoleName"]
request_id = response["ResponseMetadata"]["RequestId"]

After (With boto3-dataclass)ΒΆ

from boto3_dataclass_iam import iam_caster
import boto3

# βœ… Get your response as usual
client = boto3.client("iam")
response = client.get_role(RoleName="my-role")

# βœ… Transform it to a type-safe dataclass
typed_response = iam_caster.get_role(response)

# βœ… Enjoy autocomplete and type safety!
role_name = typed_response.Role.RoleName
request_id = typed_response.ResponseMetadata.RequestId

πŸ› οΈ Complete Working ExampleΒΆ

Here’s a complete example showing common patterns:

[15]:
aws_profile = "bmt_app_dev_us_east_1"
aws_region = "us-east-1"
[17]:
# Step 1: Import the caster for your service
from boto3_dataclass_iam import iam_caster
import boto3

# Step 2: Create your boto3 client as usual
boto_ses = boto3.Session(profile_name=aws_profile, region_name=aws_region)
client = boto_ses.client("iam")

# Step 3: Make your API calls as normal
response = client.get_role(RoleName="lambda-power-user-role")

# Step 4: Convert to dataclass for better experience
role_info = iam_caster.get_role(response)

# Step 5: Enjoy type-safe access with autocomplete!
print(f"Role Name: {role_info.Role.RoleName}")
print(f"Created: {role_info.Role.CreateDate}")
print(f"Request ID: {role_info.ResponseMetadata.RequestId}")

# =============================================================================
# AVAILABLE ATTRIBUTES WITH AUTOCOMPLETION
# =============================================================================
# Top-level response attributes:
# response.ResponseMetadata    - AWS response metadata
# response.Role               - The IAM role details
# response.boto3_raw_data     - Original raw response (for debugging)

# Role-specific attributes (all with IDE autocompletion):
# response.Role.Path                      - Role path
# response.Role.RoleName                  - Name of the role
# response.Role.RoleId                    - Unique role identifier
# response.Role.Arn                       - Amazon Resource Name
# response.Role.CreateDate                - When the role was created
# response.Role.AssumeRolePolicyDocument  - Trust policy document
# response.Role.Description               - Role description (if set)
# response.Role.MaxSessionDuration        - Maximum session duration
# response.Role.PermissionsBoundary       - Permissions boundary (if set)
# response.Role.Tags                      - Role tags
# response.Role.RoleLastUsed              - Last usage information

# =============================================================================
# BENEFITS
# =============================================================================
# βœ“ IDE autocompletion and IntelliSense
# βœ“ Type safety and error prevention
# βœ“ Better code readability
# βœ“ Reduced runtime errors from typos
# βœ“ Access to original raw data when needed
Role Name: lambda-power-user-role
Created: 2023-05-11 04:07:16+00:00
Request ID: 95dd9d22-e4ad-471b-8682-02bf8c465fff

Pro Tip: Using with boto-session-managerΒΆ

For even better type hints on your boto3 clients, combine with boto-session-manager:

[19]:
from boto_session_manager import BotoSesManager

# Get fully typed boto3 clients
bsm = BotoSesManager(profile_name=aws_profile, region_name=aws_region)
iam_client = bsm.iam_client  # βœ… This has full type hints!

# Make calls and convert responses
response = iam_client.get_role(RoleName="lambda-power-user-role")
role_info = iam_caster.get_role(response)  # βœ… Full autocomplete here too!

# Enjoy type-safe access with autocomplete!
print(f"Role Name: {role_info.Role.RoleName}")
print(f"Created: {role_info.Role.CreateDate}")
print(f"Request ID: {role_info.ResponseMetadata.RequestId}")
Role Name: lambda-power-user-role
Created: 2023-05-11 04:07:16+00:00
Request ID: ab7910de-e09f-46bb-bb32-4252ce4f5afb

🎯 How It Works - The Complete Picture¢

Understanding the boto3-dataclass ecosystem is key to using it effectively. Here’s how all the pieces fit together:

🧩 The Three-Layer Architecture¢

Layer 1: Standard boto3

  • Each AWS Service has a service name (the string you use to create clients)

  • For example, IAM uses: iam_client = boto3.client('iam')

  • See boto3 IAM documentation

Layer 2: mypy-boto3 Type Stubs

  • Each AWS Service has a Python stub-only package like mypy_boto3_{service_name}

  • For example: mypy-boto3-iam provides TypedDict-style type hints

  • Limitation: These stubs give you type checking but NO autocomplete for attribute access

Layer 3: boto3-dataclass (This Library!)

  • Each AWS Service gets a dataclass package: boto3_dataclass_{service_name}

  • For example: boto3-dataclass-iam

  • Benefit: Converts response dictionaries to user-friendly dataclasses with full autocomplete and type hints

πŸ”„ The Conversion PatternΒΆ

The magic happens with a simple, predictable pattern:

When you have:

  • A service client: {service_name}_client from boto3

  • A response dictionary from: client.method_name(...)

You get:

  • A corresponding caster: {service_name}_caster from boto3_dataclass_{service_name}

  • A conversion method: caster.method_name(response) that transforms dictionaries to dataclasses

πŸ“‹ Quick ReferenceΒΆ

# The Universal Pattern for ANY AWS Service:

# 1. Create boto3 client
client = boto3.client("{service_name}")

# 2. Import corresponding caster
from boto3_dataclass_{service_name} import {service_name}_caster

# 3. Make API calls as normal
response = client.method_name(...)

# 4. Convert to dataclass
typed_response = {service_name}_caster.method_name(response)

# 5. Enjoy autocomplete and type safety!

🎯 Why This Design Works¢

  • Predictable: Same pattern across all 200+ AWS services

  • Optional: Use dataclass conversion only when you need it

  • Compatible: Works with existing boto3 code without changes

  • Performance: Zero overhead until you convert responses

πŸ—οΈ Available ServicesΒΆ

We support all AWS services that have mypy-boto3 stubs!

πŸ” Behind the Scenes (Optional Reading)ΒΆ

Curious how the magic works? Here’s the technical overview:

The ArchitectureΒΆ

Our system automatically generates dataclasses from mypy-boto3 type stubs:

  1. Parser: Reads mypy_boto3_{service_name}/type_defs.pyi files

  2. Generator: Creates dataclass definitions in boto3_dataclass_{service_name}/type_defs.py

  3. Caster: Provides conversion methods in boto3_dataclass_{service_name}/caster.py

Smart Design ChoicesΒΆ

  • Lazy Loading: Attributes are loaded only when accessed (zero overhead)

  • Raw Data Access: Original dict always available via .boto3_raw_data

  • Type Safety: Full mypy compatibility with proper type annotations

  • Zero Magic: No monkey patching or runtime modifications

Example Generated CodeΒΆ

1. Dataclass Definitions (``type_defs.py``) Here’s what gets generated for an IAM Role:

[20]:
# content of boto3_dataclass_iam/type_defs.py
# -*- coding: utf-8 -*-

import typing as T
import dataclasses
from functools import cached_property

if T.TYPE_CHECKING:  # pragma: no cover
    from mypy_boto3_iam import type_defs


def field(name: str):
    def getter(self):
        return self.boto3_raw_data[name]

    return cached_property(getter)


@dataclasses.dataclass(frozen=True)
class ResponseMetadata:
    boto3_raw_data: "type_defs.ResponseMetadataTypeDef" = dataclasses.field()

    RequestId = field("RequestId")
    HTTPStatusCode = field("HTTPStatusCode")
    HTTPHeaders = field("HTTPHeaders")
    RetryAttempts = field("RetryAttempts")
    HostId = field("HostId")

    @classmethod
    def make_one(cls, boto3_raw_data: T.Optional["type_defs.ResponseMetadataTypeDef"]):
        if boto3_raw_data is None:
            return None
        return cls(boto3_raw_data=boto3_raw_data)

    @classmethod
    def make_many(
        cls,
        boto3_raw_data_list: T.Optional[
            T.Iterable["type_defs.ResponseMetadataTypeDef"]
        ],
    ):
        if boto3_raw_data_list is None:
            return None
        return [
            cls(boto3_raw_data=boto3_raw_data) for boto3_raw_data in boto3_raw_data_list
        ]

# More dataclass model ...

@dataclasses.dataclass(frozen=True)
class Role:
    boto3_raw_data: "type_defs.RoleTypeDef" = dataclasses.field()

    Path = field("Path")
    RoleName = field("RoleName")
    RoleId = field("RoleId")
    Arn = field("Arn")
    CreateDate = field("CreateDate")
    AssumeRolePolicyDocument = field("AssumeRolePolicyDocument")
    Description = field("Description")
    MaxSessionDuration = field("MaxSessionDuration")

    @cached_property
    def PermissionsBoundary(self):  # pragma: no cover
        return AttachedPermissionsBoundary.make_one(
            self.boto3_raw_data["PermissionsBoundary"]
        )

    @cached_property
    def Tags(self):  # pragma: no cover
        return Tag.make_many(self.boto3_raw_data["Tags"])

    @cached_property
    def RoleLastUsed(self):  # pragma: no cover
        return RoleLastUsed.make_one(self.boto3_raw_data["RoleLastUsed"])

    @classmethod
    def make_one(cls, boto3_raw_data: T.Optional["type_defs.RoleTypeDef"]):
        if boto3_raw_data is None:
            return None
        return cls(boto3_raw_data=boto3_raw_data)

    @classmethod
    def make_many(
        cls, boto3_raw_data_list: T.Optional[T.Iterable["type_defs.RoleTypeDef"]]
    ):
        if boto3_raw_data_list is None:
            return None
        return [
            cls(boto3_raw_data=boto3_raw_data) for boto3_raw_data in boto3_raw_data_list
        ]


@dataclasses.dataclass(frozen=True)
class GetRoleResponse:
    boto3_raw_data: "type_defs.GetRoleResponseTypeDef" = dataclasses.field()

    @cached_property
    def Role(self):  # pragma: no cover
        return Role.make_one(self.boto3_raw_data["Role"])

    @cached_property
    def ResponseMetadata(self):  # pragma: no cover
        return ResponseMetadata.make_one(self.boto3_raw_data["ResponseMetadata"])

    @classmethod
    def make_one(cls, boto3_raw_data: T.Optional["type_defs.GetRoleResponseTypeDef"]):
        if boto3_raw_data is None:
            return None
        return cls(boto3_raw_data=boto3_raw_data)

    @classmethod
    def make_many(
        cls,
        boto3_raw_data_list: T.Optional[T.Iterable["type_defs.GetRoleResponseTypeDef"]],
    ):
        if boto3_raw_data_list is None:
            return None
        return [
            cls(boto3_raw_data=boto3_raw_data) for boto3_raw_data in boto3_raw_data_list
        ]

2. Caster Module (``caster.py``)

Then we have a boto3_dataclass_{service_name}/caster.py module for each service like this:

[14]:
# -*- coding: utf-8 -*-

import typing as T

from . import type_defs as dc_td

if T.TYPE_CHECKING:  # pragma: no cover
    from mypy_boto3_iam import type_defs as bs_td


class IAMCaster:
    def get_role(
        self,
        res: "bs_td.GetRoleResponseTypeDef",
    ) -> "dc_td.GetRoleResponse":
        return dc_td.GetRoleResponse.make_one(res)

    # More client method ...

iam_caster = IAMCaster()

The Caster Magic:

  • The caster class mimics the exact same method names as the boto3 client

  • When you call client.get_role(...), you use caster.get_role(response)

  • No memorization needed - if the client has the method, so does the caster!

  • Each method converts the raw response dictionary to a type-safe dataclass

πŸŽ‰ Next StepsΒΆ

Ready to upgrade your boto3 experience?

  1. Install the service package you need: pip install boto3-dataclass[{service}]

  2. Import the caster: from boto3_dataclass_{service} import {service}_caster

  3. Convert your responses: typed_response = caster.method_name(response)

  4. Enjoy the autocomplete and type safety!

Questions or issues? Check out our GitHub repository or documentation.

Happy coding! 🐍✨

[ ]: