diff --git a/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/SharedCacheAcrossHierarchicalKeyringsExample.java b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/SharedCacheAcrossHierarchicalKeyringsExample.java
index dfed4b84d..62fb6d711 100644
--- a/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/SharedCacheAcrossHierarchicalKeyringsExample.java
+++ b/Examples/runtimes/java/DynamoDbEncryption/src/main/java/software/amazon/cryptography/examples/keyring/SharedCacheAcrossHierarchicalKeyringsExample.java
@@ -180,7 +180,7 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem(
     final IKeyring hierarchicalKeyring1 =
       matProv.CreateAwsKmsHierarchicalKeyring(keyringInput1);
 
-    // 4. Configure which attributes are encrypted and/or signed when writing new items.
+    // 5. Configure which attributes are encrypted and/or signed when writing new items.
     //    For each attribute that may exist on the items we plan to write to our DynamoDbTable,
     //    we must explicitly configure how they should be treated during item encryption:
     //      - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
@@ -194,14 +194,14 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem(
       CryptoAction.ENCRYPT_AND_SIGN
     );
 
-    // 5. Get the DDB Client for Hierarchical Keyring 1.
+    // 6. Get the DDB Client for Hierarchical Keyring 1.
     final DynamoDbClient ddbClient1 = GetDdbClient(
       ddbTableName,
       hierarchicalKeyring1,
       attributeActionsOnEncrypt
     );
 
-    // 6. Encrypt Decrypt roundtrip with ddbClient1
+    // 7. Encrypt Decrypt roundtrip with ddbClient1
     PutGetItems(ddbTableName, ddbClient1);
 
     // Through the above encrypt and decrypt roundtrip, the cache will be populated and
@@ -210,7 +210,7 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem(
     // - Same Logical Key Store Name of the Key Store for the Hierarchical Keyring
     // - Same Branch Key ID
 
-    // 7. Configure your KeyStore resource keystore2.
+    // 8. Configure your KeyStore resource keystore2.
     //       This SHOULD be the same configuration that you used
     //       to initially create and populate your physical KeyStore.
     //    Note that keyStoreTableName is the physical Key Store,
@@ -243,7 +243,7 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem(
       )
       .build();
 
-    // 8. Create the Hierarchical Keyring HK2 with Key Store instance K2, the shared Cache
+    // 9. Create the Hierarchical Keyring HK2 with Key Store instance K2, the shared Cache
     //    and the same partitionId and BranchKeyId used in HK1 because we want to share cache entries
     //    (and experience cache HITS).
 
@@ -262,14 +262,14 @@ public static void SharedCacheAcrossHierarchicalKeyringsGetItemPutItem(
     final IKeyring hierarchicalKeyring2 =
       matProv.CreateAwsKmsHierarchicalKeyring(keyringInput2);
 
-    // 9. Get the DDB Client for Hierarchical Keyring 2.
+    // 10. Get the DDB Client for Hierarchical Keyring 2.
     final DynamoDbClient ddbClient2 = GetDdbClient(
       ddbTableName,
       hierarchicalKeyring2,
       attributeActionsOnEncrypt
     );
 
-    // 10. Encrypt Decrypt roundtrip with ddbClient2
+    // 11. Encrypt Decrypt roundtrip with ddbClient2
     PutGetItems(ddbTableName, ddbClient2);
   }
 
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_key_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_key_example.py
index cf4c6d16e..6957290f8 100644
--- a/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_key_example.py
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_key_example.py
@@ -3,20 +3,18 @@
 """
 Example for creating a new key in a KeyStore.
 
-The Hierarchical Keyring Example and Searchable Encryption Examples
-rely on the existence of a DDB-backed key store with pre-existing
-branch key material or beacon key material.
+The Hierarchical Keyring Example and Searchable Encryption Examples rely on the
+existence of a DDB-backed key store with pre-existing branch key material or
+beacon key material.
 
-See the "Create KeyStore Table Example" for how to first set up
-the DDB Table that will back this KeyStore.
+See the "Create KeyStore Table Example" for how to first set up the DDB Table
+that will back this KeyStore.
 
-This example demonstrates configuring a KeyStore and then
-using a helper method to create a branch key and beacon key
-that share the same Id, then return that Id.
-We will always create a new beacon key alongside a new branch key,
-even if you are not using searchable encryption.
+Demonstrates configuring a KeyStore and using a helper method to create a branch
+key and beacon key that share the same Id. A new beacon key is always created
+alongside a new branch key, even if searchable encryption is not being used.
 
-This key creation should occur within your control plane.
+Note: This key creation should occur within your control plane.
 """
 
 import boto3
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_table_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_table_example.py
new file mode 100644
index 000000000..d154e33c2
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/create_keystore_table_example.py
@@ -0,0 +1,59 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Example for creating a DynamoDB table for use as a KeyStore.
+
+The Hierarchical Keyring Example and Searchable Encryption Examples rely on the
+existence of a DDB-backed key store with pre-existing branch key material or
+beacon key material.
+
+Shows how to configure a KeyStore and use a helper method to create the DDB table
+that will be used to persist branch keys and beacons keys for this KeyStore.
+
+This table creation should occur within your control plane and only needs to occur
+once. While not demonstrated in this example, you should additionally use the
+`VersionKey` API on the KeyStore to periodically rotate your branch key material.
+"""
+
+import boto3
+from aws_cryptographic_material_providers.keystore.client import KeyStore
+from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig
+from aws_cryptographic_material_providers.keystore.models import (
+    CreateKeyStoreInput,
+    KMSConfigurationKmsKeyArn,
+)
+
+
+def keystore_create_table(keystore_table_name: str, logical_keystore_name: str, kms_key_arn: str):
+    """
+    Create KeyStore Table Example.
+
+    :param keystore_table_name: The name of the DynamoDB table to create
+    :param logical_keystore_name: The logical name for this keystore
+    :param kms_key_arn: The ARN of the KMS key to use for protecting branch keys
+    """
+    # 1. Configure your KeyStore resource.
+    #    `ddb_table_name` is the name you want for the DDB table that
+    #    will back your keystore.
+    #    `kms_key_arn` is the KMS Key that will protect your branch keys and beacon keys
+    #    when they are stored in your DDB table.
+    keystore = KeyStore(
+        config=KeyStoreConfig(
+            ddb_client=boto3.client("dynamodb"),
+            ddb_table_name=keystore_table_name,
+            logical_key_store_name=logical_keystore_name,
+            kms_client=boto3.client("kms"),
+            kms_configuration=KMSConfigurationKmsKeyArn(kms_key_arn),
+        )
+    )
+
+    # 2. Create the DynamoDb table that will store the branch keys and beacon keys.
+    #    This checks if the correct table already exists at `ddb_table_name`
+    #    by using the DescribeTable API. If no table exists,
+    #    it will create one. If a table exists, it will verify
+    #    the table's configuration and will error if the configuration is incorrect.
+    keystore.create_key_store(input=CreateKeyStoreInput())
+    # It may take a couple of minutes for the table to become ACTIVE,
+    # at which point it is ready to store branch and beacon keys.
+    # See the Create KeyStore Key Example for how to populate
+    # this table.
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/get_encrypted_data_key_description_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/get_encrypted_data_key_description_example.py
new file mode 100644
index 000000000..f023577f2
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/get_encrypted_data_key_description_example.py
@@ -0,0 +1,82 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Example demonstrating how to get encrypted data key descriptions from DynamoDB items."""
+
+import boto3
+from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.client import DynamoDbEncryption
+from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.config import (
+    DynamoDbEncryptionConfig,
+)
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    GetEncryptedDataKeyDescriptionInput,
+    GetEncryptedDataKeyDescriptionUnionItem,
+)
+
+
+def get_encrypted_data_key_description(
+    table_name: str,
+    partition_key: str,
+    partition_key_val: str,
+    sort_key_name: str,
+    sort_key_value: str,
+    expected_key_provider_id: str,
+    expected_key_provider_info: str,
+    expected_branch_key_id: str,
+    expected_branch_key_version: str,
+):
+    """
+    Get encrypted data key description from a DynamoDB item.
+
+    :param table_name: The name of the DynamoDB table
+    :param partition_key: The name of the partition key
+    :param partition_key_val: The value of the partition key
+    :param sort_key_name: The name of the sort key
+    :param sort_key_value: The value of the sort key
+    :param expected_key_provider_id: The expected key provider ID
+    :param expected_key_provider_info: The expected key provider info (optional)
+    :param expected_branch_key_id: The expected branch key ID (optional)
+    :param expected_branch_key_version: The expected branch key version (optional)
+    """
+    # 1. Create a new AWS SDK DynamoDb client. This client will be used to get item from the DynamoDB table
+    ddb = boto3.client("dynamodb")
+
+    # 2. Get item from the DynamoDB table. This item will be used to Get Encrypted DataKey Description
+    key_to_get = {partition_key: {"S": partition_key_val}, sort_key_name: {"N": sort_key_value}}
+
+    response = ddb.get_item(TableName=table_name, Key=key_to_get)
+
+    returned_item = response.get("Item", {})
+    if not returned_item:
+        print(f"No item found with the key {partition_key}!")
+        return
+
+    # 3. Prepare the input for GetEncryptedDataKeyDescription method.
+    # This input can be a DynamoDB item or a header. For now, we are giving input as a DynamoDB item
+    # but users can also extract the header from the attribute "aws_dbe_head" in the DynamoDB table
+    # and use it for GetEncryptedDataKeyDescription method.
+    ddb_enc = DynamoDbEncryption(config=DynamoDbEncryptionConfig())
+
+    input_union = GetEncryptedDataKeyDescriptionUnionItem(returned_item)
+
+    input_obj = GetEncryptedDataKeyDescriptionInput(input=input_union)
+
+    output = ddb_enc.get_encrypted_data_key_description(input=input_obj)
+
+    # In the following code, we are giving input as header instead of a complete DynamoDB item
+    # This code is provided solely to demo how the alternative approach works. So, it is commented.
+
+    # header_attribute = "aws_dbe_head"
+    # header = returned_item[header_attribute]["B"]
+    # input_union = GetEncryptedDataKeyDescriptionUnion(
+    #     header=header
+    # )
+
+    # Assert everything
+    assert output.encrypted_data_key_description_output[0].key_provider_id == expected_key_provider_id
+
+    if expected_key_provider_id.startswith("aws-kms"):
+        assert output.encrypted_data_key_description_output[0].key_provider_info == expected_key_provider_info
+
+    if output.encrypted_data_key_description_output[0].key_provider_id == "aws-kms-hierarchy":
+        assert output.encrypted_data_key_description_output[0].branch_key_id == expected_branch_key_id
+        assert output.encrypted_data_key_description_output[0].branch_key_version == expected_branch_key_version
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/item_encryptor/encrypt_decrypt_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/item_encryptor/encrypt_decrypt_example.py
index 40e5e15b0..daf8082f0 100644
--- a/Examples/runtimes/python/DynamoDBEncryption/src/item_encryptor/encrypt_decrypt_example.py
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/item_encryptor/encrypt_decrypt_example.py
@@ -45,7 +45,7 @@
 )
 
 
-def encrypt_decrypt_example(kms_key_id: str, ddb_table_name: str) -> None:
+def encrypt_decrypt_example(kms_key_id: str, ddb_table_name: str):
     """Encrypt and decrypt an item with an ItemEncryptor."""
     # 1. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data.
     #    For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use.
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/__init__.py
new file mode 100644
index 000000000..fa977e22f
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/__init__.py
@@ -0,0 +1,3 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Stub to allow relative imports of examples from tests."""
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/example_branch_key_id_supplier.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/example_branch_key_id_supplier.py
new file mode 100644
index 000000000..f16218a46
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/example_branch_key_id_supplier.py
@@ -0,0 +1,61 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Example implementation of a branch key ID supplier.
+
+Used in the 'HierarchicalKeyringExample'.
+In that example, we have a table where we distinguish multiple tenants
+by a tenant ID that is stored in our partition attribute.
+The expectation is that this does not produce a confused deputy
+because the tenants are separated by partition.
+In order to create a Hierarchical Keyring that is capable of encrypting or
+decrypting data for either tenant, we implement this interface
+to map the correct branch key ID to the correct tenant ID.
+"""
+from typing import Dict
+
+from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.references import (
+    IDynamoDbKeyBranchKeyIdSupplier,
+)
+from aws_dbesdk_dynamodb.structures.dynamodb import GetBranchKeyIdFromDdbKeyInput, GetBranchKeyIdFromDdbKeyOutput
+
+
+class ExampleBranchKeyIdSupplier(IDynamoDbKeyBranchKeyIdSupplier):
+    """Example implementation of a branch key ID supplier."""
+
+    branch_key_id_for_tenant1: str
+    branch_key_id_for_tenant2: str
+
+    def __init__(self, tenant1_id: str, tenant2_id: str):
+        """
+        Initialize a branch key ID supplier.
+
+        :param tenant1_id: Branch key ID for tenant 1
+        :param tenant2_id: Branch key ID for tenant 2
+        """
+        self.branch_key_id_for_tenant1 = tenant1_id
+        self.branch_key_id_for_tenant2 = tenant2_id
+
+    def get_branch_key_id_from_ddb_key(self, param: GetBranchKeyIdFromDdbKeyInput) -> GetBranchKeyIdFromDdbKeyOutput:
+        """
+        Get branch key ID from the tenant ID in input's DDB key.
+
+        :param param: Input containing DDB key
+        :return: Output containing branch key ID
+        :raises ValueError: If DDB key is invalid or contains invalid tenant ID
+        """
+        key: Dict[str, Dict] = param.ddb_key
+
+        if "partition_key" not in key:
+            raise ValueError("Item invalid, does not contain expected partition key attribute.")
+
+        tenant_key_id = key["partition_key"]["S"]
+
+        if tenant_key_id == "tenant1Id":
+            branch_key_id = self.branch_key_id_for_tenant1
+        elif tenant_key_id == "tenant2Id":
+            branch_key_id = self.branch_key_id_for_tenant2
+        else:
+            raise ValueError("Item does not contain valid tenant ID")
+
+        return GetBranchKeyIdFromDdbKeyOutput(branch_key_id=branch_key_id)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example.py
new file mode 100644
index 000000000..9c68125e6
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/hierarchical_keyring_example.py
@@ -0,0 +1,229 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Example demonstrating DynamoDb Encryption using a Hierarchical Keyring.
+
+This example sets up DynamoDb Encryption for the AWS SDK client
+using the Hierarchical Keyring, which establishes a key hierarchy
+where "branch" keys are persisted in DynamoDb.
+These branch keys are used to protect your data keys,
+and these branch keys are themselves protected by a root KMS Key.
+
+Establishing a key hierarchy like this has two benefits:
+
+First, by caching the branch key material, and only calling back
+to KMS to re-establish authentication regularly according to your configured TTL,
+you limit how often you need to call back to KMS to protect your data.
+This is a performance/security tradeoff, where your authentication, audit, and
+logging from KMS is no longer one-to-one with every encrypt or decrypt call.
+However, the benefit is that you no longer have to make a
+network call to KMS for every encrypt or decrypt.
+
+Second, this key hierarchy makes it easy to hold multi-tenant data
+that is isolated per branch key in a single DynamoDb table.
+You can create a branch key for each tenant in your table,
+and encrypt all that tenant's data under that distinct branch key.
+On decrypt, you can either statically configure a single branch key
+to ensure you are restricting decryption to a single tenant,
+or you can implement an interface that lets you map the primary key on your items
+to the branch key that should be responsible for decrypting that data.
+
+This example then demonstrates configuring a Hierarchical Keyring
+with a Branch Key ID Supplier to encrypt and decrypt data for
+two separate tenants.
+
+Running this example requires access to the DDB Table whose name
+is provided in CLI arguments.
+This table must be configured with the following
+primary key configuration:
+  - Partition key is named "partition_key" with type (S)
+  - Sort key is named "sort_key" with type (S)
+
+This example also requires using a KMS Key whose ARN
+is provided in CLI arguments. You need the following access
+on this key:
+  - GenerateDataKeyWithoutPlaintext
+  - Decrypt
+"""
+
+import boto3
+from aws_cryptographic_material_providers.keystore.client import KeyStore
+from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig
+from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn
+from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
+from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
+from aws_cryptographic_material_providers.mpl.models import (
+    CacheTypeDefault,
+    CreateAwsKmsHierarchicalKeyringInput,
+    DefaultCache,
+)
+from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
+from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.client import DynamoDbEncryption
+from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb.config import (
+    DynamoDbEncryptionConfig,
+)
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    CreateDynamoDbEncryptionBranchKeyIdSupplierInput,
+    DynamoDbTableEncryptionConfig,
+    DynamoDbTablesEncryptionConfig,
+)
+from aws_dbesdk_dynamodb.structures.structured_encryption import (
+    CryptoAction,
+)
+
+from .example_branch_key_id_supplier import ExampleBranchKeyIdSupplier
+
+
+def hierarchical_keyring_get_item_put_item(
+    ddb_table_name: str,
+    tenant1_branch_key_id: str,
+    tenant2_branch_key_id: str,
+    keystore_table_name: str,
+    logical_keystore_name: str,
+    kms_key_id: str,
+):
+    """
+    Demonstrate using a hierarchical keyring with multiple tenants.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param tenant1_branch_key_id: Branch key ID for tenant 1
+    :param tenant2_branch_key_id: Branch key ID for tenant 2
+    :param keystore_table_name: The name of the KeyStore DynamoDB table
+    :param logical_keystore_name: The logical name for this keystore
+    :param kms_key_id: The ARN of the KMS key to use
+    """
+    # Initial KeyStore Setup: This example requires that you have already
+    # created your KeyStore, and have populated it with two new branch keys.
+    # See the "Create KeyStore Table Example" and "Create KeyStore Key Example"
+    # for an example of how to do this.
+
+    # 1. Configure your KeyStore resource.
+    #    This SHOULD be the same configuration that you used
+    #    to initially create and populate your KeyStore.
+    keystore = KeyStore(
+        config=KeyStoreConfig(
+            ddb_client=boto3.client("dynamodb"),
+            ddb_table_name=keystore_table_name,
+            logical_key_store_name=logical_keystore_name,
+            kms_client=boto3.client("kms"),
+            kms_configuration=KMSConfigurationKmsKeyArn(kms_key_id),
+        )
+    )
+
+    # 2. Create a Branch Key ID Supplier. See ExampleBranchKeyIdSupplier in this directory.
+    ddb_enc = DynamoDbEncryption(config=DynamoDbEncryptionConfig())
+    branch_key_id_supplier = ddb_enc.create_dynamo_db_encryption_branch_key_id_supplier(
+        input=CreateDynamoDbEncryptionBranchKeyIdSupplierInput(
+            ddb_key_branch_key_id_supplier=ExampleBranchKeyIdSupplier(tenant1_branch_key_id, tenant2_branch_key_id)
+        )
+    ).branch_key_id_supplier
+
+    # 3. Create the Hierarchical Keyring, using the Branch Key ID Supplier above.
+    #    With this configuration, the AWS SDK Client ultimately configured will be capable
+    #    of encrypting or decrypting items for either tenant (assuming correct KMS access).
+    #    If you want to restrict the client to only encrypt or decrypt for a single tenant,
+    #    configure this Hierarchical Keyring using `.branch_key_id=tenant1_branch_key_id` instead
+    #    of `.branch_key_id_supplier=branch_key_id_supplier`.
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    keyring_input = CreateAwsKmsHierarchicalKeyringInput(
+        key_store=keystore,
+        branch_key_id_supplier=branch_key_id_supplier,
+        ttl_seconds=600,  # This dictates how often we call back to KMS to authorize use of the branch keys
+        cache=CacheTypeDefault(  # This dictates how many branch keys will be held locally
+            value=DefaultCache(entry_capacity=100)
+        ),
+    )
+
+    hierarchical_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input)
+
+    # 4. Configure which attributes are encrypted and/or signed when writing new items.
+    #    For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    #    we must explicitly configure how they should be treated during item encryption:
+    #      - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #      - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #      - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    attribute_actions = {
+        "partition_key": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "sort_key": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "tenant_sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
+    }
+
+    # 5. Configure which attributes we expect to be included in the signature
+    #    when reading items. There are two options for configuring this:
+    #
+    #    - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
+    #      When defining your DynamoDb schema and deciding on attribute names,
+    #      choose a distinguishing prefix (such as ":") for all attributes that
+    #      you do not want to include in the signature.
+    #      This has two main benefits:
+    #      - It is easier to reason about the security and authenticity of data within your item
+    #        when all unauthenticated data is easily distinguishable by their attribute name.
+    #      - If you need to add new unauthenticated attributes in the future,
+    #        you can easily make the corresponding update to your `attribute_actions`
+    #        and immediately start writing to that new attribute, without
+    #        any other configuration update needed.
+    #      Once you configure this field, it is not safe to update it.
+    #
+    #    - Configure `allowed_unsigned_attributes`: You may also explicitly list
+    #      a set of attributes that should be considered unauthenticated when encountered
+    #      on read. Be careful if you use this configuration. Do not remove an attribute
+    #      name from this configuration, even if you are no longer writing with that attribute,
+    #      as old items may still include this attribute, and our configuration needs to know
+    #      to continue to exclude this attribute from the signature scope.
+    #      If you add new attribute names to this field, you must first deploy the update to this
+    #      field to all readers in your host fleet before deploying the update to start writing
+    #      with that new attribute.
+    #
+    #   For this example, we currently authenticate all attributes. To make it easier to
+    #   add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
+    unsign_attr_prefix = ":"
+
+    # 6. Create the DynamoDb Encryption configuration for the table we will be writing to.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=hierarchical_keyring,
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # 7. Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # 8. Put an item into our table using the above client.
+    #    Before the item gets sent to DynamoDb, it will be encrypted
+    #    client-side, according to our configuration.
+    #    Because the item we are writing uses "tenantId1" as our partition value,
+    #    based on the code we wrote in the ExampleBranchKeySupplier,
+    #    `tenant1_branch_key_id` will be used to encrypt this item.
+    item = {
+        "partition_key": {"S": "tenant1Id"},
+        "sort_key": {"N": "0"},
+        "tenant_sensitive_data": {"S": "encrypt and sign me!"},
+    }
+
+    put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
+
+    # Demonstrate that PutItem succeeded
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # 9. Get the item back from our table using the same client.
+    #    The client will decrypt the item client-side, and return
+    #    back the original item.
+    #    Because the returned item's partition value is "tenantId1",
+    #    based on the code we wrote in the ExampleBranchKeySupplier,
+    #    `tenant1_branch_key_id` will be used to decrypt this item.
+    key_to_get = {"partition_key": {"S": "tenant1Id"}, "sort_key": {"N": "0"}}
+
+    get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
+
+    # Demonstrate that GetItem succeeded and returned the decrypted item
+    assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    returned_item = get_response["Item"]
+    assert returned_item["tenant_sensitive_data"]["S"] == "encrypt and sign me!"
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example.py
new file mode 100644
index 000000000..3af215b1f
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_ecdh_keyring_example.py
@@ -0,0 +1,451 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+These examples set up DynamoDb Encryption for the AWS SDK client using the AWS KMS ECDH Keyring.
+
+This keyring, depending on its KeyAgreement scheme,
+takes in the sender's KMS ECC Key ARN, and the recipient's ECC Public Key to derive a shared secret.
+The keyring uses the shared secret to derive a data key to protect the
+data keys that encrypt and decrypt DynamoDb table items.
+
+Running these examples require access to the DDB Table whose name
+is provided in CLI arguments.
+This table must be configured with the following
+primary key configuration:
+  - Partition key is named "partition_key" with type (S)
+  - Sort key is named "sort_key" with type (S)
+"""
+
+import pathlib
+
+import boto3
+from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
+from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
+from aws_cryptographic_material_providers.mpl.models import (
+    CreateAwsKmsEcdhKeyringInput,
+    DBEAlgorithmSuiteId,
+    KmsEcdhStaticConfigurationsKmsPrivateKeyToStaticPublicKey,
+    KmsEcdhStaticConfigurationsKmsPublicKeyDiscovery,
+    KmsPrivateKeyToStaticPublicKeyInput,
+    KmsPublicKeyDiscoveryInput,
+)
+from aws_cryptographic_material_providers.mpl.references import IKeyring
+from aws_cryptography_primitives.smithygenerated.aws_cryptography_primitives.models import ECDHCurveSpec
+from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    DynamoDbTableEncryptionConfig,
+    DynamoDbTablesEncryptionConfig,
+)
+from aws_dbesdk_dynamodb.structures.structured_encryption import (
+    CryptoAction,
+)
+from cryptography.hazmat.primitives import serialization
+
+EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME = "KmsEccKeyringKeyringExamplePublicKeySender.pem"
+EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME = "KmsEccKeyringKeyringExamplePublicKeyRecipient.pem"
+
+
+def kms_ecdh_keyring_get_item_put_item(
+    ddb_table_name: str,
+    ecc_key_arn: str,
+    ecc_public_key_sender_filename: str = EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME,
+    ecc_public_key_recipient_filename: str = EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME,
+):
+    """
+    Demonstrate using a KMS ECDH keyring with static keys.
+
+    This example takes in the sender's KMS ECC key ARN, the sender's public key,
+    the recipient's public key, and the algorithm definition where the ECC keys lie.
+    The ecc_key_arn parameter takes in the sender's KMS ECC key ARN,
+    the ecc_public_key_sender_filename parameter takes in the sender's public key that corresponds to the
+    ecc_key_arn, the ecc_public_key_recipient_filename parameter takes in the recipient's public key,
+    and the Curve Specification where the keys lie.
+
+    Both public keys MUST be UTF8 PEM-encoded X.509 public key, also known as SubjectPublicKeyInfo (SPKI)
+
+    This example encrypts a test item using the provided ECC keys and puts the
+    encrypted item to the provided DynamoDb table. Then, it gets the
+    item from the table and decrypts it.
+
+    Running this example requires access to the DDB Table whose name
+    is provided in CLI arguments.
+    This table must be configured with the following
+    primary key configuration:
+      - Partition key is named "partition_key" with type (S)
+      - Sort key is named "sort_key" with type (S)
+    This example also requires access to a KMS ECC key.
+    Our tests provide a KMS ECC Key ARN that anyone can use, but you
+    can also provide your own KMS ECC key.
+    To use your own KMS ECC key, you must have either:
+     - Its public key downloaded in a UTF-8 encoded PEM file
+     - kms:GetPublicKey permissions on that key.
+    If you do not have the public key downloaded, running this example
+    through its main method will download the public key for you
+    by calling kms:GetPublicKey.
+    You must also have kms:DeriveSharedSecret permissions on the KMS ECC key.
+    This example also requires a recipient ECC Public Key that lies on the same
+    curve as the sender public key. This examples uses another distinct
+    KMS ECC Public Key, it does not have to be a KMS key; it can be a
+    valid SubjectPublicKeyInfo (SPKI) Public Key.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param ecc_key_arn: The ARN of the KMS ECC key to use
+    :param ecc_public_key_sender_filename: The filename containing the sender's public key
+    :param ecc_public_key_recipient_filename: The filename containing the recipient's public key
+    """
+    # Load UTF-8 encoded public key PEM files as DER encoded bytes.
+    # You may provide your own PEM files to use here. If you provide this, it MUST
+    # be a key on curve P256.
+    # If not, the main method in this class will call
+    # the KMS ECC key, retrieve its public key, and store it
+    # in a PEM file for example use.
+    public_key_recipient_bytes = load_public_key_bytes(ecc_public_key_recipient_filename)
+    public_key_sender_bytes = load_public_key_bytes(ecc_public_key_sender_filename)
+
+    # Create a KMS ECDH keyring.
+    # This keyring uses the KmsPrivateKeyToStaticPublicKey configuration. This configuration calls for both of
+    # the keys to be on the same curve (P256, P384, P521).
+    # On encrypt, the keyring calls AWS KMS to derive the shared secret from the sender's KMS ECC Key ARN
+    # and the recipient's public key.
+    # For this example, on decrypt, the keyring calls AWS KMS to derive the shared secret from the
+    # sender's KMS ECC Key ARN and the recipient's public key;
+    # however, on decrypt, the recipient can construct a keyring such that the shared secret is calculated with
+    # the recipient's private key and the sender's public key. In both scenarios the shared secret will be the same.
+    # For more information on this configuration see:
+    # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-ecdh-keyring.html#kms-ecdh-create
+    # The DynamoDb encryption client uses this keyring to encrypt and decrypt items.
+    # This keyring takes in:
+    #  - kms_client
+    #  - kms_key_id: Must be an ARN representing a KMS ECC key meant for KeyAgreement
+    #  - curve_spec: The curve name where the public keys lie
+    #  - sender_public_key: A ByteBuffer of a UTF-8 encoded public
+    #               key for the key passed into kms_key_id in DER format
+    #  - recipient_public_key: A ByteBuffer of a UTF-8 encoded public
+    #               key for the key passed into kms_key_id in DER format
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    keyring_input = CreateAwsKmsEcdhKeyringInput(
+        kms_client=boto3.client("kms"),
+        curve_spec=ECDHCurveSpec.ECC_NIST_P256,
+        key_agreement_scheme=KmsEcdhStaticConfigurationsKmsPrivateKeyToStaticPublicKey(
+            KmsPrivateKeyToStaticPublicKeyInput(
+                sender_kms_identifier=ecc_key_arn,
+                # Must be a DER-encoded X.509 public key
+                sender_public_key=public_key_sender_bytes,
+                # Must be a DER-encoded X.509 public key
+                recipient_public_key=public_key_recipient_bytes,
+            )
+        ),
+    )
+
+    kms_ecdh_keyring = mat_prov.create_aws_kms_ecdh_keyring(input=keyring_input)
+
+    put_get_item_with_keyring(kms_ecdh_keyring, ddb_table_name)
+
+
+def kms_ecdh_discovery_get_item(ddb_table_name: str, ecc_recipient_key_arn: str):
+    """
+    Demonstrate using a KMS ECDH keyring with discovery.
+
+    This example takes in the recipient's KMS ECC key ARN via
+    the ecc_recipient_key_arn parameter.
+
+    This example attempts to decrypt a test item using the provided ecc_recipient_key_arn,
+    it does so by checking if the message header contains the recipient's public key.
+
+    Running this example requires access to the DDB Table whose name
+    is provided in CLI arguments.
+    This table must be configured with the following
+    primary key configuration:
+      - Partition key is named "partition_key" with type (S)
+      - Sort key is named "sort_key" with type (S)
+    This example also requires access to a KMS ECC key.
+    Our tests provide a KMS ECC Key ARN that anyone can use, but you
+    can also provide your own KMS ECC key.
+    To use your own KMS ECC key, you must have:
+     - kms:GetPublicKey permissions on that key.
+    This example will call kms:GetPublicKey on keyring creation.
+    You must also have kms:DeriveSharedSecret permissions on the KMS ECC key.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param ecc_recipient_key_arn: The ARN of the recipient's KMS ECC key
+    """
+    # Create a KMS ECDH keyring.
+    # This keyring uses the KmsPublicKeyDiscovery configuration.
+    # On encrypt, the keyring will fail as it is not allowed to encrypt data under this configuration.
+    # On decrypt, the keyring will check if its corresponding public key is stored in the message header. It
+    # will AWS KMS to derive the shared from the recipient's KMS ECC Key ARN and the sender's public key;
+    # For more information on this configuration see:
+    # https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-ecdh-keyring.html#kms-ecdh-discovery
+    # The DynamoDb encryption client uses this to encrypt and decrypt items.
+    # This keyring takes in:
+    #  - kms_client
+    #  - recipient_kms_identifier: Must be an ARN representing a KMS ECC key meant for KeyAgreement
+    #  - curve_spec: The curve name where the public keys lie
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    keyring_input = CreateAwsKmsEcdhKeyringInput(
+        kms_client=boto3.client("kms"),
+        curve_spec=ECDHCurveSpec.ECC_NIST_P256,
+        key_agreement_scheme=KmsEcdhStaticConfigurationsKmsPublicKeyDiscovery(
+            KmsPublicKeyDiscoveryInput(recipient_kms_identifier=ecc_recipient_key_arn)
+        ),
+    )
+
+    kms_ecdh_keyring = mat_prov.create_aws_kms_ecdh_keyring(input=keyring_input)
+
+    get_item_with_keyring(kms_ecdh_keyring, ddb_table_name)
+
+
+def get_item_with_keyring(kms_ecdh_keyring: IKeyring, ddb_table_name: str):
+    """
+    Demonstrate get operation with a KMS ECDH keyring.
+
+    :param kms_ecdh_keyring: The KMS ECDH keyring to use
+    :param ddb_table_name: The name of the DynamoDB table
+    """
+    # Configure which attributes are encrypted and/or signed when writing new items.
+    # For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    # we must explicitly configure how they should be treated during item encryption:
+    #   - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #   - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #   - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    attribute_actions = {
+        "partition_key": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "sort_key": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
+    }
+
+    # Configure which attributes we expect to be included in the signature
+    # when reading items. There are two options for configuring this:
+    #
+    # - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
+    #   When defining your DynamoDb schema and deciding on attribute names,
+    #   choose a distinguishing prefix (such as ":") for all attributes that
+    #   you do not want to include in the signature.
+    #   This has two main benefits:
+    #   - It is easier to reason about the security and authenticity of data within your item
+    #     when all unauthenticated data is easily distinguishable by their attribute name.
+    #   - If you need to add new unauthenticated attributes in the future,
+    #     you can easily make the corresponding update to your `attribute_actions`
+    #     and immediately start writing to that new attribute, without
+    #     any other configuration update needed.
+    #   Once you configure this field, it is not safe to update it.
+    #
+    # - Configure `allowed_unsigned_attributes`: You may also explicitly list
+    #   a set of attributes that should be considered unauthenticated when encountered
+    #   on read. Be careful if you use this configuration. Do not remove an attribute
+    #   name from this configuration, even if you are no longer writing with that attribute,
+    #   as old items may still include this attribute, and our configuration needs to know
+    #   to continue to exclude this attribute from the signature scope.
+    #   If you add new attribute names to this field, you must first deploy the update to this
+    #   field to all readers in your host fleet before deploying the update to start writing
+    #   with that new attribute.
+    #
+    # For this example, we currently authenticate all attributes. To make it easier to
+    # add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
+    unsign_attr_prefix = ":"
+
+    # Create the DynamoDb Encryption configuration for the table we will be writing to.
+    # Note: To use the KMS RSA keyring, your table config must specify an algorithmSuite
+    # that does not use asymmetric signing.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=kms_ecdh_keyring,
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+        # Specify algorithmSuite without asymmetric signing here
+        # As of v3.0.0, the only supported algorithmSuite without asymmetric signing is
+        # ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384.
+        algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384,
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # Get the item back from our table using the client.
+    # The client will decrypt the item client-side using the ECDH keyring
+    # and return the original item.
+    key_to_get = {"partition_key": {"S": "awsKmsEcdhKeyringItem"}, "sort_key": {"N": "0"}}
+
+    get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
+
+    # Demonstrate that GetItem succeeded and returned the decrypted item
+    assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    returned_item = get_response["Item"]
+    assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
+
+
+def put_get_item_with_keyring(aws_kms_ecdh_keyring: IKeyring, ddb_table_name: str):
+    """
+    Demonstrate put and get operations with a KMS ECDH keyring.
+
+    :param aws_kms_ecdh_keyring: The KMS ECDH keyring to use
+    :param ddb_table_name: The name of the DynamoDB table
+    """
+    # Configure which attributes are encrypted and/or signed when writing new items.
+    # For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    # we must explicitly configure how they should be treated during item encryption:
+    #   - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #   - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #   - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    attribute_actions = {
+        "partition_key": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "sort_key": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
+    }
+
+    # Configure which attributes we expect to be included in the signature
+    # when reading items. There are two options for configuring this:
+    #
+    # - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
+    #   When defining your DynamoDb schema and deciding on attribute names,
+    #   choose a distinguishing prefix (such as ":") for all attributes that
+    #   you do not want to include in the signature.
+    #   This has two main benefits:
+    #   - It is easier to reason about the security and authenticity of data within your item
+    #     when all unauthenticated data is easily distinguishable by their attribute name.
+    #   - If you need to add new unauthenticated attributes in the future,
+    #     you can easily make the corresponding update to your `attribute_actions`
+    #     and immediately start writing to that new attribute, without
+    #     any other configuration update needed.
+    #   Once you configure this field, it is not safe to update it.
+    #
+    # - Configure `allowed_unsigned_attributes`: You may also explicitly list
+    #   a set of attributes that should be considered unauthenticated when encountered
+    #   on read. Be careful if you use this configuration. Do not remove an attribute
+    #   name from this configuration, even if you are no longer writing with that attribute,
+    #   as old items may still include this attribute, and our configuration needs to know
+    #   to continue to exclude this attribute from the signature scope.
+    #   If you add new attribute names to this field, you must first deploy the update to this
+    #   field to all readers in your host fleet before deploying the update to start writing
+    #   with that new attribute.
+    #
+    # For this example, we currently authenticate all attributes. To make it easier to
+    # add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
+    unsign_attr_prefix = ":"
+
+    # Create the DynamoDb Encryption configuration for the table we will be writing to.
+    # Note: To use the KMS RSA keyring, your table config must specify an algorithmSuite
+    # that does not use asymmetric signing.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=aws_kms_ecdh_keyring,
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+        # Specify algorithmSuite without asymmetric signing here
+        # As of v3.0.0, the only supported algorithmSuite without asymmetric signing is
+        # ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384.
+        algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384,
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # Put an item into our table using the above client.
+    # Before the item gets sent to DynamoDb, it will be encrypted
+    # client-side, according to our configuration.
+    item = {
+        "partition_key": {"S": "awsKmsEcdhKeyringItem"},
+        "sort_key": {"N": "0"},
+        "sensitive_data": {"S": "encrypt and sign me!"},
+    }
+
+    put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
+
+    # Demonstrate that PutItem succeeded
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # Get the item back from our table using the client.
+    # The client will decrypt the item client-side using the RSA keyring
+    # and return the original item.
+    key_to_get = {"partition_key": {"S": "awsKmsEcdhKeyringItem"}, "sort_key": {"N": "0"}}
+
+    get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
+
+    # Demonstrate that GetItem succeeded and returned the decrypted item
+    assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    returned_item = get_response["Item"]
+    assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
+
+
+def load_public_key_bytes(ecc_public_key_filename: str) -> bytes:
+    """
+    Load public key bytes from a PEM file.
+
+    :param ecc_public_key_filename: The filename containing the public key
+    :return: The public key bytes
+    """
+    try:
+        with open(ecc_public_key_filename, "rb") as f:
+            public_key_file_bytes = f.read()
+            public_key = serialization.load_pem_public_key(public_key_file_bytes)
+            return public_key.public_bytes(
+                encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo
+            )
+    except IOError as e:
+        raise OSError("IOError while reading public key from file") from e
+
+
+def should_get_new_public_keys() -> bool:
+    """
+    Check if new public keys should be generated.
+
+    :return: True if new keys should be generated, False otherwise
+    """
+    # Check if public keys already exist
+    sender_public_key_file = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME)
+    recipient_public_key_file = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME)
+
+    if sender_public_key_file.exists() or recipient_public_key_file.exists():
+        return False
+
+    if not sender_public_key_file.exists() and recipient_public_key_file.exists():
+        raise FileNotFoundError(f"Missing public key sender file at {EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME}")
+
+    if not recipient_public_key_file.exists() and sender_public_key_file.exists():
+        raise FileNotFoundError(f"Missing public key recipient file at {EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME}")
+
+    return True
+
+
+def write_public_key_pem_for_ecc_key(ecc_key_arn: str, ecc_public_key_filename: str):
+    """
+    Write a public key PEM file for an ECC key.
+
+    :param ecc_key_arn: The ARN of the KMS ECC key
+    :param ecc_public_key_filename: The filename to write the public key to
+    """
+    # Safety check: Validate file is not present
+    public_key_file = pathlib.Path(ecc_public_key_filename)
+    if public_key_file.exists():
+        raise FileExistsError("writePublicKeyPemForEccKey will not overwrite existing PEM files")
+
+    # This code will call KMS to get the public key for the KMS ECC key.
+    # You must have kms:GetPublicKey permissions on the key for this to succeed.
+    # The public key will be written to the file EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME
+    # or EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME.
+    kms_client = boto3.client("kms")
+    response = kms_client.get_public_key(KeyId=ecc_key_arn)
+    public_key_bytes = response["PublicKey"]
+
+    # Write the public key to a PEM file
+    public_key = serialization.load_der_public_key(public_key_bytes)
+    pem_data = public_key.public_bytes(
+        encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo
+    )
+
+    with open(ecc_public_key_filename, "wb") as f:
+        f.write(pem_data)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example.py
new file mode 100644
index 000000000..dd18e9e48
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/kms_rsa_keyring_example.py
@@ -0,0 +1,230 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Example demonstrating DynamoDb Encryption using a KMS RSA Keyring.
+
+The KMS RSA Keyring uses a KMS RSA key pair to encrypt and decrypt records. The client
+uses the downloaded public key to encrypt items it adds to the table. The keyring
+uses the private key to decrypt existing table items it retrieves by calling
+KMS' decrypt API.
+
+Running this example requires access to the DDB Table whose name is provided
+in CLI arguments. This table must be configured with the following primary key
+configuration:
+  - Partition key is named "partition_key" with type (S)
+  - Sort key is named "sort_key" with type (S)
+
+The example also requires access to a KMS RSA key. Our tests provide a KMS RSA
+ARN that anyone can use, but you can also provide your own KMS RSA key.
+To use your own KMS RSA key, you must have either:
+ - Its public key downloaded in a UTF-8 encoded PEM file
+ - kms:GetPublicKey permissions on that key
+
+If you do not have the public key downloaded, running this example through its
+main method will download the public key for you by calling kms:GetPublicKey.
+You must also have kms:Decrypt permissions on the KMS RSA key.
+"""
+
+import os
+
+import boto3
+from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
+from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
+from aws_cryptographic_material_providers.mpl.models import (
+    CreateAwsKmsRsaKeyringInput,
+    DBEAlgorithmSuiteId,
+)
+from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    DynamoDbTableEncryptionConfig,
+    DynamoDbTablesEncryptionConfig,
+)
+from aws_dbesdk_dynamodb.structures.structured_encryption import (
+    CryptoAction,
+)
+from cryptography.hazmat.primitives import serialization
+
+DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME = "KmsRsaKeyringExamplePublicKey.pem"
+
+
+def kms_rsa_keyring_example(
+    ddb_table_name: str, rsa_key_arn: str, rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME
+):
+    """
+    Create a KMS RSA keyring and use it to encrypt/decrypt DynamoDB items.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param rsa_key_arn: ARN of the KMS RSA key
+    :param rsa_public_key_filename: Path to the public key PEM file
+    """
+    # 1. Load UTF-8 encoded public key PEM file.
+    #    You may have an RSA public key file already defined.
+    #    If not, the main method in this class will call
+    #    the KMS RSA key, retrieve its public key, and store it
+    #    in a PEM file for example use.
+    try:
+        with open(rsa_public_key_filename, "rb") as f:
+            public_key_utf8_encoded = f.read()
+    except IOError as e:
+        raise RuntimeError("IOError while reading public key from file") from e
+
+    # 2. Create a KMS RSA keyring.
+    #    This keyring takes in:
+    #     - kms_client
+    #     - kms_key_id: Must be an ARN representing a KMS RSA key
+    #     - public_key: A ByteBuffer of a UTF-8 encoded PEM file representing the public
+    #                  key for the key passed into kms_key_id
+    #     - encryption_algorithm: Must be either RSAES_OAEP_SHA_256 or RSAES_OAEP_SHA_1
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    keyring_input = CreateAwsKmsRsaKeyringInput(
+        kms_key_id=rsa_key_arn,
+        kms_client=boto3.client("kms"),
+        public_key=public_key_utf8_encoded,
+        encryption_algorithm="RSAES_OAEP_SHA_256",
+    )
+
+    kms_rsa_keyring = mat_prov.create_aws_kms_rsa_keyring(input=keyring_input)
+
+    # 3. Configure which attributes are encrypted and/or signed when writing new items.
+    #    For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    #    we must explicitly configure how they should be treated during item encryption:
+    #      - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #      - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #      - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    attribute_actions = {
+        "partition_key": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "sort_key": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
+    }
+
+    # 4. Configure which attributes we expect to be included in the signature
+    #    when reading items. There are two options for configuring this:
+    #
+    #    - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
+    #      When defining your DynamoDb schema and deciding on attribute names,
+    #      choose a distinguishing prefix (such as ":") for all attributes that
+    #      you do not want to include in the signature.
+    #      This has two main benefits:
+    #      - It is easier to reason about the security and authenticity of data within your item
+    #        when all unauthenticated data is easily distinguishable by their attribute name.
+    #      - If you need to add new unauthenticated attributes in the future,
+    #        you can easily make the corresponding update to your `attribute_actions`
+    #        and immediately start writing to that new attribute, without
+    #        any other configuration update needed.
+    #      Once you configure this field, it is not safe to update it.
+    #
+    #    - Configure `allowed_unsigned_attributes`: You may also explicitly list
+    #      a set of attributes that should be considered unauthenticated when encountered
+    #      on read. Be careful if you use this configuration. Do not remove an attribute
+    #      name from this configuration, even if you are no longer writing with that attribute,
+    #      as old items may still include this attribute, and our configuration needs to know
+    #      to continue to exclude this attribute from the signature scope.
+    #      If you add new attribute names to this field, you must first deploy the update to this
+    #      field to all readers in your host fleet before deploying the update to start writing
+    #      with that new attribute.
+    #
+    #   For this example, we currently authenticate all attributes. To make it easier to
+    #   add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
+    unsign_attr_prefix = ":"
+
+    # 5. Create the DynamoDb Encryption configuration for the table we will be writing to.
+    #    Note: To use the KMS RSA keyring, your table config must specify an algorithmSuite
+    #    that does not use asymmetric signing.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=kms_rsa_keyring,
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+        # Specify algorithmSuite without asymmetric signing here
+        # As of v3.0.0, the only supported algorithmSuite without asymmetric signing is
+        # ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384.
+        algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_SYMSIG_HMAC_SHA384,
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # 6. Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # 7. Put an item into our table using the above client.
+    #    Before the item gets sent to DynamoDb, it will be encrypted
+    #    client-side using the KMS RSA keyring.
+    item = {
+        "partition_key": {"S": "awsKmsRsaKeyringItem"},
+        "sort_key": {"N": "0"},
+        "sensitive_data": {"S": "encrypt and sign me!"},
+    }
+
+    put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
+
+    # Demonstrate that PutItem succeeded
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # 8. Get the item back from our table using the client.
+    #    The client will decrypt the item client-side using the RSA keyring
+    #    and return the original item.
+    key_to_get = {"partition_key": {"S": "awsKmsRsaKeyringItem"}, "sort_key": {"N": "0"}}
+
+    get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
+
+    # Demonstrate that GetItem succeeded and returned the decrypted item
+    assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    returned_item = get_response["Item"]
+    assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
+
+
+def should_get_new_public_key(rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME) -> bool:
+    """
+    Check if we need to get a new public key.
+
+    :param rsa_public_key_filename: Path to the public key PEM file
+    :return: True if we need to get a new public key, False otherwise
+    """
+    # Check if a public key file already exists
+    public_key_file = os.path.exists(rsa_public_key_filename)
+
+    # If a public key file already exists: do not overwrite existing file
+    if public_key_file:
+        return False
+
+    # If file is not present, generate a new key pair
+    return True
+
+
+def write_public_key_pem_for_rsa_key(
+    rsa_key_arn: str, rsa_public_key_filename: str = DEFAULT_EXAMPLE_RSA_PUBLIC_KEY_FILENAME
+):
+    """
+    Get the public key from KMS and write it to a PEM file.
+
+    :param rsa_key_arn: The ARN of the KMS RSA key
+    :param rsa_public_key_filename: Path to write the public key PEM file
+    """
+    # Safety check: Validate file is not present
+    if os.path.exists(rsa_public_key_filename):
+        raise RuntimeError("getRsaPublicKey will not overwrite existing PEM files")
+
+    # This code will call KMS to get the public key for the KMS RSA key.
+    # You must have kms:GetPublicKey permissions on the key for this to succeed.
+    # The public key will be written to the file EXAMPLE_RSA_PUBLIC_KEY_FILENAME.
+    kms_client = boto3.client("kms")
+    response = kms_client.get_public_key(KeyId=rsa_key_arn)
+    public_key_bytes = response["PublicKey"]
+
+    # Convert the public key to PEM format
+    public_key = serialization.load_der_public_key(public_key_bytes)
+    pem_data = public_key.public_bytes(
+        encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo
+    )
+
+    # Write the PEM file
+    try:
+        with open(rsa_public_key_filename, "wb") as f:
+            f.write(pem_data)
+    except IOError as e:
+        raise RuntimeError("IOError while writing public key PEM") from e
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example.py
new file mode 100644
index 000000000..7d6c77357
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_discovery_multi_keyring_example.py
@@ -0,0 +1,188 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Example demonstrating DynamoDb Encryption using a MRK discovery multi-keyring.
+
+A discovery keyring is not provided with any wrapping keys; instead, it recognizes
+the KMS key that was used to encrypt a data key, and asks KMS to decrypt with that
+KMS key. Discovery keyrings cannot be used to encrypt data.
+
+For more information on discovery keyrings, see:
+https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-keyring.html#kms-keyring-discovery
+
+The example encrypts an item using an MRK multi-keyring and puts the encrypted
+item to the configured DynamoDb table. Then, it gets the item from the table and
+decrypts it using the discovery keyring.
+
+Running this example requires access to the DDB Table whose name is provided in
+CLI arguments. This table must be configured with the following primary key
+configuration:
+  - Partition key is named "partition_key" with type (S)
+  - Sort key is named "sort_key" with type (S)
+"""
+from typing import List
+
+import boto3
+from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
+from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
+from aws_cryptographic_material_providers.mpl.models import (
+    CreateAwsKmsMrkDiscoveryMultiKeyringInput,
+    CreateAwsKmsMrkMultiKeyringInput,
+    DiscoveryFilter,
+)
+from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    DynamoDbTableEncryptionConfig,
+    DynamoDbTablesEncryptionConfig,
+)
+from aws_dbesdk_dynamodb.structures.structured_encryption import (
+    CryptoAction,
+)
+
+
+def multi_mrk_discovery_keyring_get_item_put_item(
+    ddb_table_name: str, key_arn: str, account_ids: List[str], regions: List[str]
+):
+    """
+    Demonstrate using a MRK discovery multi-keyring.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param key_arn: The ARN of the KMS key to use for encryption
+    :param account_ids: List of AWS account IDs for discovery filter
+    :param regions: List of AWS regions for discovery keyring
+    """
+    # 1. Create a single MRK multi-keyring using the key arn.
+    #    Although this example demonstrates use of the MRK discovery multi-keyring,
+    #    a discovery keyring cannot be used to encrypt. So we will need to construct
+    #    a non-discovery keyring for this example to encrypt. For more information on MRK
+    #    multi-keyrings, see the MultiMrkKeyringExample in this directory.
+    #    Though this is an "MRK multi-keyring", we do not need to provide multiple keys,
+    #    and can use single-region KMS keys. We will provide a single key here; this
+    #    can be either an MRK or a single-region key.
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    encrypt_keyring = mat_prov.create_aws_kms_mrk_multi_keyring(
+        input=CreateAwsKmsMrkMultiKeyringInput(generator=key_arn)
+    )
+
+    # 2. Configure which attributes are encrypted and/or signed when writing new items.
+    #    For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    #    we must explicitly configure how they should be treated during item encryption:
+    #      - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #      - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #      - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    attribute_actions_on_encrypt = {
+        "partition_key": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "sort_key": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
+    }
+
+    # 3. Configure which attributes we expect to be included in the signature
+    #    when reading items. There are two options for configuring this:
+    #
+    #    - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
+    #      When defining your DynamoDb schema and deciding on attribute names,
+    #      choose a distinguishing prefix (such as ":") for all attributes that
+    #      you do not want to include in the signature.
+    #      This has two main benefits:
+    #      - It is easier to reason about the security and authenticity of data within your item
+    #        when all unauthenticated data is easily distinguishable by their attribute name.
+    #      - If you need to add new unauthenticated attributes in the future,
+    #        you can easily make the corresponding update to your `attribute_actions_on_encrypt`
+    #        and immediately start writing to that new attribute, without
+    #        any other configuration update needed.
+    #      Once you configure this field, it is not safe to update it.
+    #
+    #    - Configure `allowed_unsigned_attributes`: You may also explicitly list
+    #      a set of attributes that should be considered unauthenticated when encountered
+    #      on read. Be careful if you use this configuration. Do not remove an attribute
+    #      name from this configuration, even if you are no longer writing with that attribute,
+    #      as old items may still include this attribute, and our configuration needs to know
+    #      to continue to exclude this attribute from the signature scope.
+    #      If you add new attribute names to this field, you must first deploy the update to this
+    #      field to all readers in your host fleet before deploying the update to start writing
+    #      with that new attribute.
+    #
+    #   For this example, we currently authenticate all attributes. To make it easier to
+    #   add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
+    unsign_attr_prefix = ":"
+
+    # 4. Create the DynamoDb Encryption configuration for the table we will be writing to.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions_on_encrypt,
+        keyring=encrypt_keyring,
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # 5. Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # 6. Put an item into our table using the above client.
+    #    Before the item gets sent to DynamoDb, it will be encrypted
+    #    client-side using the MRK multi-keyring.
+    item = {
+        "partition_key": {"S": "awsKmsMrkDiscoveryMultiKeyringItem"},
+        "sort_key": {"N": "0"},
+        "sensitive_data": {"S": "encrypt and sign me!"},
+    }
+
+    put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
+
+    # Demonstrate that PutItem succeeded
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # 7. Construct a discovery filter.
+    #    A discovery filter limits the set of encrypted data keys
+    #    the keyring can use to decrypt data.
+    #    We will only let the keyring use keys in the selected AWS accounts
+    #    and in the `aws` partition.
+    #    This is the suggested config for most users; for more detailed config, see
+    #      https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-kms-keyring.html#kms-keyring-discovery
+    discovery_filter = DiscoveryFilter(partition="aws", account_ids=account_ids)
+
+    # 8. Construct a discovery keyring.
+    #    Note that we choose to use the MRK discovery multi-keyring, even though
+    #    our original keyring used a single KMS key.
+    decrypt_keyring = mat_prov.create_aws_kms_mrk_discovery_multi_keyring(
+        input=CreateAwsKmsMrkDiscoveryMultiKeyringInput(discovery_filter=discovery_filter, regions=regions)
+    )
+
+    # 9. Create new DDB config and client using the decrypt discovery keyring.
+    #    This is the same as the above config, except we pass in the decrypt keyring.
+    table_config_for_decrypt = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions_on_encrypt,
+        # Add decrypt keyring here
+        keyring=decrypt_keyring,
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+    )
+
+    table_configs_for_decrypt = {ddb_table_name: table_config_for_decrypt}
+    tables_config_for_decrypt = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs_for_decrypt)
+
+    encrypted_ddb_client_for_decrypt = EncryptedClient(client=ddb_client, encryption_config=tables_config_for_decrypt)
+
+    # 10. Get the item back from our table using the client.
+    #     The client will retrieve encrypted items from the DDB table, then
+    #     detect the KMS key that was used to encrypt their data keys.
+    #     The client will make a request to KMS to decrypt with the encrypting KMS key.
+    #     If the client has permission to decrypt with the KMS key,
+    #     the client will decrypt the item client-side using the keyring
+    #     and return the original item.
+    key_to_get = {"partition_key": {"S": "awsKmsMrkDiscoveryMultiKeyringItem"}, "sort_key": {"N": "0"}}
+
+    get_response = encrypted_ddb_client_for_decrypt.get_item(TableName=ddb_table_name, Key=key_to_get)
+
+    # Demonstrate that GetItem succeeded and returned the decrypted item
+    assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    returned_item = get_response["Item"]
+    assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example.py
new file mode 100644
index 000000000..dbc774763
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/mrk_multi_keyring_example.py
@@ -0,0 +1,245 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Example demonstrating DynamoDb Encryption using an MRK multi-keyring configuration.
+
+The MRK multi-keyring accepts multiple AWS KMS MRKs (multi-region keys) or regular
+AWS KMS keys (single-region keys) and uses them to encrypt and decrypt data. Data
+encrypted using an MRK multi-keyring can be decrypted using any of its component
+keys. If a component key is an MRK with a replica in a second region, the replica
+key can also be used to decrypt data.
+
+For more information on MRKs and multi-keyrings, see:
+- MRKs: https://docs.aws.amazon.com/kms/latest/developerguide/multi-region-keys-overview.html
+- Multi-keyrings: https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-multi-keyring.html
+
+The example creates a new MRK multi-keyring consisting of one MRK (labeled as the
+"generator keyring") and one single-region key (labeled as the only "child keyring").
+The MRK also has a replica in a second region.
+
+The example encrypts a test item using the MRK multi-keyring and puts the encrypted
+item to the provided DynamoDb table. Then, it gets the item from the table and
+decrypts it using three different configs:
+  1. The MRK multi-keyring, where the MRK key is used to decrypt
+  2. Another MRK multi-keyring, where the replica MRK key is used to decrypt
+  3. Another MRK multi-keyring, where the single-region key that was present
+     in the original MRK multi-keyring is used to decrypt
+
+Running this example requires access to the DDB Table whose name is provided in
+CLI arguments. This table must be configured with the following primary key
+configuration:
+  - Partition key is named "partition_key" with type (S)
+  - Sort key is named "sort_key" with type (S)
+
+Since this example demonstrates multi-region use cases, it requires a default
+region set in your AWS client. You can set a default region through the AWS CLI:
+  aws configure set region [region-name]
+For example:
+  aws configure set region us-west-2
+
+For more information on using AWS CLI to set config, see:
+https://awscli.amazonaws.com/v2/documentation/api/latest/reference/configure/set.html
+"""
+import boto3
+from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
+from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
+from aws_cryptographic_material_providers.mpl.models import (
+    CreateAwsKmsMrkMultiKeyringInput,
+)
+from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    DynamoDbTableEncryptionConfig,
+    DynamoDbTablesEncryptionConfig,
+)
+from aws_dbesdk_dynamodb.structures.structured_encryption import (
+    CryptoAction,
+)
+
+
+def multi_mrk_keyring_get_item_put_item(ddb_table_name: str, mrk_key_arn: str, key_arn: str, mrk_replica_key_arn: str):
+    """
+    Demonstrate using a MRK multi-keyring.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param mrk_key_arn: The ARN of the MRK key to use as generator
+    :param key_arn: The ARN of the single-region key to use as child
+    :param mrk_replica_key_arn: The ARN of the MRK replica key
+    """
+    # 1. Create a single MRK multi-keyring using the MRK arn and the single-region key arn.
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    # Create the multi-keyring, using the MRK as the generator key,
+    #   and the single-region key as a child key.
+    # Note that the generator key will generate and encrypt a plaintext data key
+    #   and all child keys will only encrypt that same plaintext data key.
+    # As such, you must have permission to call KMS:GenerateDataKey on your generator key
+    #   and permission to call KMS:Encrypt on all child keys.
+    # For more information, see the AWS docs on multi-keyrings above.
+    aws_kms_mrk_multi_keyring = mat_prov.create_aws_kms_mrk_multi_keyring(
+        input=CreateAwsKmsMrkMultiKeyringInput(generator=mrk_key_arn, kms_key_ids=[key_arn])
+    )
+
+    # 2. Configure which attributes are encrypted and/or signed when writing new items.
+    #    For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    #    we must explicitly configure how they should be treated during item encryption:
+    #      - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #      - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #      - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    attribute_actions_on_encrypt = {
+        "partition_key": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "sort_key": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
+    }
+
+    # 3. Configure which attributes we expect to be included in the signature
+    #    when reading items. There are two options for configuring this:
+    #
+    #    - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
+    #      When defining your DynamoDb schema and deciding on attribute names,
+    #      choose a distinguishing prefix (such as ":") for all attributes that
+    #      you do not want to include in the signature.
+    #      This has two main benefits:
+    #      - It is easier to reason about the security and authenticity of data within your item
+    #        when all unauthenticated data is easily distinguishable by their attribute name.
+    #      - If you need to add new unauthenticated attributes in the future,
+    #        you can easily make the corresponding update to your `attribute_actions_on_encrypt`
+    #        and immediately start writing to that new attribute, without
+    #        any other configuration update needed.
+    #      Once you configure this field, it is not safe to update it.
+    #
+    #    - Configure `allowed_unsigned_attributes`: You may also explicitly list
+    #      a set of attributes that should be considered unauthenticated when encountered
+    #      on read. Be careful if you use this configuration. Do not remove an attribute
+    #      name from this configuration, even if you are no longer writing with that attribute,
+    #      as old items may still include this attribute, and our configuration needs to know
+    #      to continue to exclude this attribute from the signature scope.
+    #      If you add new attribute names to this field, you must first deploy the update to this
+    #      field to all readers in your host fleet before deploying the update to start writing
+    #      with that new attribute.
+    #
+    #   For this example, we currently authenticate all attributes. To make it easier to
+    #   add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
+    unsign_attr_prefix = ":"
+
+    # 4. Create the DynamoDb Encryption configuration for the table we will be writing to.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions_on_encrypt,
+        keyring=aws_kms_mrk_multi_keyring,
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # 5. Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # 6. Put an item into our table using the above client.
+    #    Before the item gets sent to DynamoDb, it will be encrypted
+    #    client-side using the MRK multi-keyring.
+    #    The data key protecting this item will be encrypted
+    #    with all the KMS Keys in this keyring, so that it can be
+    #    decrypted with any one of those KMS Keys.
+    item = {
+        "partition_key": {"S": "awsKmsMrkMultiKeyringItem"},
+        "sort_key": {"N": "0"},
+        "sensitive_data": {"S": "encrypt and sign me!"},
+    }
+
+    put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
+
+    # Demonstrate that PutItem succeeded
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # 7. Get the item back from our table using the client.
+    #    The client will decrypt the item client-side using the MRK
+    #    and return back the original item.
+    #    Since the generator key is the first available key in the keyring,
+    #    that is the KMS Key that will be used to decrypt this item.
+    key_to_get = {"partition_key": {"S": "awsKmsMrkMultiKeyringItem"}, "sort_key": {"N": "0"}}
+
+    get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
+
+    # Demonstrate that GetItem succeeded and returned the decrypted item
+    assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    returned_item = get_response["Item"]
+    assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
+
+    # 8. Create a MRK keyring using the replica MRK arn.
+    #    We will use this to demonstrate that the replica MRK
+    #    can decrypt data created with the original MRK,
+    #    even when the replica MRK was not present in the
+    #    encrypting multi-keyring.
+    only_replica_key_mrk_multi_keyring = mat_prov.create_aws_kms_mrk_multi_keyring(
+        input=CreateAwsKmsMrkMultiKeyringInput(kms_key_ids=[mrk_replica_key_arn])
+    )
+
+    # 9. Create a new config and client using the MRK keyring.
+    #    This is the same setup as above, except we provide the MRK keyring to the config.
+    only_replica_key_table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions_on_encrypt,
+        keyring=only_replica_key_mrk_multi_keyring,  # Only replica keyring added here
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+    )
+
+    only_replica_key_table_configs = {ddb_table_name: only_replica_key_table_config}
+    only_replica_key_tables_config = DynamoDbTablesEncryptionConfig(
+        table_encryption_configs=only_replica_key_table_configs
+    )
+
+    only_replica_key_encrypted_ddb_client = EncryptedClient(
+        client=ddb_client, encryption_config=only_replica_key_tables_config
+    )
+
+    # 10. Get the item back from our table using the client configured with the replica.
+    #     The client will decrypt the item client-side using the replica MRK
+    #     and return back the original item.
+    only_replica_key_get_response = only_replica_key_encrypted_ddb_client.get_item(
+        TableName=ddb_table_name, Key=key_to_get
+    )
+
+    # Demonstrate that GetItem succeeded and returned the decrypted item
+    assert only_replica_key_get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    only_replica_key_returned_item = only_replica_key_get_response["Item"]
+    assert only_replica_key_returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
+
+    # 11. Create an AWS KMS keyring using the single-region key ARN.
+    #     We will use this to demonstrate that the single-region key
+    #     can decrypt data created with the MRK multi-keyring,
+    #     since it is present in the keyring used to encrypt.
+    only_srk_keyring = mat_prov.create_aws_kms_mrk_multi_keyring(
+        input=CreateAwsKmsMrkMultiKeyringInput(kms_key_ids=[key_arn])
+    )
+
+    # 12. Create a new config and client using the AWS KMS keyring.
+    #     This is the same setup as above, except we provide the AWS KMS keyring to the config.
+    only_srk_table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions_on_encrypt,
+        keyring=only_srk_keyring,  # Only single-region key keyring added here
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+    )
+
+    only_srk_table_configs = {ddb_table_name: only_srk_table_config}
+    only_srk_tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=only_srk_table_configs)
+
+    only_srk_encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=only_srk_tables_config)
+
+    # 13. Get the item back from our table using the client configured with the AWS KMS keyring.
+    #     The client will decrypt the item client-side using the single-region key
+    #     and return back the original item.
+    only_srk_get_response = only_srk_encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
+
+    # Demonstrate that GetItem succeeded and returned the decrypted item
+    assert only_srk_get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    only_srk_returned_item = only_srk_get_response["Item"]
+    assert only_srk_returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example.py
new file mode 100644
index 000000000..5cb67df61
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/multi_keyring_example.py
@@ -0,0 +1,211 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Example demonstrating DynamoDb Encryption using a multi-keyring configuration.
+
+A multi-keyring accepts multiple keyrings and uses them to encrypt and decrypt data.
+Data encrypted with a multi-keyring can be decrypted with any of its component keyrings.
+
+For more information on multi-keyrings, see:
+https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-multi-keyring.html
+
+The example creates a multi-keyring consisting of an AWS KMS keyring (labeled the
+"generator keyring") and a raw AES keyring (labeled as the only "child keyring").
+It encrypts a test item using the multi-keyring and puts the encrypted item to the
+provided DynamoDb table. Then, it gets the item from the table and decrypts it
+using only the raw AES keyring.
+
+The example takes an `aes_key_bytes` parameter representing a 256-bit AES key.
+If run through the class's main method, it will create a new key. In practice,
+users should not randomly generate a key, but instead retrieve an existing key
+from a secure key management system (e.g. an HSM).
+
+Running this example requires access to the DDB Table whose name is provided in
+CLI arguments. This table must be configured with the following primary key
+configuration:
+  - Partition key is named "partition_key" with type (S)
+  - Sort key is named "sort_key" with type (S)
+"""
+import boto3
+from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
+from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
+from aws_cryptographic_material_providers.mpl.models import (
+    AesWrappingAlg,
+    CreateAwsKmsMrkMultiKeyringInput,
+    CreateMultiKeyringInput,
+    CreateRawAesKeyringInput,
+)
+from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    DynamoDbTableEncryptionConfig,
+    DynamoDbTablesEncryptionConfig,
+)
+from aws_dbesdk_dynamodb.structures.structured_encryption import (
+    CryptoAction,
+)
+
+
+def multi_keyring_get_item_put_item(ddb_table_name: str, key_arn: str, aes_key_bytes: bytes):
+    """
+    Demonstrate using a multi-keyring.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param key_arn: The ARN of the KMS key to use
+    :param aes_key_bytes: The AES key bytes to use
+    """
+    # 1. Create the raw AES keyring.
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    raw_aes_keyring_input = CreateRawAesKeyringInput(
+        key_name="my-aes-key-name",
+        key_namespace="my-key-namespace",
+        wrapping_key=aes_key_bytes,
+        wrapping_alg=AesWrappingAlg.ALG_AES256_GCM_IV12_TAG16,
+    )
+
+    raw_aes_keyring = mat_prov.create_raw_aes_keyring(input=raw_aes_keyring_input)
+
+    # 2. Create the AWS KMS keyring.
+    #    We create a MRK multi keyring, as this interface also supports
+    #    single-region KMS keys (standard KMS keys),
+    #    and creates the KMS client for us automatically.
+    aws_kms_mrk_multi_keyring = mat_prov.create_aws_kms_mrk_multi_keyring(
+        input=CreateAwsKmsMrkMultiKeyringInput(generator=key_arn)
+    )
+
+    # 3. Create the multi-keyring.
+    #    We will label the AWS KMS keyring as the generator and the raw AES keyring as the
+    #        only child keyring.
+    #    You must provide a generator keyring to encrypt data.
+    #    You may provide additional child keyrings. Each child keyring will be able to
+    #        decrypt data encrypted with the multi-keyring on its own. It does not need
+    #        knowledge of any other child keyrings or the generator keyring to decrypt.
+    multi_keyring = mat_prov.create_multi_keyring(
+        input=CreateMultiKeyringInput(generator=aws_kms_mrk_multi_keyring, child_keyrings=[raw_aes_keyring])
+    )
+
+    # 4. Configure which attributes are encrypted and/or signed when writing new items.
+    #    For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    #    we must explicitly configure how they should be treated during item encryption:
+    #      - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #      - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #      - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    attribute_actions = {
+        "partition_key": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "sort_key": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
+    }
+
+    # 5. Configure which attributes we expect to be included in the signature
+    #    when reading items. There are two options for configuring this:
+    #
+    #    - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
+    #      When defining your DynamoDb schema and deciding on attribute names,
+    #      choose a distinguishing prefix (such as ":") for all attributes that
+    #      you do not want to include in the signature.
+    #      This has two main benefits:
+    #      - It is easier to reason about the security and authenticity of data within your item
+    #        when all unauthenticated data is easily distinguishable by their attribute name.
+    #      - If you need to add new unauthenticated attributes in the future,
+    #        you can easily make the corresponding update to your `attribute_actions`
+    #        and immediately start writing to that new attribute, without
+    #        any other configuration update needed.
+    #      Once you configure this field, it is not safe to update it.
+    #
+    #    - Configure `allowed_unsigned_attributes`: You may also explicitly list
+    #      a set of attributes that should be considered unauthenticated when encountered
+    #      on read. Be careful if you use this configuration. Do not remove an attribute
+    #      name from this configuration, even if you are no longer writing with that attribute,
+    #      as old items may still include this attribute, and our configuration needs to know
+    #      to continue to exclude this attribute from the signature scope.
+    #      If you add new attribute names to this field, you must first deploy the update to this
+    #      field to all readers in your host fleet before deploying the update to start writing
+    #      with that new attribute.
+    #
+    #   For this example, we currently authenticate all attributes. To make it easier to
+    #   add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
+    unsign_attr_prefix = ":"
+
+    # 6. Create the DynamoDb Encryption configuration for the table we will be writing to.
+    #    Note that this example creates one config/client combination for PUT, and another
+    #        for GET. The PUT config uses the multi-keyring, while the GET config uses the
+    #        raw AES keyring. This is solely done to demonstrate that a keyring included as
+    #        a child of a multi-keyring can be used to decrypt data on its own.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=multi_keyring,  # Multi-keyring is added here
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # 7. Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # 8. Put an item into our table using the above client.
+    #    Before the item gets sent to DynamoDb, it will be encrypted
+    #    client-side using the multi-keyring.
+    #    The item will be encrypted with all wrapping keys in the keyring,
+    #    so that it can be decrypted with any one of the keys.
+    item = {
+        "partition_key": {"S": "multiKeyringItem"},
+        "sort_key": {"N": "0"},
+        "sensitive_data": {"S": "encrypt and sign me!"},
+    }
+
+    put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
+
+    # Demonstrate that PutItem succeeded
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # 9. Get the item back from our table using the above client.
+    #    The client will decrypt the item client-side using the AWS KMS
+    #    keyring, and return back the original item.
+    #    Since the generator key is the first available key in the keyring,
+    #    that is the key that will be used to decrypt this item.
+    key_to_get = {"partition_key": {"S": "multiKeyringItem"}, "sort_key": {"N": "0"}}
+
+    get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
+
+    # Demonstrate that GetItem succeeded and returned the decrypted item
+    assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    returned_item = get_response["Item"]
+    assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
+
+    # 10. Create a new config and client with only the raw AES keyring to GET the item
+    #     This is the same setup as above, except the config uses the `raw_aes_keyring`.
+    only_aes_keyring_table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=raw_aes_keyring,  # Raw AES keyring is added here
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+    )
+
+    only_aes_keyring_table_configs = {ddb_table_name: only_aes_keyring_table_config}
+    only_aes_keyring_tables_config = DynamoDbTablesEncryptionConfig(
+        table_encryption_configs=only_aes_keyring_table_configs
+    )
+
+    only_aes_keyring_encrypted_ddb_client = EncryptedClient(
+        client=ddb_client, encryption_config=only_aes_keyring_tables_config
+    )
+
+    # 11. Get the item back from our table using the client
+    #     configured with only the raw AES keyring.
+    #     The client will decrypt the item client-side using the raw
+    #     AES keyring, and return back the original item.
+    only_aes_keyring_get_response = only_aes_keyring_encrypted_ddb_client.get_item(
+        TableName=ddb_table_name, Key=key_to_get
+    )
+
+    # Demonstrate that GetItem succeeded and returned the decrypted item
+    assert only_aes_keyring_get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    only_aes_keyring_returned_item = only_aes_keyring_get_response["Item"]
+    assert only_aes_keyring_returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example.py
new file mode 100644
index 000000000..2b4993e26
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_aes_keyring_example.py
@@ -0,0 +1,145 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Example demonstrating DynamoDb Encryption using a raw AES Keyring.
+
+The raw AES Keyring takes in an AES key and uses that key to protect the data
+keys that encrypt and decrypt DynamoDb table items.
+
+This example takes an `aes_key_bytes` parameter representing a 256-bit AES key.
+If run through the script's main method, it will create a new key. In practice,
+users should not randomly generate a key, but instead retrieve an existing key
+from a secure key management system (e.g. an HSM).
+
+This example encrypts a test item using the provided AES key and puts the encrypted
+item to the provided DynamoDb table. Then, it gets the item from the table and
+decrypts it.
+
+Running this example requires access to the DDB Table whose name is provided in
+CLI arguments. This table must be configured with the following primary key
+configuration:
+  - Partition key is named "partition_key" with type (S)
+  - Sort key is named "sort_key" with type (S)
+"""
+
+import boto3
+from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
+from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
+from aws_cryptographic_material_providers.mpl.models import (
+    AesWrappingAlg,
+    CreateRawAesKeyringInput,
+)
+from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    DynamoDbTableEncryptionConfig,
+    DynamoDbTablesEncryptionConfig,
+)
+from aws_dbesdk_dynamodb.structures.structured_encryption import (
+    CryptoAction,
+)
+
+
+def raw_aes_keyring_get_item_put_item(ddb_table_name: str, aes_key_bytes: bytes):
+    """
+    Demonstrate using a raw AES keyring.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param aes_key_bytes: The AES key bytes to use
+    """
+    # 1. Create the keyring.
+    #    The DynamoDb encryption client uses this to encrypt and decrypt items.
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    keyring_input = CreateRawAesKeyringInput(
+        key_name="my-aes-key-name",
+        key_namespace="my-key-namespace",
+        wrapping_key=aes_key_bytes,
+        wrapping_alg=AesWrappingAlg.ALG_AES256_GCM_IV12_TAG16,
+    )
+
+    raw_aes_keyring = mat_prov.create_raw_aes_keyring(input=keyring_input)
+
+    # 2. Configure which attributes are encrypted and/or signed when writing new items.
+    #    For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    #    we must explicitly configure how they should be treated during item encryption:
+    #      - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #      - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #      - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    attribute_actions = {
+        "partition_key": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "sort_key": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
+    }
+
+    # 3. Configure which attributes we expect to be included in the signature
+    #    when reading items. There are two options for configuring this:
+    #
+    #    - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
+    #      When defining your DynamoDb schema and deciding on attribute names,
+    #      choose a distinguishing prefix (such as ":") for all attributes that
+    #      you do not want to include in the signature.
+    #      This has two main benefits:
+    #      - It is easier to reason about the security and authenticity of data within your item
+    #        when all unauthenticated data is easily distinguishable by their attribute name.
+    #      - If you need to add new unauthenticated attributes in the future,
+    #        you can easily make the corresponding update to your `attribute_actions`
+    #        and immediately start writing to that new attribute, without
+    #        any other configuration update needed.
+    #      Once you configure this field, it is not safe to update it.
+    #
+    #    - Configure `allowed_unsigned_attributes`: You may also explicitly list
+    #      a set of attributes that should be considered unauthenticated when encountered
+    #      on read. Be careful if you use this configuration. Do not remove an attribute
+    #      name from this configuration, even if you are no longer writing with that attribute,
+    #      as old items may still include this attribute, and our configuration needs to know
+    #      to continue to exclude this attribute from the signature scope.
+    #      If you add new attribute names to this field, you must first deploy the update to this
+    #      field to all readers in your host fleet before deploying the update to start writing
+    #      with that new attribute.
+    #
+    #   For this example, we currently authenticate all attributes. To make it easier to
+    #   add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
+    unsign_attr_prefix = ":"
+
+    # 4. Create the DynamoDb Encryption configuration for the table we will be writing to.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=raw_aes_keyring,
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # 5. Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # 6. Put an item into our table using the above client.
+    #    Before the item gets sent to DynamoDb, it will be encrypted
+    #    client-side, according to our configuration.
+    item = {
+        "partition_key": {"S": "rawAesKeyringItem"},
+        "sort_key": {"N": "0"},
+        "sensitive_data": {"S": "encrypt and sign me!"},
+    }
+
+    put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
+
+    # Demonstrate that PutItem succeeded
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # 7. Get the item back from our table using the same client.
+    #    The client will decrypt the item client-side, and return
+    #    back the original item.
+    key_to_get = {"partition_key": {"S": "rawAesKeyringItem"}, "sort_key": {"N": "0"}}
+
+    get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
+
+    # Demonstrate that GetItem succeeded and returned the decrypted item
+    assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    returned_item = get_response["Item"]
+    assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example.py
new file mode 100644
index 000000000..54f79a46d
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_ecdh_keyring_example.py
@@ -0,0 +1,564 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+These examples set up DynamoDb Encryption for the AWS SDK client using the raw ECDH Keyring.
+
+This keyring, depending on its KeyAgreement scheme,
+takes in the sender's ECC private key, and the recipient's ECC Public Key to derive a shared secret.
+The keyring uses the shared secret to derive a data key to protect the
+data keys that encrypt and decrypt DynamoDb table items.
+
+Running these examples require access to the DDB Table whose name
+is provided in CLI arguments.
+This table must be configured with the following
+primary key configuration:
+  - Partition key is named "partition_key" with type (S)
+  - Sort key is named "sort_key" with type (S)
+"""
+import pathlib
+
+import boto3
+from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
+from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
+from aws_cryptographic_material_providers.mpl.models import (
+    CreateRawEcdhKeyringInput,
+    EphemeralPrivateKeyToStaticPublicKeyInput,
+    PublicKeyDiscoveryInput,
+    RawEcdhStaticConfigurationsEphemeralPrivateKeyToStaticPublicKey,
+    RawEcdhStaticConfigurationsPublicKeyDiscovery,
+    RawEcdhStaticConfigurationsRawPrivateKeyToStaticPublicKey,
+    RawPrivateKeyToStaticPublicKeyInput,
+)
+from aws_cryptographic_material_providers.mpl.references import IKeyring
+from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    DynamoDbTableEncryptionConfig,
+    DynamoDbTablesEncryptionConfig,
+)
+from aws_dbesdk_dynamodb.structures.structured_encryption import (
+    CryptoAction,
+)
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import ec
+
+EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER = "RawEcdhKeyringExamplePrivateKeySender.pem"
+EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT = "RawEcdhKeyringExamplePrivateKeyRecipient.pem"
+EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT = "RawEcdhKeyringExamplePublicKeyRecipient.pem"
+
+
+def raw_ecdh_keyring_get_item_put_item(ddb_table_name: str, curve_spec: str):
+    """
+    Demonstrate using a raw ECDH keyring with static keys.
+
+    This example takes in the sender's private key as a
+    UTF8 PEM-encoded (PKCS #8 PrivateKeyInfo structures)
+    located at the file location defined in EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER,
+    the recipient's public key as a UTF8 PEM-encoded X.509 public key, also known as SubjectPublicKeyInfo (SPKI),
+    located at the file location defined in EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT,
+    and the Curve Specification where the keys lie.
+
+    This example encrypts a test item using the provided ECC keys and puts the
+    encrypted item to the provided DynamoDb table. Then, it gets the
+    item from the table and decrypts it.
+
+    This examples creates a RawECDH keyring with the RawPrivateKeyToStaticPublicKey key agreement scheme.
+    For more information on this configuration see:
+    https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-raw-ecdh-keyring.html#raw-ecdh-RawPrivateKeyToStaticPublicKey
+
+    On encrypt, the shared secret is derived from the sender's private key and the recipient's public key.
+    On decrypt, the shared secret is derived from the sender's private key and the recipient's public key;
+    however, on decrypt the recipient can construct a keyring such that the shared secret is calculated with
+    the recipient's private key and the sender's public key. In both scenarios the shared secret will be the same.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param curve_spec: The curve specification to use
+    """
+    # Load key pair from UTF-8 encoded PEM files.
+    # You may provide your own PEM files to use here. If you provide this, it MUST
+    # be a key on curve P256.
+    # If you do not, the main method in this class will generate PEM
+    # files for example use. Do not use these files for any other purpose.
+    try:
+        with open(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER, "rb") as f:
+            private_key_utf8_encoded = f.read()
+    except IOError as e:
+        raise OSError("IOError while reading the private key from file") from e
+
+    try:
+        with open(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT, "rb") as f:
+            public_key_utf8_encoded = f.read()
+            public_key = serialization.load_pem_public_key(public_key_utf8_encoded)
+            public_key_bytes = public_key.public_bytes(
+                encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo
+            )
+    except IOError as e:
+        raise OSError("IOError while reading the public key from file") from e
+
+    # Create the keyring.
+    # This keyring uses static sender and recipient keys. This configuration calls for both of
+    # the keys to be on the same curve (P256, P384, P521).
+    # On encrypt, the shared secret is derived from the sender's private key and the recipient's public key.
+    # For this example, on decrypt, the shared secret is derived from the sender's private key
+    # and the recipient's public key;
+    # however, on decrypt the recipient can construct a keyring such that the shared secret is calculated with
+    # the recipient's private key and the sender's public key. In both scenarios the shared secret will be the same.
+    # The DynamoDb encryption client uses this to encrypt and decrypt items.
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    keyring_input = CreateRawEcdhKeyringInput(
+        curve_spec=curve_spec,
+        key_agreement_scheme=RawEcdhStaticConfigurationsRawPrivateKeyToStaticPublicKey(
+            RawPrivateKeyToStaticPublicKeyInput(
+                # Must be a UTF8 PEM-encoded private key
+                sender_static_private_key=private_key_utf8_encoded,
+                # Must be a DER-encoded X.509 public key
+                recipient_public_key=public_key_bytes,
+            )
+        ),
+    )
+
+    raw_ecdh_keyring = mat_prov.create_raw_ecdh_keyring(input=keyring_input)
+
+    put_get_example_with_keyring(raw_ecdh_keyring, ddb_table_name)
+
+
+def ephemeral_raw_ecdh_keyring_put_item(ddb_table_name: str, curve_spec: str):
+    """
+    Demonstrate using a raw ECDH keyring with ephemeral keys.
+
+    This example takes in the recipient's public key located at EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT
+    as a UTF8 PEM-encoded X.509 public key, and the Curve Specification where the key lies.
+
+    This examples creates a RawECDH keyring with the EphemeralPrivateKeyToStaticPublicKey key agreement scheme.
+    This configuration will always create a new key pair as the sender key pair for the key agreement operation.
+    The ephemeral configuration can only encrypt data and CANNOT decrypt messages.
+    For more information on this configuration see:
+    https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-raw-ecdh-keyring.html#raw-ecdh-EphemeralPrivateKeyToStaticPublicKey
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param curve_spec: The curve specification to use
+    """
+    # Load public key from UTF-8 encoded PEM files into a DER encoded public key.
+    # You may provide your own PEM files to use here. If you provide this, it MUST
+    # be a key on curve P256.
+    # If you do not, the main method in this class will generate PEM
+    # files for example use. Do not use these files for any other purpose.
+    try:
+        with open(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT, "rb") as f:
+            public_key_utf8_encoded = f.read()
+            public_key = serialization.load_pem_public_key(public_key_utf8_encoded)
+            public_key_bytes = public_key.public_bytes(
+                encoding=serialization.Encoding.DER, format=serialization.PublicFormat.SubjectPublicKeyInfo
+            )
+    except IOError as e:
+        raise OSError("IOError while reading the public key from file") from e
+
+    # Create the keyring.
+    # This keyring uses an ephemeral configuration. This configuration will always create a new
+    # key pair as the sender key pair for the key agreement operation. The ephemeral configuration can only
+    # encrypt data and CANNOT decrypt messages.
+    # The DynamoDb encryption client uses this to encrypt items.
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    keyring_input = CreateRawEcdhKeyringInput(
+        curve_spec=curve_spec,
+        key_agreement_scheme=RawEcdhStaticConfigurationsEphemeralPrivateKeyToStaticPublicKey(
+            EphemeralPrivateKeyToStaticPublicKeyInput(recipient_public_key=public_key_bytes)
+        ),
+    )
+
+    raw_ecdh_keyring = mat_prov.create_raw_ecdh_keyring(input=keyring_input)
+
+    # A raw ecdh keyring with Ephemeral configuration cannot decrypt data since the key pair
+    # used as the sender is ephemeral. This means that at decrypt time it does not have
+    # the private key that corresponds to the public key that is stored on the message.
+    put_example_with_keyring(raw_ecdh_keyring, ddb_table_name)
+
+
+def discovery_raw_ecdh_keyring_get_item(ddb_table_name: str, curve_spec: str):
+    """
+    Demonstrate using a raw ECDH keyring with discovery.
+
+    This example takes in the recipient's private key located at EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT
+    as a UTF8 PEM-encoded (PKCS #8 PrivateKeyInfo structures) private key,
+    and the Curve Specification where the key lies.
+
+    This examples creates a RawECDH keyring with the PublicKeyDiscovery key agreement scheme.
+    This scheme is only available on decrypt.
+    For more information on this configuration see:
+    https://docs.aws.amazon.com/encryption-sdk/latest/developer-guide/use-raw-ecdh-keyring.html#raw-ecdh-PublicKeyDiscovery
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param curve_spec: The curve specification to use
+    """
+    # Load key pair from UTF-8 encoded PEM files.
+    # You may provide your own PEM files to use here. If you provide this, it MUST
+    # be a key on curve P256.
+    # If you do not, the main method in this class will generate PEM
+    # files for example use. Do not use these files for any other purpose.
+    try:
+        with open(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT, "rb") as f:
+            private_key_utf8_encoded = f.read()
+    except IOError as e:
+        raise OSError("IOError while reading the private key from file") from e
+
+    # Create the keyring.
+    # This keyring uses a discovery configuration. This configuration will check on decrypt
+    # if it is meant to decrypt the message by checking if the configured public key is stored on the message.
+    # The discovery configuration can only decrypt messages and CANNOT encrypt messages.
+    # The DynamoDb encryption client uses this to decrypt items.
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    keyring_input = CreateRawEcdhKeyringInput(
+        curve_spec=curve_spec,
+        key_agreement_scheme=RawEcdhStaticConfigurationsPublicKeyDiscovery(
+            PublicKeyDiscoveryInput(recipient_static_private_key=private_key_utf8_encoded)
+        ),
+    )
+
+    raw_ecdh_keyring = mat_prov.create_raw_ecdh_keyring(input=keyring_input)
+
+    # A raw ecdh keyring with discovery configuration cannot encrypt data since the keyring
+    # looks for its configured public key on the message.
+    get_example_with_keyring(raw_ecdh_keyring, ddb_table_name)
+
+
+def put_get_example_with_keyring(raw_ecdh_keyring: IKeyring, ddb_table_name: str):
+    """
+    Demonstrate put and get operations with a raw ECDH keyring.
+
+    :param raw_ecdh_keyring: The raw ECDH keyring to use
+    :param ddb_table_name: The name of the DynamoDB table
+    """
+    # Configure which attributes are encrypted and/or signed when writing new items.
+    # For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    # we must explicitly configure how they should be treated during item encryption:
+    #   - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #   - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #   - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    attribute_actions = {
+        "partition_key": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "sort_key": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
+    }
+
+    # Configure which attributes we expect to be included in the signature
+    # when reading items. There are two options for configuring this:
+    #
+    # - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
+    #   When defining your DynamoDb schema and deciding on attribute names,
+    #   choose a distinguishing prefix (such as ":") for all attributes that
+    #   you do not want to include in the signature.
+    #   This has two main benefits:
+    #   - It is easier to reason about the security and authenticity of data within your item
+    #     when all unauthenticated data is easily distinguishable by their attribute name.
+    #   - If you need to add new unauthenticated attributes in the future,
+    #     you can easily make the corresponding update to your `attribute_actions`
+    #     and immediately start writing to that new attribute, without
+    #     any other configuration update needed.
+    #   Once you configure this field, it is not safe to update it.
+    #
+    # - Configure `allowed_unsigned_attributes`: You may also explicitly list
+    #   a set of attributes that should be considered unauthenticated when encountered
+    #   on read. Be careful if you use this configuration. Do not remove an attribute
+    #   name from this configuration, even if you are no longer writing with that attribute,
+    #   as old items may still include this attribute, and our configuration needs to know
+    #   to continue to exclude this attribute from the signature scope.
+    #   If you add new attribute names to this field, you must first deploy the update to this
+    #   field to all readers in your host fleet before deploying the update to start writing
+    #   with that new attribute.
+    #
+    # For this example, we currently authenticate all attributes. To make it easier to
+    # add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
+    unsign_attr_prefix = ":"
+
+    # Create the DynamoDb Encryption configuration for the table we will be writing to.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=raw_ecdh_keyring,
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # Put an item into our table using the above client.
+    # Before the item gets sent to DynamoDb, it will be encrypted
+    # client-side, according to our configuration.
+    item = {
+        "partition_key": {"S": "rawEcdhKeyringItem"},
+        "sort_key": {"N": "0"},
+        "sensitive_data": {"S": "encrypt and sign me!"},
+    }
+
+    put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
+
+    # Demonstrate that PutItem succeeded
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # Get the item back from our table using the client.
+    # The client will decrypt the item client-side using the RSA keyring
+    # and return the original item.
+    key_to_get = {"partition_key": {"S": "rawEcdhKeyringItem"}, "sort_key": {"N": "0"}}
+
+    get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
+
+    # Demonstrate that GetItem succeeded and returned the decrypted item
+    assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    returned_item = get_response["Item"]
+    assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
+
+
+def put_example_with_keyring(raw_ecdh_keyring: IKeyring, ddb_table_name: str):
+    """
+    Demonstrate put operation with a raw ECDH keyring.
+
+    :param raw_ecdh_keyring: The raw ECDH keyring to use
+    :param ddb_table_name: The name of the DynamoDB table
+    """
+    # Configure which attributes are encrypted and/or signed when writing new items.
+    # For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    # we must explicitly configure how they should be treated during item encryption:
+    #   - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #   - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #   - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    attribute_actions = {
+        "partition_key": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "sort_key": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
+    }
+
+    # Configure which attributes we expect to be included in the signature
+    # when reading items. There are two options for configuring this:
+    #
+    # - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
+    #   When defining your DynamoDb schema and deciding on attribute names,
+    #   choose a distinguishing prefix (such as ":") for all attributes that
+    #   you do not want to include in the signature.
+    #   This has two main benefits:
+    #   - It is easier to reason about the security and authenticity of data within your item
+    #     when all unauthenticated data is easily distinguishable by their attribute name.
+    #   - If you need to add new unauthenticated attributes in the future,
+    #     you can easily make the corresponding update to your `attribute_actions`
+    #     and immediately start writing to that new attribute, without
+    #     any other configuration update needed.
+    #   Once you configure this field, it is not safe to update it.
+    #
+    # - Configure `allowed_unsigned_attributes`: You may also explicitly list
+    #   a set of attributes that should be considered unauthenticated when encountered
+    #   on read. Be careful if you use this configuration. Do not remove an attribute
+    #   name from this configuration, even if you are no longer writing with that attribute,
+    #   as old items may still include this attribute, and our configuration needs to know
+    #   to continue to exclude this attribute from the signature scope.
+    #   If you add new attribute names to this field, you must first deploy the update to this
+    #   field to all readers in your host fleet before deploying the update to start writing
+    #   with that new attribute.
+    #
+    # For this example, we currently authenticate all attributes. To make it easier to
+    # add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
+    unsign_attr_prefix = ":"
+
+    # Create the DynamoDb Encryption configuration for the table we will be writing to.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=raw_ecdh_keyring,
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # Put an item into our table using the above client.
+    # Before the item gets sent to DynamoDb, it will be encrypted
+    # client-side, according to our configuration.
+    item = {
+        "partition_key": {"S": "rawEcdhKeyringItem"},
+        "sort_key": {"N": "0"},
+        "sensitive_data": {"S": "encrypt and sign me!"},
+    }
+
+    put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
+
+    # Demonstrate that PutItem succeeded
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+
+def get_example_with_keyring(raw_ecdh_keyring: IKeyring, ddb_table_name: str):
+    """
+    Demonstrate get operation with a raw ECDH keyring.
+
+    :param raw_ecdh_keyring: The raw ECDH keyring to use
+    :param ddb_table_name: The name of the DynamoDB table
+    """
+    # Configure which attributes are encrypted and/or signed when writing new items.
+    # For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    # we must explicitly configure how they should be treated during item encryption:
+    #   - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #   - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #   - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    attribute_actions = {
+        "partition_key": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "sort_key": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
+    }
+
+    # Configure which attributes we expect to be included in the signature
+    # when reading items. There are two options for configuring this:
+    #
+    # - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
+    #   When defining your DynamoDb schema and deciding on attribute names,
+    #   choose a distinguishing prefix (such as ":") for all attributes that
+    #   you do not want to include in the signature.
+    #   This has two main benefits:
+    #   - It is easier to reason about the security and authenticity of data within your item
+    #     when all unauthenticated data is easily distinguishable by their attribute name.
+    #   - If you need to add new unauthenticated attributes in the future,
+    #     you can easily make the corresponding update to your `attribute_actions`
+    #     and immediately start writing to that new attribute, without
+    #     any other configuration update needed.
+    #   Once you configure this field, it is not safe to update it.
+    #
+    # - Configure `allowed_unsigned_attributes`: You may also explicitly list
+    #   a set of attributes that should be considered unauthenticated when encountered
+    #   on read. Be careful if you use this configuration. Do not remove an attribute
+    #   name from this configuration, even if you are no longer writing with that attribute,
+    #   as old items may still include this attribute, and our configuration needs to know
+    #   to continue to exclude this attribute from the signature scope.
+    #   If you add new attribute names to this field, you must first deploy the update to this
+    #   field to all readers in your host fleet before deploying the update to start writing
+    #   with that new attribute.
+    #
+    # For this example, we currently authenticate all attributes. To make it easier to
+    # add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
+    unsign_attr_prefix = ":"
+
+    # Create the DynamoDb Encryption configuration for the table we will be writing to.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=raw_ecdh_keyring,
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # Get the item back from our table using the client.
+    # The client will decrypt the item client-side using the RSA keyring
+    # and return the original item.
+    key_to_get = {"partition_key": {"S": "rawEcdhKeyringItem"}, "sort_key": {"N": "0"}}
+
+    get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
+
+    # Demonstrate that GetItem succeeded and returned the decrypted item
+    assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    returned_item = get_response["Item"]
+    assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
+
+
+def should_generate_new_ecc_key_pairs() -> bool:
+    """
+    Check if new ECC key pairs should be generated.
+
+    :return: True if new key pairs should be generated, False otherwise
+    """
+    private_key_file_sender = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER)
+    private_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT)
+    public_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT)
+
+    # If keys already exist: do not overwrite existing keys
+    return (
+        not private_key_file_sender.exists()
+        and not public_key_file_recipient.exists()
+        and not private_key_file_recipient.exists()
+    )
+
+
+def generate_ecc_key_pairs():
+    """
+    Generate new ECC key pairs.
+
+    This code will generate new ECC key pairs for example use.
+    The keys will be written to the files:
+     - public_sender: EXAMPLE_ECC_PUBLIC_KEY_FILENAME_SENDER
+     - private_sender: EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER
+     - public_recipient: EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT
+    This example uses cryptography's EllipticCurve to generate the key pairs.
+    In practice, you should not generate this in your code, and should instead
+    retrieve this key from a secure key management system (e.g. HSM).
+    These examples only demonstrate using the P256 curve while the keyring accepts
+    P256, P384, or P521.
+    These keys are created here for example purposes only.
+    """
+    private_key_file_sender = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER)
+    private_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT)
+    public_key_file_recipient = pathlib.Path(EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT)
+
+    if private_key_file_sender.exists() or public_key_file_recipient.exists() or private_key_file_recipient.exists():
+        raise FileExistsError("generateEccKeyPairs will not overwrite existing PEM files")
+
+    # Generate sender key pair
+    sender_private_key = ec.generate_private_key(ec.SECP256R1())
+
+    # Generate recipient key pair
+    recipient_private_key = ec.generate_private_key(ec.SECP256R1())
+    recipient_public_key = recipient_private_key.public_key()
+
+    # Write private keys
+    write_private_key(sender_private_key, EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER)
+    write_private_key(recipient_private_key, EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT)
+
+    # Write public key
+    write_public_key(recipient_public_key, EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT)
+
+
+def write_private_key(private_key: ec.EllipticCurvePrivateKey, filename: str):
+    """
+    Write a private key to a PEM file.
+
+    :param private_key: The private key to write
+    :param filename: The filename to write to
+    """
+    pem_data = private_key.private_bytes(
+        encoding=serialization.Encoding.PEM,
+        format=serialization.PrivateFormat.PKCS8,
+        encryption_algorithm=serialization.NoEncryption(),
+    )
+
+    with open(filename, "wb") as f:
+        f.write(pem_data)
+
+
+def write_public_key(public_key: ec.EllipticCurvePublicKey, filename: str):
+    """
+    Write a public key to a PEM file.
+
+    :param public_key: The public key to write
+    :param filename: The filename to write to
+    """
+    pem_data = public_key.public_bytes(
+        encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo
+    )
+
+    with open(filename, "wb") as f:
+        f.write(pem_data)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example.py
new file mode 100644
index 000000000..f0de43eee
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/raw_rsa_keyring_example.py
@@ -0,0 +1,245 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Example demonstrating DynamoDb Encryption using a raw RSA Keyring.
+
+The raw RSA Keyring uses an RSA key pair to encrypt and decrypt records.
+The keyring accepts PEM encodings of the key pair as UTF-8 interpreted bytes.
+The client uses the public key to encrypt items it adds to the table and
+uses the private key to decrypt existing table items it retrieves.
+
+The example loads a key pair from PEM files with paths defined in:
+ - EXAMPLE_RSA_PRIVATE_KEY_FILENAME
+ - EXAMPLE_RSA_PUBLIC_KEY_FILENAME
+
+If you do not provide these files, running this example through the main method
+will generate these files for you in the directory where the example is run.
+In practice, users of this library should not generate new key pairs like this,
+and should instead retrieve an existing key from a secure key management system
+(e.g. an HSM).
+
+You may also provide your own key pair by placing PEM files in the directory
+where the example is run or modifying the paths in the code below. These files
+must be valid PEM encodings of the key pair as UTF-8 encoded bytes. If you do
+provide your own key pair, or if a key pair already exists, this class' main
+method will not generate a new key pair.
+
+The example loads a key pair from disk, encrypts a test item, and puts the
+encrypted item to the provided DynamoDb table. Then, it gets the item from
+the table and decrypts it.
+
+Running this example requires access to the DDB Table whose name is provided
+in CLI arguments. This table must be configured with the following primary
+key configuration:
+  - Partition key is named "partition_key" with type (S)
+  - Sort key is named "sort_key" with type (S)
+"""
+import os
+
+import boto3
+from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
+from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
+from aws_cryptographic_material_providers.mpl.models import (
+    CreateRawRsaKeyringInput,
+    PaddingScheme,
+)
+from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    DynamoDbTableEncryptionConfig,
+    DynamoDbTablesEncryptionConfig,
+)
+from aws_dbesdk_dynamodb.structures.structured_encryption import (
+    CryptoAction,
+)
+from cryptography.hazmat.primitives import serialization
+from cryptography.hazmat.primitives.asymmetric import rsa
+
+EXAMPLE_RSA_PRIVATE_KEY_FILENAME = "RawRsaKeyringExamplePrivateKey.pem"
+EXAMPLE_RSA_PUBLIC_KEY_FILENAME = "RawRsaKeyringExamplePublicKey.pem"
+
+
+def raw_rsa_keyring_example(ddb_table_name: str):
+    """
+    Create a Raw RSA keyring and use it to encrypt/decrypt DynamoDB items.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    """
+    # 1. Load key pair from UTF-8 encoded PEM files.
+    #    You may provide your own PEM files to use here.
+    #    If you do not, the main method in this class will generate PEM
+    #    files for example use. Do not use these files for any other purpose.
+    try:
+        with open(EXAMPLE_RSA_PUBLIC_KEY_FILENAME, "rb") as f:
+            public_key_utf8_encoded = f.read()
+    except IOError as e:
+        raise RuntimeError("IOError while reading public key from file") from e
+
+    try:
+        with open(EXAMPLE_RSA_PRIVATE_KEY_FILENAME, "rb") as f:
+            private_key_utf8_encoded = f.read()
+    except IOError as e:
+        raise RuntimeError("IOError while reading private key from file") from e
+
+    # 2. Create the keyring.
+    #    The DynamoDb encryption client uses this to encrypt and decrypt items.
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    keyring_input = CreateRawRsaKeyringInput(
+        key_name="my-rsa-key-name",
+        key_namespace="my-key-namespace",
+        padding_scheme=PaddingScheme.OAEP_SHA256_MGF1,
+        public_key=public_key_utf8_encoded,
+        private_key=private_key_utf8_encoded,
+    )
+
+    raw_rsa_keyring = mat_prov.create_raw_rsa_keyring(input=keyring_input)
+
+    # 3. Configure which attributes are encrypted and/or signed when writing new items.
+    #    For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    #    we must explicitly configure how they should be treated during item encryption:
+    #      - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #      - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #      - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    attribute_actions = {
+        "partition_key": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "sort_key": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
+    }
+
+    # 4. Configure which attributes we expect to be included in the signature
+    #    when reading items. There are two options for configuring this:
+    #
+    #    - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
+    #      When defining your DynamoDb schema and deciding on attribute names,
+    #      choose a distinguishing prefix (such as ":") for all attributes that
+    #      you do not want to include in the signature.
+    #      This has two main benefits:
+    #      - It is easier to reason about the security and authenticity of data within your item
+    #        when all unauthenticated data is easily distinguishable by their attribute name.
+    #      - If you need to add new unauthenticated attributes in the future,
+    #        you can easily make the corresponding update to your `attribute_actions`
+    #        and immediately start writing to that new attribute, without
+    #        any other configuration update needed.
+    #      Once you configure this field, it is not safe to update it.
+    #
+    #    - Configure `allowed_unsigned_attributes`: You may also explicitly list
+    #      a set of attributes that should be considered unauthenticated when encountered
+    #      on read. Be careful if you use this configuration. Do not remove an attribute
+    #      name from this configuration, even if you are no longer writing with that attribute,
+    #      as old items may still include this attribute, and our configuration needs to know
+    #      to continue to exclude this attribute from the signature scope.
+    #      If you add new attribute names to this field, you must first deploy the update to this
+    #      field to all readers in your host fleet before deploying the update to start writing
+    #      with that new attribute.
+    #
+    #   For this example, we currently authenticate all attributes. To make it easier to
+    #   add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
+    unsign_attr_prefix = ":"
+
+    # 5. Create the DynamoDb Encryption configuration for the table we will be writing to.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=raw_rsa_keyring,
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # 6. Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # 7. Put an item into our table using the above client.
+    #    Before the item gets sent to DynamoDb, it will be encrypted
+    #    client-side using the Raw RSA keyring.
+    item = {
+        "partition_key": {"S": "rawRsaKeyringItem"},
+        "sort_key": {"N": "0"},
+        "sensitive_data": {"S": "encrypt and sign me!"},
+    }
+
+    put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
+
+    # Demonstrate that PutItem succeeded
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # 8. Get the item back from our table using the same client.
+    #    The client will decrypt the item client-side using the Raw RSA keyring
+    #    and return the original item.
+    key_to_get = {"partition_key": {"S": "rawRsaKeyringItem"}, "sort_key": {"N": "0"}}
+
+    get_response = encrypted_ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
+
+    # Demonstrate that GetItem succeeded and returned the decrypted item
+    assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    returned_item = get_response["Item"]
+    assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
+
+
+def should_generate_new_rsa_key_pair() -> bool:
+    """
+    Check if we need to generate a new RSA key pair.
+
+    :return: True if we need to generate a new key pair, False otherwise
+    """
+    # Check if a key pair already exists
+    private_key_file = os.path.exists(EXAMPLE_RSA_PRIVATE_KEY_FILENAME)
+    public_key_file = os.path.exists(EXAMPLE_RSA_PUBLIC_KEY_FILENAME)
+
+    # If a key pair already exists: do not overwrite existing key pair
+    if private_key_file and public_key_file:
+        return False
+
+    # If only one file is present: throw exception
+    if private_key_file and not public_key_file:
+        raise ValueError(f"Missing public key file at {EXAMPLE_RSA_PUBLIC_KEY_FILENAME}")
+    if not private_key_file and public_key_file:
+        raise ValueError(f"Missing private key file at {EXAMPLE_RSA_PRIVATE_KEY_FILENAME}")
+
+    # If neither file is present, generate a new key pair
+    return True
+
+
+def generate_rsa_key_pair():
+    """Generate a new RSA key pair and save to PEM files."""
+    # Safety check: Validate neither file is present
+    if os.path.exists(EXAMPLE_RSA_PRIVATE_KEY_FILENAME) or os.path.exists(EXAMPLE_RSA_PUBLIC_KEY_FILENAME):
+        raise FileExistsError("generateRsaKeyPair will not overwrite existing PEM files")
+
+    # This code will generate a new RSA key pair for example use.
+    # The public and private key will be written to the files:
+    #  - public: EXAMPLE_RSA_PUBLIC_KEY_FILENAME
+    #  - private: EXAMPLE_RSA_PRIVATE_KEY_FILENAME
+    # In practice, you should not generate this in your code, and should instead
+    # retrieve this key from a secure key management system (e.g. HSM)
+    # This key is created here for example purposes only.
+    private_key = rsa.generate_private_key(public_exponent=65537, key_size=2048)
+
+    # Write private key PEM file
+    private_key_pem = private_key.private_bytes(
+        encoding=serialization.Encoding.PEM,
+        format=serialization.PrivateFormat.PKCS8,
+        encryption_algorithm=serialization.NoEncryption(),
+    )
+
+    try:
+        with open(EXAMPLE_RSA_PRIVATE_KEY_FILENAME, "wb") as f:
+            f.write(private_key_pem)
+    except IOError as e:
+        raise OSError("IOError while writing private key PEM") from e
+
+    # Write public key PEM file
+    public_key = private_key.public_key()
+    public_key_pem = public_key.public_bytes(
+        encoding=serialization.Encoding.PEM, format=serialization.PublicFormat.SubjectPublicKeyInfo
+    )
+
+    try:
+        with open(EXAMPLE_RSA_PUBLIC_KEY_FILENAME, "wb") as f:
+            f.write(public_key_pem)
+    except IOError as e:
+        raise RuntimeError("IOError while writing public key PEM") from e
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example.py
new file mode 100644
index 000000000..a7a2338e2
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/keyring/shared_cache_across_hierarchical_keyrings_example.py
@@ -0,0 +1,352 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Example demonstrates how to use a shared cache across multiple Hierarchical Keyrings in single-threaded environments.
+
+IMPORTANT: This example and the shared cache functionality should ONLY be used in single-threaded environments.
+The AWS Cryptographic Material Providers Library (MPL) for Python does not support multithreading for
+components that interact with KMS. For more information about multithreading limitations, see:
+https://github.com/aws/aws-cryptographic-material-providers-library/blob/main/AwsCryptographicMaterialProviders/runtimes/python/README.rst
+
+With this functionality, users only need to maintain one common shared cache across multiple
+Hierarchical Keyrings with different Key Stores instances/KMS Clients/KMS Keys in a single-threaded environment.
+
+There are three important parameters that users need to carefully set while providing the shared cache:
+
+1. Partition ID - Partition ID is an optional parameter provided to the Hierarchical Keyring input,
+which distinguishes Cryptographic Material Providers (i.e: Keyrings) writing to a cache.
+- If the Partition ID is set and is the same for two Hierarchical Keyrings (or another Material Provider),
+  they CAN share the same cache entries in the cache.
+- If the Partition ID is set and is different for two Hierarchical Keyrings (or another Material Provider),
+  they CANNOT share the same cache entries in the cache.
+- If the Partition ID is not set by the user, it is initialized as a random 16-byte UUID which makes
+  it unique for every Hierarchical Keyring, and two Hierarchical Keyrings (or another Material Provider)
+  CANNOT share the same cache entries in the cache.
+
+2. Logical Key Store Name - This parameter is set by the user when configuring the Key Store for
+the Hierarchical Keyring. This is a logical name for the branch key store.
+Suppose you have a physical Key Store (K). You create two instances of K (K1 and K2). Now, you create
+two Hierarchical Keyrings (HK1 and HK2) with these Key Store instances (K1 and K2 respectively).
+- If you want to share cache entries across these two keyrings, you should set the Logical Key Store Names
+  for both the Key Store instances (K1 and K2) to be the same.
+- If you set the Logical Key Store Names for K1 and K2 to be different, HK1 (which uses Key Store instance K1)
+  and HK2 (which uses Key Store instance K2) will NOT be able to share cache entries.
+
+3. Branch Key ID - Choose an effective Branch Key ID Schema
+
+This is demonstrated in the example below.
+Notice that both K1 and K2 are instances of the same physical Key Store (K).
+You MUST NEVER have two different physical Key Stores with the same Logical Key Store Name.
+
+Important Note: If you have two or more Hierarchy Keyrings with:
+- Same Partition ID
+- Same Logical Key Store Name of the Key Store for the Hierarchical Keyring
+- Same Branch Key ID
+then they WILL share the cache entries in the Shared Cache.
+Please make sure that you set all of Partition ID, Logical Key Store Name and Branch Key ID
+to be the same for two Hierarchical Keyrings if and only if you want them to share cache entries.
+
+This example sets up DynamoDb Encryption for the AWS SDK client using the Hierarchical
+Keyring, which establishes a key hierarchy where "branch" keys are persisted in DynamoDb.
+These branch keys are used to protect your data keys, and these branch keys are themselves
+protected by a root KMS Key.
+
+This example first creates a shared cache that you can use across multiple Hierarchical Keyrings.
+The example then configures a Hierarchical Keyring (HK1 and HK2) with the shared cache,
+a Branch Key ID and two instances (K1 and K2) of the same physical Key Store (K) respectively,
+i.e. HK1 with K1 and HK2 with K2. The example demonstrates that if you set the same Partition ID
+for HK1 and HK2, the two keyrings can share cache entries.
+If you set different Partition ID of the Hierarchical Keyrings, or different
+Logical Key Store Names of the Key Store instances, then the keyrings will NOT
+be able to share cache entries.
+
+Running this example requires access to the DDB Table whose name
+is provided in CLI arguments.
+This table must be configured with the following
+primary key configuration:
+  - Partition key is named "partition_key" with type (S)
+  - Sort key is named "sort_key" with type (S)
+
+This example also requires using a KMS Key whose ARN
+is provided in CLI arguments. You need the following access
+on this key:
+  - GenerateDataKeyWithoutPlaintext
+  - Decrypt
+"""
+from typing import Dict
+
+import boto3
+from aws_cryptographic_material_providers.keystore import KeyStore
+from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig
+from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn
+from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
+from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
+from aws_cryptographic_material_providers.mpl.models import (
+    CacheTypeDefault,
+    CacheTypeShared,
+    CreateAwsKmsHierarchicalKeyringInput,
+    CreateCryptographicMaterialsCacheInput,
+    DefaultCache,
+)
+from aws_cryptographic_material_providers.mpl.references import IKeyring
+from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    DynamoDbTableEncryptionConfig,
+    DynamoDbTablesEncryptionConfig,
+)
+from aws_dbesdk_dynamodb.structures.structured_encryption import (
+    CryptoAction,
+)
+
+
+def get_ddb_client(
+    ddb_table_name: str, hierarchical_keyring: IKeyring, attribute_actions_on_encrypt: Dict[str, CryptoAction]
+) -> boto3.client:
+    """
+    Get a DynamoDB client configured with encryption using the given keyring.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param hierarchical_keyring: The hierarchical keyring to use
+    :param attribute_actions_on_encrypt: The attribute actions for encryption
+    :return: The configured DynamoDB client
+    """
+    # Configure which attributes we expect to be included in the signature
+    # when reading items. There are two options for configuring this:
+    #
+    # - (Recommended) Configure `allowedUnsignedAttributesPrefix`:
+    #   When defining your DynamoDb schema and deciding on attribute names,
+    #   choose a distinguishing prefix (such as ":") for all attributes that
+    #   you do not want to include in the signature.
+    #   This has two main benefits:
+    #   - It is easier to reason about the security and authenticity of data within your item
+    #     when all unauthenticated data is easily distinguishable by their attribute name.
+    #   - If you need to add new unauthenticated attributes in the future,
+    #     you can easily make the corresponding update to your `attributeActionsOnEncrypt`
+    #     and immediately start writing to that new attribute, without
+    #     any other configuration update needed.
+    #   Once you configure this field, it is not safe to update it.
+    #
+    # - Configure `allowedUnsignedAttributes`: You may also explicitly list
+    #   a set of attributes that should be considered unauthenticated when encountered
+    #   on read. Be careful if you use this configuration. Do not remove an attribute
+    #   name from this configuration, even if you are no longer writing with that attribute,
+    #   as old items may still include this attribute, and our configuration needs to know
+    #   to continue to exclude this attribute from the signature scope.
+    #   If you add new attribute names to this field, you must first deploy the update to this
+    #   field to all readers in your host fleet before deploying the update to start writing
+    #   with that new attribute.
+    #
+    #   For this example, we currently authenticate all attributes. To make it easier to
+    #   add unauthenticated attributes in the future, we define a prefix ":" for such attributes.
+    unsign_attr_prefix = ":"
+
+    # Create the DynamoDb Encryption configuration for the table we will be writing to.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions_on_encrypt,
+        keyring=hierarchical_keyring,
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    return encrypted_ddb_client
+
+
+def put_get_items(ddb_table_name: str, ddb_client: boto3.client):
+    """
+    Put and get items using the given DynamoDB client.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param ddb_client: The DynamoDB client to use
+    """
+    # Put an item into our table using the given ddb client.
+    # Before the item gets sent to DynamoDb, it will be encrypted
+    # client-side, according to our configuration.
+    # This example creates a Hierarchical Keyring for a single BranchKeyId. You can, however, use a
+    # BranchKeyIdSupplier as per your use-case. See the HierarchicalKeyringsExample.java for more
+    # information.
+    item = {"partition_key": {"S": "id"}, "sort_key": {"N": "0"}, "sensitive_data": {"S": "encrypt and sign me!"}}
+
+    put_response = ddb_client.put_item(TableName=ddb_table_name, Item=item)
+
+    # Demonstrate that PutItem succeeded
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # Get the item back from our table using the same client.
+    # The client will decrypt the item client-side, and return
+    # back the original item.
+    # This example creates a Hierarchical Keyring for a single BranchKeyId. You can, however, use a
+    # BranchKeyIdSupplier as per your use-case. See the HierarchicalKeyringsExample.java for more
+    # information.
+    key_to_get = {"partition_key": {"S": "id"}, "sort_key": {"N": "0"}}
+
+    get_response = ddb_client.get_item(TableName=ddb_table_name, Key=key_to_get)
+
+    # Demonstrate that GetItem succeeded and returned the decrypted item
+    assert get_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+    returned_item = get_response["Item"]
+    assert returned_item["sensitive_data"]["S"] == "encrypt and sign me!"
+
+
+def shared_cache_across_hierarchical_keyrings_example(
+    ddb_table_name: str,
+    branch_key_id: str,
+    key_store_table_name: str,
+    logical_key_store_name: str,
+    partition_id: str,
+    kms_key_id: str,
+):
+    """
+    Create multiple hierarchical keyrings sharing a cache and use them to encrypt/decrypt DynamoDB items.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param branch_key_id: The branch key ID to use
+    :param key_store_table_name: The name of the KeyStore DynamoDB table
+    :param logical_key_store_name: The logical name for the KeyStore
+    :param partition_id: The partition ID for cache sharing
+    :param kms_key_id: ARN of the KMS key
+    """
+    # 1. Create the CryptographicMaterialsCache (CMC) to share across multiple Hierarchical Keyrings
+    #    using the Material Providers Library in a single-threaded environment.
+    #    IMPORTANT: This shared cache must only be used in single-threaded environments as the
+    #    MPL for Python does not support multithreading for KMS operations.
+    #      This CMC takes in:
+    #      - CacheType
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    cache = CacheTypeDefault(DefaultCache(entry_capacity=100))
+
+    cryptographic_materials_cache_input = CreateCryptographicMaterialsCacheInput(cache=cache)
+
+    shared_cryptographic_materials_cache = mat_prov.create_cryptographic_materials_cache(
+        input=cryptographic_materials_cache_input
+    )
+
+    # 2. Create a CacheType object for the sharedCryptographicMaterialsCache
+    #    Note that the `cache` parameter in the Hierarchical Keyring Input takes a `CacheType` as input
+    shared_cache = CacheTypeShared(
+        # This is the `Shared` CacheType that passes an already initialized shared cache
+        shared_cryptographic_materials_cache
+    )
+
+    # Initial KeyStore Setup: This example requires that you have already
+    # created your KeyStore, and have populated it with a new branch key.
+
+    # 3. Configure your KeyStore resource keystore1.
+    #    This SHOULD be the same configuration that you used
+    #    to initially create and populate your KeyStore.
+    # Note that key_store_table_name is the physical Key Store,
+    # and keystore1 is instances of this physical Key Store.
+    keystore1 = KeyStore(
+        config=KeyStoreConfig(
+            ddb_client=boto3.client("dynamodb"),
+            ddb_table_name=key_store_table_name,
+            logical_key_store_name=logical_key_store_name,
+            kms_client=boto3.client("kms"),
+            kms_configuration=KMSConfigurationKmsKeyArn(kms_key_id),
+        )
+    )
+
+    # 4. Create the Hierarchical Keyring HK1 with Key Store instance K1, partitionId,
+    #    the shared Cache and the BranchKeyId.
+    #    Note that we are now providing an already initialized shared cache instead of just mentioning
+    #    the cache type and the Hierarchical Keyring initializing a cache at initialization.
+
+    # This example creates a Hierarchical Keyring for a single BranchKeyId. You can, however, use a
+    # BranchKeyIdSupplier as per your use-case. See the HierarchicalKeyringsExample.java for more
+    # information.
+
+    # Please make sure that you read the guidance on how to set Partition ID, Logical Key Store Name and
+    # Branch Key ID at the top of this example before creating Hierarchical Keyrings with a Shared Cache.
+    # partitionId for this example is a random UUID
+    keyring_input1 = CreateAwsKmsHierarchicalKeyringInput(
+        key_store=keystore1,
+        branch_key_id=branch_key_id,
+        ttl_seconds=600,  # This dictates how often we call back to KMS to authorize use of the branch keys
+        cache=shared_cache,
+        partition_id=partition_id,
+    )
+
+    hierarchical_keyring1 = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input1)
+
+    # 5. Configure which attributes are encrypted and/or signed when writing new items.
+    #    For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    #    we must explicitly configure how they should be treated during item encryption:
+    #      - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #      - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #      - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    attribute_actions_on_encrypt = {
+        "partition_key": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "sort_key": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "sensitive_data": CryptoAction.ENCRYPT_AND_SIGN,
+    }
+
+    # 6. Get the DDB Client for Hierarchical Keyring 1.
+    ddb_client1 = get_ddb_client(ddb_table_name, hierarchical_keyring1, attribute_actions_on_encrypt)
+
+    # 7. Encrypt Decrypt roundtrip with ddb_client1
+    put_get_items(ddb_table_name, ddb_client1)
+
+    # Through the above encrypt and decrypt roundtrip, the cache will be populated and
+    # the cache entries can be used by another Hierarchical Keyring with the
+    # - Same Partition ID
+    # - Same Logical Key Store Name of the Key Store for the Hierarchical Keyring
+    # - Same Branch Key ID
+
+    # 8. Configure your KeyStore resource keystore2.
+    #       This SHOULD be the same configuration that you used
+    #       to initially create and populate your physical KeyStore.
+    #    Note that key_store_table_name is the physical Key Store,
+    #    and keystore2 is instances of this physical Key Store.
+
+    # Note that for this example, keystore2 is identical to keystore1.
+    # You can optionally change configurations like KMS Client or KMS Key ID based
+    # on your use-case.
+    # Make sure you have the required permissions to use different configurations.
+
+    # - If you want to share cache entries across two keyrings HK1 and HK2,
+    #   you should set the Logical Key Store Names for both
+    #   Key Store instances (K1 and K2) to be the same.
+    # - If you set the Logical Key Store Names for K1 and K2 to be different,
+    #   HK1 (which uses Key Store instance K1) and HK2 (which uses Key Store
+    #   instance K2) will NOT be able to share cache entries.
+    keystore2 = KeyStore(
+        config=KeyStoreConfig(
+            ddb_client=boto3.client("dynamodb"),
+            ddb_table_name=key_store_table_name,
+            logical_key_store_name=logical_key_store_name,
+            kms_client=boto3.client("kms"),
+            kms_configuration=KMSConfigurationKmsKeyArn(kms_key_id),
+        )
+    )
+
+    # 9. Create the Hierarchical Keyring HK2 with Key Store instance K2, the shared Cache
+    #    and the same partitionId and BranchKeyId used in HK1 because we want to share cache entries
+    #    (and experience cache HITS).
+
+    # Please make sure that you read the guidance on how to set Partition ID, Logical Key Store Name and
+    # Branch Key ID at the top of this example before creating Hierarchical Keyrings with a Shared Cache.
+    # partitionId for this example is a random UUID
+    keyring_input2 = CreateAwsKmsHierarchicalKeyringInput(
+        key_store=keystore2,
+        branch_key_id=branch_key_id,
+        ttl_seconds=600,  # This dictates how often we call back to KMS to authorize use of the branch keys
+        cache=shared_cache,
+        partition_id=partition_id,
+    )
+
+    hierarchical_keyring2 = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input2)
+
+    # 10. Get the DDB Client for Hierarchical Keyring 2.
+    ddb_client2 = get_ddb_client(ddb_table_name, hierarchical_keyring2, attribute_actions_on_encrypt)
+
+    # 11. Encrypt Decrypt roundtrip with ddb_client2
+    put_get_items(ddb_table_name, ddb_client2)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example.py
new file mode 100644
index 000000000..b844b573a
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/scan_error_example.py
@@ -0,0 +1,149 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+
+"""
+Example demonstrating error handling for failed decryption during DynamoDB Scan operations.
+
+Uses the Scan operation to show how to retrieve error messages from the
+returned CollectionOfErrors when some of the Scan results do not decrypt successfully.
+
+Running this example requires access to the DDB Table whose name is provided in
+CLI arguments. This table must be configured with the following primary key
+configuration:
+  - Partition key is named "partition_key" with type (S)
+  - Sort key is named "sort_key" with type (N)
+"""
+
+import sys
+
+import boto3
+from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
+from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
+from aws_cryptographic_material_providers.mpl.errors import CollectionOfErrors
+from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsMrkMultiKeyringInput, DBEAlgorithmSuiteId
+from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    DynamoDbTableEncryptionConfig,
+    DynamoDbTablesEncryptionConfig,
+)
+from aws_dbesdk_dynamodb.structures.structured_encryption import (
+    CryptoAction,
+)
+
+
+def print_exception(e: Exception, indent: str = ""):
+    """
+    Print exception and any nested CollectionOfErrors.
+
+    :param e: Exception to print
+    :param indent: Indentation string for nested errors
+    """
+    print(indent + str(e), file=sys.stderr)
+    if isinstance(e.__cause__, CollectionOfErrors):
+        print(indent + str(e.__cause__), file=sys.stderr)
+        for err in e.__cause__.list():
+            print_exception(err, indent + "   ")
+    elif isinstance(e, CollectionOfErrors):
+        for err in e.list():
+            print_exception(err, indent + "   ")
+
+
+def scan_error(kms_key_id: str, ddb_table_name: str):
+    """
+    Demonstrate handling scan errors.
+
+    :param kms_key_id: The ARN of the KMS key to use
+    :param ddb_table_name: The name of the DynamoDB table
+    """
+    # 1. Create a Keyring. This Keyring will be responsible for protecting the data keys that protect your data.
+    #    For this example, we will create a AWS KMS Keyring with the AWS KMS Key we want to use.
+    #    We will use the `create_aws_kms_mrk_multi_keyring` method to create this keyring,
+    #    as it will correctly handle both single region and Multi-Region KMS Keys.
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    kms_keyring = mat_prov.create_aws_kms_mrk_multi_keyring(
+        input=CreateAwsKmsMrkMultiKeyringInput(generator=kms_key_id)
+    )
+
+    # 2. Configure which attributes are encrypted and/or signed when writing new items.
+    #    For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    #    we must explicitly configure how they should be treated during item encryption:
+    #      - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #      - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #      - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    attribute_actions = {
+        "partition_key": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "sort_key": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "attribute1": CryptoAction.ENCRYPT_AND_SIGN,
+        "attribute2": CryptoAction.SIGN_ONLY,
+        ":attribute3": CryptoAction.DO_NOTHING,
+    }
+
+    # 3. Configure which attributes we expect to be included in the signature
+    #    when reading items. There are two options for configuring this:
+    #
+    #    - (Recommended) Configure `allowed_unsigned_attribute_prefix`:
+    #      When defining your DynamoDb schema and deciding on attribute names,
+    #      choose a distinguishing prefix (such as ":") for all attributes that
+    #      you do not want to include in the signature.
+    #      This has two main benefits:
+    #      - It is easier to reason about the security and authenticity of data within your item
+    #        when all unauthenticated data is easily distinguishable by their attribute name.
+    #      - If you need to add new unauthenticated attributes in the future,
+    #        you can easily make the corresponding update to your `attribute_actions`
+    #        and immediately start writing to that new attribute, without
+    #        any other configuration update needed.
+    #      Once you configure this field, it is not safe to update it.
+    #
+    #    - Configure `allowed_unsigned_attributes`: You may also explicitly list
+    #      a set of attributes that should be considered unauthenticated when encountered
+    #      on read. Be careful if you use this configuration. Do not remove an attribute
+    #      name from this configuration, even if you are no longer writing with that attribute,
+    #      as old items may still include this attribute, and our configuration needs to know
+    #      to continue to exclude this attribute from the signature scope.
+    #      If you add new attribute names to this field, you must first deploy the update to this
+    #      field to all readers in your host fleet before deploying the update to start writing
+    #      with that new attribute.
+    #
+    #   For this example, we have designed our DynamoDb table such that any attribute name with
+    #   the ":" prefix should be considered unauthenticated.
+    unsign_attr_prefix = ":"
+
+    # 4. Create the DynamoDb Encryption configuration for the table we will be writing to.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="partition_key",
+        sort_key_name="sort_key",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=kms_keyring,
+        allowed_unsigned_attribute_prefix=unsign_attr_prefix,
+        # Specifying an algorithm suite is not required,
+        # but is done here to demonstrate how to do so.
+        # We suggest using the
+        # `ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384` suite,
+        # which includes AES-GCM with key derivation, signing, and key commitment.
+        # This is also the default algorithm suite if one is not specified in this config.
+        # For more information on supported algorithm suites, see:
+        #   https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/supported-algorithms.html
+        algorithm_suite_id=DBEAlgorithmSuiteId.ALG_AES_256_GCM_HKDF_SHA512_COMMIT_KEY_ECDSA_P384_SYMSIG_HMAC_SHA384,
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # 5. Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # 6. Perform a Scan for which some records will not decrypt
+    expression_attribute_values = {":prefix": {"S": "Broken"}}
+
+    try:
+        encrypted_ddb_client.scan(
+            TableName=ddb_table_name,
+            FilterExpression="begins_with(partition_key, :prefix)",
+            ExpressionAttributeValues=expression_attribute_values,
+        )
+        assert False, "scan should have failed"
+    except Exception as e:
+        print_exception(e)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example.py
new file mode 100644
index 000000000..0b486e872
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/basic_searchable_encryption_example.py
@@ -0,0 +1,312 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Example demonstrating DynamoDB encryption using beacons.
+
+This example demonstrates how to set up a beacon on an encrypted attribute,
+put an item with the beacon, and query against that beacon.
+This example follows a use case of a database that stores unit inspection information.
+
+Running this example requires access to a DDB table with the
+following key configuration:
+  - Partition key is named "work_id" with type (S)
+  - Sort key is named "inspection_date" with type (S)
+This table must have a Global Secondary Index (GSI) configured named "last4-unit-index":
+  - Partition key is named "aws_dbe_b_inspector_id_last4" with type (S)
+  - Sort key is named "aws_dbe_b_unit" with type (S)
+
+In this example for storing unit inspection information, this schema is utilized for the data:
+ - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID)
+ - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD)
+ - "inspector_id_last4" stores the last 4 digits of the ID of the inspector performing the work
+ - "unit" stores a 12-digit serial number for the unit being inspected
+
+The example requires the following ordered input command line parameters:
+  1. DDB table name for table to put/query data from
+  2. Branch key ID for a branch key that was previously created in your key store. See the
+     CreateKeyStoreKeyExample.
+  3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID
+     provided in arg 2
+  4. Branch key DDB table name for the DDB table representing the branch key store
+"""
+import time
+from typing import List
+
+import boto3
+from aws_cryptographic_material_providers.keystore.client import KeyStore
+from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig
+from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn
+from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
+from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
+from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsHierarchicalKeyringInput
+from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    BeaconKeySourceSingle,
+    BeaconVersion,
+    DynamoDbTableEncryptionConfig,
+    DynamoDbTablesEncryptionConfig,
+    SearchConfig,
+    SingleKeyStore,
+    StandardBeacon,
+)
+from aws_dbesdk_dynamodb.structures.structured_encryption import CryptoAction
+
+GSI_NAME = "last4-unit-index"
+
+
+def put_item_query_item_with_beacon(
+    ddb_table_name: str, branch_key_id: str, branch_key_wrapping_kms_key_arn: str, branch_key_ddb_table_name: str
+):
+    """
+    Demonstrate using beacons with DynamoDB encryption.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param branch_key_id: Branch key ID for a branch key previously created in key store
+    :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key
+    :param branch_key_ddb_table_name: Name of DDB table representing the branch key store
+    """
+    # 1. Configure Beacons.
+    #    The beacon name must be the name of a table attribute that will be encrypted.
+    #    The `length` parameter dictates how many bits are in the beacon attribute value.
+    #    The following link provides guidance on choosing a beacon length:
+    #        https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html
+    standard_beacon_list: List[StandardBeacon] = []
+
+    # The configured DDB table has a GSI on the `aws_dbe_b_inspector_id_last4` AttributeName.
+    # This field holds the last 4 digits of an inspector ID.
+    # For our example, this field may range from 0 to 9,999 (10,000 possible values).
+    # For our example, we assume a full inspector ID is an integer
+    #     ranging from 0 to 99,999,999. We do not assume that the full inspector ID's
+    #     values are uniformly distributed across its range of possible values.
+    #     In many use cases, the prefix of an identifier encodes some information
+    #     about that identifier (e.g. zipcode and SSN prefixes encode geographic
+    #     information), while the suffix does not and is more uniformly distributed.
+    #     We will assume that the inspector ID field matches a similar use case.
+    #     So for this example, we only store and use the last
+    #     4 digits of the inspector ID, which we assume is uniformly distributed.
+    # Since the full ID's range is divisible by the range of the last 4 digits,
+    #     then the last 4 digits of the inspector ID are uniformly distributed
+    #     over the range from 0 to 9,999.
+    # See our documentation for why you should avoid creating beacons over non-uniform distributions
+    #  https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption.html#are-beacons-right-for-me
+    # A single inspector ID suffix may be assigned to multiple `work_id`s.
+    #
+    # This link provides guidance for choosing a beacon length:
+    #    https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html
+    # We follow the guidance in the link above to determine reasonable bounds
+    # for the length of a beacon on the last 4 digits of an inspector ID:
+    #  - min: log(sqrt(10,000))/log(2) ~= 6.6, round up to 7
+    #  - max: log((10,000/2))/log(2) ~= 12.3, round down to 12
+    # You will somehow need to round results to a nearby integer.
+    # We choose to round to the nearest integer; you might consider a different rounding approach.
+    # Rounding up will return fewer expected "false positives" in queries,
+    #    leading to fewer decrypt calls and better performance,
+    #    but it is easier to identify which beacon values encode distinct plaintexts.
+    # Rounding down will return more expected "false positives" in queries,
+    #    leading to more decrypt calls and worse performance,
+    #    but it is harder to identify which beacon values encode distinct plaintexts.
+    # We can choose a beacon length between 7 and 12:
+    #  - Closer to 7, we expect more "false positives" to be returned,
+    #    making it harder to identify which beacon values encode distinct plaintexts,
+    #    but leading to more decrypt calls and worse performance
+    #  - Closer to 12, we expect fewer "false positives" returned in queries,
+    #    leading to fewer decrypt calls and better performance,
+    #    but it is easier to identify which beacon values encode distinct plaintexts.
+    # As an example, we will choose 10.
+    #
+    # Values stored in aws_dbe_b_inspector_id_last4 will be 10 bits long (0x000 - 0x3ff)
+    # There will be 2^10 = 1,024 possible HMAC values.
+    # With a sufficiently large number of well-distributed inspector IDs,
+    #    for a particular beacon we expect (10,000/1,024) ~= 9.8 4-digit inspector ID suffixes
+    #    sharing that beacon value.
+    last4_beacon = StandardBeacon(name="inspector_id_last4", length=10)
+    standard_beacon_list.append(last4_beacon)
+
+    # The configured DDB table has a GSI on the `aws_dbe_b_unit` AttributeName.
+    # This field holds a unit serial number.
+    # For this example, this is a 12-digit integer from 0 to 999,999,999,999 (10^12 possible values).
+    # We will assume values for this attribute are uniformly distributed across this range.
+    # A single unit serial number may be assigned to multiple `work_id`s.
+    #
+    # This link provides guidance for choosing a beacon length:
+    #    https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html
+    # We follow the guidance in the link above to determine reasonable bounds
+    # for the length of a beacon on a unit serial number:
+    #  - min: log(sqrt(999,999,999,999))/log(2) ~= 19.9, round up to 20
+    #  - max: log((999,999,999,999/2))/log(2) ~= 38.9, round up to 39
+    # We can choose a beacon length between 20 and 39:
+    #  - Closer to 20, we expect more "false positives" to be returned,
+    #    making it harder to identify which beacon values encode distinct plaintexts,
+    #    but leading to more decrypt calls and worse performance
+    #  - Closer to 39, we expect fewer "false positives" returned in queries,
+    #    leading to fewer decrypt calls and better performance,
+    #    but it is easier to identify which beacon values encode distinct plaintexts.
+    # As an example, we will choose 30.
+    #
+    # Values stored in aws_dbe_b_unit will be 30 bits long (0x00000000 - 0x3fffffff)
+    # There will be 2^30 = 1,073,741,824 ~= 1.1B possible HMAC values.
+    # With a sufficiently large number of well-distributed inspector IDs,
+    #    for a particular beacon we expect (10^12/2^30) ~= 931.3 unit serial numbers
+    #    sharing that beacon value.
+    unit_beacon = StandardBeacon(name="unit", length=30)
+    standard_beacon_list.append(unit_beacon)
+
+    # 2. Configure Keystore.
+    #    The keystore is a separate DDB table where the client stores encryption and decryption materials.
+    #    In order to configure beacons on the DDB client, you must configure a keystore.
+    #
+    #    This example expects that you have already set up a KeyStore with a single branch key.
+    #    See the "Create KeyStore Table Example" and "Create KeyStore Key Example" for how to do this.
+    #    After you create a branch key, you should persist its ID for use in this example.
+    keystore = KeyStore(
+        config=KeyStoreConfig(
+            ddb_client=boto3.client("dynamodb"),
+            ddb_table_name=branch_key_ddb_table_name,
+            logical_key_store_name=branch_key_ddb_table_name,
+            kms_client=boto3.client("kms"),
+            kms_configuration=KMSConfigurationKmsKeyArn(value=branch_key_wrapping_kms_key_arn),
+        )
+    )
+
+    # 3. Create BeaconVersion.
+    #    The BeaconVersion inside the list holds the list of beacons on the table.
+    #    The BeaconVersion also stores information about the keystore.
+    #    BeaconVersion must be provided:
+    #      - keyStore: The keystore configured in step 2.
+    #      - keySource: A configuration for the key source.
+    #        For simple use cases, we can configure a 'singleKeySource' which
+    #        statically configures a single beaconKey. That is the approach this example takes.
+    #        For use cases where you want to use different beacon keys depending on the data
+    #        (for example if your table holds data for multiple tenants, and you want to use
+    #        a different beacon key per tenant), look into configuring a MultiKeyStore:
+    #          https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption-multitenant.html
+    beacon_versions = [
+        BeaconVersion(
+            standard_beacons=standard_beacon_list,
+            version=1,  # MUST be 1
+            key_store=keystore,
+            key_source=BeaconKeySourceSingle(
+                SingleKeyStore(
+                    # `key_id` references a beacon key.
+                    # For every branch key we create in the keystore,
+                    # we also create a beacon key.
+                    # This beacon key is not the same as the branch key,
+                    # but is created with the same ID as the branch key.
+                    key_id=branch_key_id,
+                    cache_ttl=6000,
+                )
+            ),
+        )
+    ]
+
+    # 4. Create a Hierarchical Keyring
+    #    This is a KMS keyring that utilizes the keystore table.
+    #    This config defines how items are encrypted and decrypted.
+    #    NOTE: You should configure this to use the same keystore as your search config.
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    keyring_input = CreateAwsKmsHierarchicalKeyringInput(
+        branch_key_id=branch_key_id, key_store=keystore, ttl_seconds=6000
+    )
+
+    kms_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input)
+
+    # 5. Configure which attributes are encrypted and/or signed when writing new items.
+    #    For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    #    we must explicitly configure how they should be treated during item encryption:
+    #      - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #      - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #      - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    #    Any attributes that will be used in beacons must be configured as ENCRYPT_AND_SIGN.
+    attribute_actions = {
+        "work_id": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "inspection_date": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "inspector_id_last4": CryptoAction.ENCRYPT_AND_SIGN,  # Beaconized attributes must be encrypted
+        "unit": CryptoAction.ENCRYPT_AND_SIGN,  # Beaconized attributes must be encrypted
+    }
+
+    # 6. Create the DynamoDb Encryption configuration for the table we will be writing to.
+    #    The beaconVersions are added to the search configuration.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="work_id",
+        sort_key_name="inspection_date",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=kms_keyring,
+        search=SearchConfig(write_version=1, versions=beacon_versions),  # MUST be 1
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # 7. Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # 8. Put an item into our table using the above client.
+    #    Before the item gets sent to DynamoDb, it will be encrypted
+    #        client-side, according to our configuration.
+    #    Since our configuration includes beacons for `inspector_id_last4` and `unit`,
+    #        the client will add two additional attributes to the item. These attributes will have names
+    #        `aws_dbe_b_inspector_id_last4` and `aws_dbe_b_unit`. Their values will be HMACs
+    #        truncated to as many bits as the beacon's `length` parameter; e.g.
+    #    aws_dbe_b_inspector_id_last4 = truncate(HMAC("4321"), 10)
+    #    aws_dbe_b_unit = truncate(HMAC("123456789012"), 30)
+    item = {
+        "work_id": {"S": "1313ba89-5661-41eb-ba6c-cb1b4cb67b2d"},
+        "inspection_date": {"S": "2023-06-13"},
+        "inspector_id_last4": {"S": "4321"},
+        "unit": {"S": "123456789012"},
+    }
+
+    put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item)
+
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # 9. Query for the item we just put.
+    #    Note that we are constructing the query as if we were querying on plaintext values.
+    #    However, the DDB encryption client will detect that this attribute name has a beacon configured.
+    #    The client will add the beaconized attribute name and attribute value to the query,
+    #        and transform the query to use the beaconized name and value.
+    #    Internally, the client will query for and receive all items with a matching HMAC value in the beacon field.
+    #    This may include a number of "false positives" with different ciphertext, but the same truncated HMAC.
+    #    e.g. if truncate(HMAC("123456789012"), 30)
+    #         == truncate(HMAC("098765432109"), 30),
+    #    the query will return both items.
+    #    The client will decrypt all returned items to determine which ones have the expected attribute values,
+    #        and only surface items with the correct plaintext to the user.
+    #    This procedure is internal to the client and is abstracted away from the user;
+    #    e.g. the user will only see "123456789012" and never
+    #       "098765432109", though the actual query returned both.
+    expression_attribute_names = {"#last4": "inspector_id_last4", "#unit": "unit"}
+
+    expression_attribute_values = {":last4": {"S": "4321"}, ":unit": {"S": "123456789012"}}
+
+    # GSIs do not update instantly
+    # so if the results come back empty
+    # we retry after a short sleep
+    for _ in range(10):
+        query_response = encrypted_ddb_client.query(
+            TableName=ddb_table_name,
+            IndexName=GSI_NAME,
+            KeyConditionExpression="#last4 = :last4 and #unit = :unit",
+            ExpressionAttributeNames=expression_attribute_names,
+            ExpressionAttributeValues=expression_attribute_values,
+        )
+
+        # Validate query was returned successfully
+        assert query_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+        items = query_response.get("Items", [])
+        # if no results, sleep and try again
+        if not items:
+            time.sleep(0.02)
+            continue
+
+        # Validate only 1 item was returned: the item we just put
+        assert len(items) == 1
+        returned_item = items[0]
+        # Validate the item has the expected attributes
+        assert returned_item["inspector_id_last4"]["S"] == "4321"
+        assert returned_item["unit"]["S"] == "123456789012"
+        break
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example.py
new file mode 100644
index 000000000..06ea52ea6
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/beacon_styles_searchable_encryption_example.py
@@ -0,0 +1,298 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Example demonstrating DynamoDB encryption using beacon styles.
+
+This example demonstrates how to use Beacons Styles on Standard Beacons on encrypted attributes,
+    put an item with the beacon, and query against that beacon.
+This example follows a use case of a database that stores food information.
+    This is an extension of the "BasicSearchableEncryptionExample" in this directory
+    and uses the same table schema.
+
+Running this example requires access to a DDB table with the
+following key configuration:
+  - Partition key is named "work_id" with type (S)
+  - Sort key is named "inspection_time" with type (S)
+
+In this example for storing food information, this schema is utilized for the data:
+ - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID)
+ - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD)
+ - "fruit" stores one type of fruit
+ - "basket" stores a set of types of fruit
+ - "dessert" stores one type of dessert
+ - "veggies" stores a set of types of vegetable
+ - "work_type" stores a unit inspection category
+
+The example requires the following ordered input command line parameters:
+  1. DDB table name for table to put/query data from
+  2. Branch key ID for a branch key that was previously created in your key store. See the
+     CreateKeyStoreKeyExample.
+  3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID
+     provided in arg 2
+  4. Branch key DDB table name for the DDB table representing the branch key store
+"""
+from typing import List
+
+import boto3
+from aws_cryptographic_material_providers.keystore.client import KeyStore
+from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig
+from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn
+from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
+from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
+from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsHierarchicalKeyringInput
+from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    AsSet,
+    BeaconKeySourceSingle,
+    BeaconStyleAsSet,
+    BeaconStylePartOnly,
+    BeaconStyleShared,
+    BeaconStyleSharedSet,
+    BeaconVersion,
+    CompoundBeacon,
+    DynamoDbTableEncryptionConfig,
+    DynamoDbTablesEncryptionConfig,
+    EncryptedPart,
+    PartOnly,
+    SearchConfig,
+    Shared,
+    SharedSet,
+    SignedPart,
+    SingleKeyStore,
+    StandardBeacon,
+)
+from aws_dbesdk_dynamodb.structures.structured_encryption import CryptoAction
+
+
+def put_item_query_item_with_beacon_styles(
+    ddb_table_name: str, branch_key_id: str, branch_key_wrapping_kms_key_arn: str, branch_key_ddb_table_name: str
+):
+    """
+    Demonstrate using beacon styles with DynamoDB encryption.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param branch_key_id: Branch key ID for a branch key previously created in key store
+    :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key
+    :param branch_key_ddb_table_name: Name of DDB table representing the branch key store
+    """
+    # 1. Create Beacons.
+    standard_beacon_list: List[StandardBeacon] = []
+
+    # The fruit beacon allows searching on the encrypted fruit attribute
+    # We have selected 30 as an example beacon length, but you should go to
+    # https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html
+    # when creating your beacons.
+    fruit_beacon = StandardBeacon(name="fruit", length=30)
+    standard_beacon_list.append(fruit_beacon)
+
+    # The basket beacon allows searching on the encrypted basket attribute
+    # basket is used as a Set, and therefore needs a beacon style to reflect that.
+    # Further, we need to be able to compare the items in basket to the fruit attribute
+    # so we `share` this beacon with `fruit`.
+    # Since we need both of these things, we use the SharedSet style.
+    basket_beacon = StandardBeacon(name="basket", length=30, style=BeaconStyleSharedSet(SharedSet(other="fruit")))
+    standard_beacon_list.append(basket_beacon)
+
+    # The dessert beacon allows searching on the encrypted dessert attribute
+    # We need to be able to compare the dessert attribute to the fruit attribute
+    # so we `share` this beacon with `fruit`.
+    dessert_beacon = StandardBeacon(name="dessert", length=30, style=BeaconStyleShared(Shared(other="fruit")))
+    standard_beacon_list.append(dessert_beacon)
+
+    # The veggie_beacon allows searching on the encrypted veggies attribute
+    # veggies is used as a Set, and therefore needs a beacon style to reflect that.
+    veggie_beacon = StandardBeacon(name="veggies", length=30, style=BeaconStyleAsSet(AsSet()))
+    standard_beacon_list.append(veggie_beacon)
+
+    # The work_type_beacon allows searching on the encrypted work_type attribute
+    # We only use it as part of the compound work_unit beacon,
+    # so we disable its use as a standalone beacon
+    work_type_beacon = StandardBeacon(name="work_type", length=30, style=BeaconStylePartOnly(PartOnly()))
+    standard_beacon_list.append(work_type_beacon)
+
+    # Here we build a compound beacon from work_id and work_type
+    # If we had tried to make a StandardBeacon from work_type, we would have seen an error
+    # because work_type is "PartOnly"
+    encrypted_part_list = [EncryptedPart(name="work_type", prefix="T-")]
+
+    signed_part_list = [SignedPart(name="work_id", prefix="I-")]
+
+    compound_beacon_list = [
+        CompoundBeacon(name="work_unit", split=".", encrypted=encrypted_part_list, signed=signed_part_list)
+    ]
+
+    # 2. Configure the Keystore
+    #    These are the same constructions as in the Basic example, which describes these in more detail.
+    keystore = KeyStore(
+        config=KeyStoreConfig(
+            ddb_client=boto3.client("dynamodb"),
+            ddb_table_name=branch_key_ddb_table_name,
+            logical_key_store_name=branch_key_ddb_table_name,
+            kms_client=boto3.client("kms"),
+            kms_configuration=KMSConfigurationKmsKeyArn(value=branch_key_wrapping_kms_key_arn),
+        )
+    )
+
+    # 3. Create BeaconVersion.
+    #    This is similar to the Basic example
+    beacon_versions = [
+        BeaconVersion(
+            standard_beacons=standard_beacon_list,
+            compound_beacons=compound_beacon_list,
+            version=1,  # MUST be 1
+            key_store=keystore,
+            key_source=BeaconKeySourceSingle(SingleKeyStore(key_id=branch_key_id, cache_ttl=6000)),
+        )
+    ]
+
+    # 4. Create a Hierarchical Keyring
+    #    This is the same configuration as in the Basic example.
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    keyring_input = CreateAwsKmsHierarchicalKeyringInput(
+        branch_key_id=branch_key_id, key_store=keystore, ttl_seconds=6000
+    )
+
+    kms_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input)
+
+    # 5. Configure which attributes are encrypted and/or signed when writing new items.
+    attribute_actions = {
+        "work_id": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "inspection_date": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "dessert": CryptoAction.ENCRYPT_AND_SIGN,  # Beaconized attributes must be encrypted
+        "fruit": CryptoAction.ENCRYPT_AND_SIGN,  # Beaconized attributes must be encrypted
+        "basket": CryptoAction.ENCRYPT_AND_SIGN,  # Beaconized attributes must be encrypted
+        "veggies": CryptoAction.ENCRYPT_AND_SIGN,  # Beaconized attributes must be encrypted
+        "work_type": CryptoAction.ENCRYPT_AND_SIGN,  # Beaconized attributes must be encrypted
+    }
+
+    # 6. Create the DynamoDb Encryption configuration for the table we will be writing to.
+    #    The beaconVersions are added to the search configuration.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="work_id",
+        sort_key_name="inspection_date",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=kms_keyring,
+        search=SearchConfig(write_version=1, versions=beacon_versions),  # MUST be 1
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # 7. Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # 8. Create item one, specifically with "dessert != fruit", and "fruit in basket".
+    item1 = {
+        "work_id": {"S": "1"},
+        "inspection_date": {"S": "2023-06-13"},
+        "dessert": {"S": "cake"},
+        "fruit": {"S": "banana"},
+        "basket": {"SS": ["apple", "banana", "pear"]},
+        "veggies": {"SS": ["beans", "carrots", "celery"]},
+        "work_type": {"S": "small"},
+    }
+
+    # 9. Create item two, specifically with "dessert == fruit", and "fruit not in basket".
+    item2 = {
+        "work_id": {"S": "2"},
+        "inspection_date": {"S": "2023-06-13"},
+        "fruit": {"S": "orange"},
+        "dessert": {"S": "orange"},
+        "basket": {"SS": ["blackberry", "blueberry", "strawberry"]},
+        "veggies": {"SS": ["beans", "carrots", "peas"]},
+        "work_type": {"S": "large"},
+    }
+
+    # 10. Add the two items
+    put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item1)
+    # Validate object put successfully
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item2)
+    # Validate object put successfully
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # 11. Test the first type of Set operation:
+    # Select records where the basket attribute holds a particular value
+    expression_attribute_values = {":value": {"S": "banana"}}
+
+    scan_response = encrypted_ddb_client.scan(
+        TableName=ddb_table_name,
+        FilterExpression="contains(basket, :value)",
+        ExpressionAttributeValues=expression_attribute_values,
+    )
+    # Validate query was returned successfully
+    assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # Validate only 1 item was returned: item1
+    assert len(scan_response["Items"]) == 1
+    assert scan_response["Items"][0] == item1
+
+    # 12. Test the second type of Set operation:
+    # Select records where the basket attribute holds the fruit attribute
+    scan_response = encrypted_ddb_client.scan(TableName=ddb_table_name, FilterExpression="contains(basket, fruit)")
+    # Validate query was returned successfully
+    assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # Validate only 1 item was returned: item1
+    assert len(scan_response["Items"]) == 1
+    assert scan_response["Items"][0] == item1
+
+    # 13. Test the third type of Set operation:
+    # Select records where the fruit attribute exists in a particular set
+    expression_attribute_values = {":value": {"SS": ["boysenberry", "grape", "orange"]}}
+
+    scan_response = encrypted_ddb_client.scan(
+        TableName=ddb_table_name,
+        FilterExpression="contains(:value, fruit)",
+        ExpressionAttributeValues=expression_attribute_values,
+    )
+    # Validate query was returned successfully
+    assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # Validate only 1 item was returned: item2
+    assert len(scan_response["Items"]) == 1
+    assert scan_response["Items"][0] == item2
+
+    # 14. Test a Shared search. Select records where the dessert attribute matches the fruit attribute
+    scan_response = encrypted_ddb_client.scan(TableName=ddb_table_name, FilterExpression="dessert = fruit")
+    # Validate query was returned successfully
+    assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # Validate only 1 item was returned: item2
+    assert len(scan_response["Items"]) == 1
+    assert scan_response["Items"][0] == item2
+
+    # 15. Test the AsSet attribute 'veggies':
+    # Select records where the veggies attribute holds a particular value
+    expression_attribute_values = {":value": {"S": "peas"}}
+
+    scan_response = encrypted_ddb_client.scan(
+        TableName=ddb_table_name,
+        FilterExpression="contains(veggies, :value)",
+        ExpressionAttributeValues=expression_attribute_values,
+    )
+    # Validate query was returned successfully
+    assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # Validate only 1 item was returned: item2
+    assert len(scan_response["Items"]) == 1
+    assert scan_response["Items"][0] == item2
+
+    # 16. Test the compound beacon 'work_unit':
+    expression_attribute_values = {":value": {"S": "I-1.T-small"}}
+
+    scan_response = encrypted_ddb_client.scan(
+        TableName=ddb_table_name,
+        FilterExpression="work_unit = :value",
+        ExpressionAttributeValues=expression_attribute_values,
+    )
+    # Validate query was returned successfully
+    assert scan_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # Validate only 1 item was returned: item1
+    assert len(scan_response["Items"]) == 1
+    assert scan_response["Items"][0] == item1
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example.py
new file mode 100644
index 000000000..56b10e865
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/compound_beacon_searchable_encryption_example.py
@@ -0,0 +1,310 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Example demonstrating DynamoDB encryption using compound beacons.
+
+This example demonstrates how to set up a compound beacon on encrypted attributes,
+    put an item with the beacon, and query against that beacon.
+This example follows a use case of a database that stores unit inspection information.
+    This is an extension of the "BasicSearchableEncryptionExample" in this directory.
+    This example uses the same situation (storing unit inspection information)
+    and the same table schema.
+However, this example uses a different Global Secondary Index (GSI)
+    that is based on a compound beacon configuration composed of
+    the `last4` and `unit` attributes.
+
+Running this example requires access to a DDB table with the
+following key configuration:
+  - Partition key is named "work_id" with type (S)
+  - Sort key is named "inspection_time" with type (S)
+This table must have a Global Secondary Index (GSI) configured named "last4UnitCompound-index":
+  - Partition key is named "aws_dbe_b_last4UnitCompound" with type (S)
+
+In this example for storing unit inspection information, this schema is utilized for the data:
+ - "work_id" stores a unique identifier for a unit inspection work order (v4 UUID)
+ - "inspection_date" stores an ISO 8601 date for the inspection (YYYY-MM-DD)
+ - "inspector_id_last4" stores the last 4 digits of the ID of the inspector performing the work
+ - "unit" stores a 12-digit serial number for the unit being inspected
+
+The example requires the following ordered input command line parameters:
+  1. DDB table name for table to put/query data from
+  2. Branch key ID for a branch key that was previously created in your key store. See the
+     CreateKeyStoreKeyExample.
+  3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID
+     provided in arg 2
+  4. Branch key DDB table name for the DDB table representing the branch key store
+"""
+import concurrent.futures
+import time
+from typing import Dict
+
+import boto3
+from aws_cryptographic_material_providers.keystore.client import KeyStore
+from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig
+from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn
+from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
+from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
+from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsHierarchicalKeyringInput
+from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
+from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.client import (
+    DynamoDbEncryptionTransforms,
+)
+from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import (
+    ResolveAttributesInput,
+)
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    BeaconKeySourceSingle,
+    BeaconStylePartOnly,
+    BeaconVersion,
+    CompoundBeacon,
+    Constructor,
+    ConstructorPart,
+    DynamoDbTableEncryptionConfig,
+    DynamoDbTablesEncryptionConfig,
+    EncryptedPart,
+    PartOnly,
+    SearchConfig,
+    SingleKeyStore,
+    StandardBeacon,
+)
+from aws_dbesdk_dynamodb.structures.structured_encryption import CryptoAction
+
+GSI_NAME = "last4UnitCompound-index"
+MAX_CONCURRENT_QUERY_THREADS = 10
+
+
+def put_and_query_item_with_compound_beacon(ddb_client: EncryptedClient, ddb_table_name: str, item: Dict):
+    """
+    Put and query an item using a compound beacon.
+
+    :param ddb_client: The encrypted DynamoDB client
+    :param ddb_table_name: The name of the DynamoDB table
+    :param item: The item to put and query
+    """
+    # Write the item to the table
+    put_response = ddb_client.put_item(TableName=ddb_table_name, Item=item)
+    # Validate object put successfully
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # Query for the item we just put.
+    expression_attribute_names = {"#compound": "last4UnitCompound"}
+
+    # This query expression takes a few factors into consideration:
+    #  - The configured prefix for the last 4 digits of an inspector ID is "L-";
+    #    the prefix for the unit is "U-"
+    #  - The configured split character, separating component parts, is "."
+    #  - The default constructor adds encrypted parts in the order they are in the encrypted list, which
+    #    configures `last4` to come before `unit``
+    # NOTE: We did not need to create a compound beacon for this query. This query could have also been
+    #       done by querying on the partition and sort key, as was done in the Basic example.
+    #       This is intended to be a simple example to demonstrate how one might set up a compound beacon.
+    #       For examples where compound beacons are required, see the Complex example.
+    #       The most basic extension to this example that would require a compound beacon would add a third
+    #       part to the compound beacon, then query against three parts.
+    expression_attribute_values = {":value": {"S": "L-5678.U-011899988199"}}
+
+    # GSIs do not update instantly
+    # so if the results come back empty
+    # we retry after a short sleep
+    for _ in range(10):
+        query_response = ddb_client.query(
+            TableName=ddb_table_name,
+            IndexName=GSI_NAME,
+            KeyConditionExpression="#compound = :value",
+            ExpressionAttributeNames=expression_attribute_names,
+            ExpressionAttributeValues=expression_attribute_values,
+        )
+
+        # Validate query was returned successfully
+        assert query_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+        items = query_response.get("Items", [])
+        # if no results, sleep and try again
+        if not items:
+            time.sleep(0.02)
+            continue
+
+        # Validate only 1 item was returned: the item we just put
+        assert len(items) == 1
+        returned_item = items[0]
+        # Validate the item has the expected attributes
+        assert returned_item["inspector_id_last4"]["S"] == "5678"
+        assert returned_item["unit"]["S"] == "011899988199"
+        break
+
+
+def put_item_query_item_with_compound_beacon(
+    ddb_table_name: str, branch_key_id: str, branch_key_wrapping_kms_key_arn: str, branch_key_ddb_table_name: str
+):
+    """
+    Demonstrate using compound beacons with DynamoDB encryption.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param branch_key_id: Branch key ID for a branch key previously created in key store
+    :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key
+    :param branch_key_ddb_table_name: Name of DDB table representing the branch key store
+    """
+    # 1. Create Beacons.
+    #    These are the same beacons as in the "BasicSearchableEncryptionExample" in this directory.
+    #    See that file to see details on beacon construction and parameters.
+    #    While we will not directly query against these beacons,
+    #      you must create standard beacons on encrypted fields
+    #      that we wish to use in compound beacons.
+    #    We mark them both as PartOnly to enforce the fact that
+    #      we will not directly query against these beacons.
+    standard_beacon_list = [
+        StandardBeacon(name="inspector_id_last4", length=10, style=BeaconStylePartOnly(PartOnly())),
+        StandardBeacon(name="unit", length=30, style=BeaconStylePartOnly(PartOnly())),
+    ]
+
+    # 2. Define encrypted parts.
+    #    Encrypted parts define the beacons that can be used to construct a compound beacon,
+    #        and how the compound beacon prefixes those beacon values.
+    # A encrypted part must receive:
+    #  - name: Name of a standard beacon
+    #  - prefix: Any string. This is plaintext that prefixes the beaconized value in the compound beacon.
+    #            Prefixes must be unique across the configuration, and must not be a prefix of another prefix;
+    #            i.e. for all configured prefixes, the first N characters of a prefix must not equal another prefix.
+    # In practice, it is suggested to have a short value distinguishable from other parts served on the prefix.
+    encrypted_part_list = [
+        # For this example, we will choose "L-" as the prefix for "Last 4 digits of inspector ID".
+        # With this prefix and the standard beacon's bit length definition (10), the beaconized
+        #     version of the inspector ID's last 4 digits will appear as
+        #     `L-000` to `L-3ff` inside a compound beacon.
+        EncryptedPart(name="inspector_id_last4", prefix="L-"),
+        # For this example, we will choose "U-" as the prefix for "unit".
+        # With this prefix and the standard beacon's bit length definition (30), a unit beacon will appear
+        #     as `U-00000000` to `U-3fffffff` inside a compound beacon.
+        EncryptedPart(name="unit", prefix="U-"),
+    ]
+
+    constructor_parts = [
+        ConstructorPart(name="inspector_id_last4", required=True),
+        ConstructorPart(name="unit", required=True),
+    ]
+
+    constructors = [Constructor(parts=constructor_parts)]
+
+    # 3. Define compound beacon.
+    #    A compound beacon allows one to serve multiple beacons or attributes from a single index.
+    #    A compound beacon must receive:
+    #     - name: The name of the beacon. Compound beacon values will be written to `aws_ddb_e_[name]`.
+    #     - split: A character separating parts in a compound beacon
+    #    A compound beacon may also receive:
+    #     - encrypted: A list of encrypted parts. This is effectively a list of beacons. We provide the list
+    #                  that we created above.
+    #     - constructors: A list of constructors. This is an ordered list of possible ways to create a beacon.
+    #                     We have not defined any constructors here; see the complex example for how to do this.
+    #                     The client will provide a default constructor, which will write a compound beacon as:
+    #                     all signed parts in the order they are added to the signed list;
+    #                     all encrypted parts in order they are added to the encrypted list; all parts required.
+    #                     In this example, we expect compound beacons to be written as
+    #                     `L-XXX.U-YYYYYYYY`, since our encrypted list looks like
+    #                     [last4EncryptedPart, unitEncryptedPart].
+    #     - signed: A list of signed parts, i.e. plaintext attributes. This would be provided if we
+    #                     wanted to use plaintext values as part of constructing our compound beacon. We do not
+    #                     provide this here; see the Complex example for an example.
+    compound_beacon_list = [CompoundBeacon(name="last4UnitCompound", constructors=constructors, split=".")]
+
+    # 4. Configure the Keystore
+    #    These are the same constructions as in the Basic example, which describes these in more detail.
+    keystore = KeyStore(
+        config=KeyStoreConfig(
+            ddb_client=boto3.client("dynamodb"),
+            ddb_table_name=branch_key_ddb_table_name,
+            logical_key_store_name=branch_key_ddb_table_name,
+            kms_client=boto3.client("kms"),
+            kms_configuration=KMSConfigurationKmsKeyArn(value=branch_key_wrapping_kms_key_arn),
+        )
+    )
+
+    # 5. Create BeaconVersion.
+    #    This is similar to the Basic example, except we have also provided a compoundBeaconList.
+    #    We must also continue to provide all of the standard beacons that compose a compound beacon list.
+    beacon_versions = [
+        BeaconVersion(
+            encrypted_parts=encrypted_part_list,
+            standard_beacons=standard_beacon_list,
+            compound_beacons=compound_beacon_list,
+            version=1,  # MUST be 1
+            key_store=keystore,
+            key_source=BeaconKeySourceSingle(SingleKeyStore(key_id=branch_key_id, cache_ttl=6000)),
+        )
+    ]
+
+    # 6. Create a Hierarchical Keyring
+    #    This is the same configuration as in the Basic example.
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    keyring_input = CreateAwsKmsHierarchicalKeyringInput(
+        branch_key_id=branch_key_id, key_store=keystore, ttl_seconds=6000
+    )
+
+    kms_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input)
+
+    # 7. Configure which attributes are encrypted and/or signed when writing new items.
+    attribute_actions = {
+        "work_id": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "inspection_date": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "inspector_id_last4": CryptoAction.ENCRYPT_AND_SIGN,  # Beaconized attributes must be encrypted
+        "unit": CryptoAction.ENCRYPT_AND_SIGN,  # Beaconized attributes must be encrypted
+    }
+
+    # We do not need to define a crypto action on last4UnitCompound.
+    # We only need to define crypto actions on attributes that we pass to PutItem.
+
+    # 8. Create the DynamoDb Encryption configuration for the table we will be writing to.
+    #    The beaconVersions are added to the search configuration.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="work_id",
+        sort_key_name="inspection_date",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=kms_keyring,
+        search=SearchConfig(write_version=1, versions=beacon_versions),  # MUST be 1
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # 9. Create an item with both attributes used in the compound beacon.
+    item = {
+        "work_id": {"S": "9ce39272-8068-4efd-a211-cd162ad65d4c"},
+        "inspection_date": {"S": "2023-06-13"},
+        "inspector_id_last4": {"S": "5678"},
+        "unit": {"S": "011899988199"},
+    }
+
+    # 10. If developing or debugging, verify config by checking compound beacon values directly
+    trans = DynamoDbEncryptionTransforms(config=tables_config)
+
+    resolve_input = ResolveAttributesInput(table_name=ddb_table_name, item=item, version=1)
+
+    resolve_output = trans.resolve_attributes(input=resolve_input)
+
+    # VirtualFields is empty because we have no Virtual Fields configured
+    assert not resolve_output.virtual_fields
+
+    # Verify that CompoundBeacons has the expected value
+    cbs = {"last4UnitCompound": "L-5678.U-011899988199"}
+    assert resolve_output.compound_beacons == cbs
+    # Note : the compound beacon actually stored in the table is not "L-5678.U-011899988199"
+    # but rather something like "L-abc.U-123", as both parts are EncryptedParts
+    # and therefore the text is replaced by the associated beacon
+
+    # 11. Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    put_and_query_item_with_compound_beacon(encrypted_ddb_client, ddb_table_name, item)
+
+    # If instead you were working in a multi-threaded context
+    # it might look like this
+    with concurrent.futures.ThreadPoolExecutor(max_workers=MAX_CONCURRENT_QUERY_THREADS) as executor:
+        futures = []
+        for _ in range(2 * MAX_CONCURRENT_QUERY_THREADS):
+            for _ in range(20):
+                futures.append(
+                    executor.submit(put_and_query_item_with_compound_beacon, encrypted_ddb_client, ddb_table_name, item)
+                )
+        concurrent.futures.wait(futures, timeout=30)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example.py
new file mode 100644
index 000000000..681f660cc
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/src/searchable_encryption/virtual_beacon_searchable_encryption_example.py
@@ -0,0 +1,415 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Example demonstrating DynamoDB encryption using virtual beacons.
+
+This example demonstrates how to set up a virtual field from two DDB
+attributes, create a standard beacon with that field, put an item with
+that beacon, and query against that beacon.
+
+A virtual field is a field consisting of a transformation of one or more attributes in a DDB item.
+Virtual fields are useful in querying against encrypted fields that only have a handful of
+possible values. They allow you to take fields with few possible values, concatenate
+them to other fields, then query against the combined field. This enables using these types of
+fields in queries while making it infeasible to identify which beacon values encode
+the few possible distinct plaintexts. This is explained in more detail below.
+Virtual fields are not stored in the DDB table. However, they are used to construct
+a beacon, the value of which is stored.
+
+For more information on virtual fields, see
+  https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/beacons.html#virtual-field
+
+For our example, we will construct a virtual field
+from two DDB attributes `state` and `hasTestResult` as `state`+prefix(`hasTestResult`, 1).
+We will then create a beacon out of this virtual field and use it to search.
+
+This example follows a use case of a database that stores customer test result metadata.
+Records are indexed by `customer_id` and store a `state` attribute, representing the
+US state or territory where the customer lives, and a `hasTestResult` boolean attribute,
+representing whether the customer has a "test result" available. (Maybe this represents
+some medical test result, and this table stores "result available" metadata.) We assume
+that values in these fields are uniformly distributed across all possible values for
+these fields (56 for `state`, 2 for `hasTestResult`), and are uniformly distributed across
+customer IDs.
+
+The motivation behind this example is to demonstrate how and why one would use a virtual beacon.
+In this example, our table stores records with an encrypted boolean `hasTestResult` attribute.
+We would like to be able to query for customers in a given state with a `true` hasTestResult
+attribute.
+
+To be able to execute this query securely and efficiently, we want the following
+properties on our table:
+ 1. Hide the distribution of `hasTestResult` attribute values (i.e. it should be infeasible
+    to determine the percentage of `true`s to `false`s across the dataset from beaconized
+    values)
+ 2. Query against a combination of whether `hasTestResult` is true/false and the `state` field
+We cannot achieve these properties with a standard beacon on a true/false attribute. Following
+the guidance to choose a beacon length:
+  https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html
+For a boolean value (in our case, whether `hasTestResult` is true or false), the acceptable
+bounds for beacon length are either 0 or 1. This corresponds to either not storing a beacon
+(length 0), or effectively storing another boolean attribute (length 1). With
+length 0, this beacon is useless for searching (violating property 2); with length 1, this
+beacon may not hide the attribute (violating property 1).
+In addition, choosing a longer beacon length does not help us.
+Each attribute value is mapped to a distinct beacon.
+Since booleans only have 2 possible attribute values, we will still only have 2 possible
+beacon values, though those values may be longer. A longer beacon provides no advantages over
+beacon of length 1 in this situation.
+
+A compound beacon also does not help.
+To (over)simplify, a compound beacon is a concatenation of standard beacons,
+i.e. beacon(`state`)+beacon(`hasTestResult`).
+The `hasTestResult` beacon is still visible, so we would still have the problems above.
+
+To achieve these properties, we instead construct a virtual field and use that in our beacon,
+i.e. beacon(`state`+`hasTestResult`). Assuming these fields are well-distributed across
+customer IDs and possible values, this gives us both desired properties; we can query against
+both attributes while hiding information from the underlying data. This is demonstrated in more
+detail below.
+
+Running this example requires access to a DDB table  with the
+following primary key configuration:
+  - Partition key is named "customer_id" with type (S)
+  - Sort key is named "create_time" with type (S)
+This table must have a Global Secondary Index (GSI) configured named "stateAndHasTestResult-index":
+  - Partition key is named "aws_dbe_b_stateAndHasTestResult" with type (S)
+
+In this example for storing customer location data, this schema is utilized for the data:
+ - "customer_id" stores a unique customer identifier
+ - "create_time" stores a Unix timestamp
+ - "state" stores an encrypted 2-letter US state or territory abbreviation
+       (https://www.faa.gov/air_traffic/publications/atpubs/cnt_html/appendix_a.html)
+ - "hasTestResult" is not part of the schema, but is an attribute utilized in this example.
+    It stores a boolean attribute (false/true) indicating whether this customer has a test result
+    available.
+
+The example requires the following ordered input command line parameters:
+  1. DDB table name for table to put/query data from
+  2. Branch key ID for a branch key that was previously created in your key store. See the
+     CreateKeyStoreKeyExample.
+  3. Branch key wrapping KMS key ARN for the KMS key used to create the branch key with ID
+     provided in arg 2
+  4. Branch key DDB table name for the DDB table representing the branch key store
+"""
+import time
+
+import boto3
+from aws_cryptographic_material_providers.keystore.client import KeyStore
+from aws_cryptographic_material_providers.keystore.config import KeyStoreConfig
+from aws_cryptographic_material_providers.keystore.models import KMSConfigurationKmsKeyArn
+from aws_cryptographic_material_providers.mpl import AwsCryptographicMaterialProviders
+from aws_cryptographic_material_providers.mpl.config import MaterialProvidersConfig
+from aws_cryptographic_material_providers.mpl.models import CreateAwsKmsHierarchicalKeyringInput
+from aws_dbesdk_dynamodb.encrypted.client import EncryptedClient
+from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.client import (
+    DynamoDbEncryptionTransforms,
+)
+from aws_dbesdk_dynamodb.smithygenerated.aws_cryptography_dbencryptionsdk_dynamodb_transforms.models import (
+    ResolveAttributesInput,
+)
+from aws_dbesdk_dynamodb.structures.dynamodb import (
+    BeaconKeySourceSingle,
+    BeaconVersion,
+    DynamoDbTableEncryptionConfig,
+    DynamoDbTablesEncryptionConfig,
+    GetPrefix,
+    SearchConfig,
+    SingleKeyStore,
+    StandardBeacon,
+    VirtualField,
+    VirtualPart,
+    VirtualTransformPrefix,
+)
+from aws_dbesdk_dynamodb.structures.structured_encryption import CryptoAction
+
+GSI_NAME = "stateAndHasTestResult-index"
+
+
+def put_item_query_item_with_virtual_beacon(
+    ddb_table_name: str, branch_key_id: str, branch_key_wrapping_kms_key_arn: str, branch_key_ddb_table_name: str
+):
+    """
+    Demonstrate using virtual beacons with DynamoDB encryption.
+
+    :param ddb_table_name: The name of the DynamoDB table
+    :param branch_key_id: Branch key ID for a branch key previously created in key store
+    :param branch_key_wrapping_kms_key_arn: ARN of KMS key used to create the branch key
+    :param branch_key_ddb_table_name: Name of DDB table representing the branch key store
+    """
+    # 1. Construct a length-1 prefix virtual transform.
+    #    `hasTestResult` is a binary attribute, containing either `true` or `false`.
+    #    As an example to demonstrate virtual transforms, we will truncate the value
+    #    of `hasTestResult` in the virtual field to the length-1 prefix of the binary value, i.e.:
+    #     - "true" -> "t"
+    #     - "false -> "f"
+    #    This is not necessary. This is done as a demonstration of virtual transforms.
+    #    Virtual transform operations treat all attributes as strings
+    #    (i.e. the boolean value `true` is interpreted as a string "true"),
+    #    so its length-1 prefix is just "t".
+    length1_prefix_virtual_transform_list = [VirtualTransformPrefix(GetPrefix(length=1))]
+
+    # 2. Construct the VirtualParts required for the VirtualField
+    has_test_result_part = VirtualPart(
+        loc="hasTestResult",
+        # Here, we apply the length-1 prefix virtual transform
+        trans=length1_prefix_virtual_transform_list,
+    )
+
+    state_part = VirtualPart(
+        loc="state",
+        # Note that we do not apply any transform to the `state` attribute,
+        # and the virtual field will read in the attribute as-is.
+    )
+
+    # 3. Construct the VirtualField from the VirtualParts
+    #    Note that the order that virtual parts are added to the virtualPartList
+    #    dictates the order in which they are concatenated to build the virtual field.
+    #    You must add virtual parts in the same order on write as you do on read.
+    virtual_part_list = [state_part, has_test_result_part]
+
+    state_and_has_test_result_field = VirtualField(name="stateAndHasTestResult", parts=virtual_part_list)
+
+    virtual_field_list = [state_and_has_test_result_field]
+
+    # 4. Configure our beacon.
+    #    The virtual field is assumed to hold a US 2-letter state abbreviation
+    #    (56 possible values = 50 states + 6 territories) concatenated with a binary attribute
+    #    (2 possible values: true/false hasTestResult field), we expect a population size of
+    #    56 * 2 = 112 possible values.
+    #    We will also assume that these values are reasonably well-distributed across
+    #    customer IDs. In practice, this will not be true. We would expect
+    #    more populous states to appear more frequently in the database.
+    #    A more complex analysis would show that a stricter upper bound
+    #    is necessary to account for this by hiding information from the
+    #    underlying distribution.
+    #
+    #    This link provides guidance for choosing a beacon length:
+    #       https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/choosing-beacon-length.html
+    #    We follow the guidance in the link above to determine reasonable bounds for beacon length:
+    #     - min: log(sqrt(112))/log(2) ~= 3.4, round down to 3
+    #     - max: log((112/2))/log(2) ~= 5.8, round up to 6
+    #    You will somehow need to round results to a nearby integer.
+    #    We choose to round to the nearest integer; you might consider a different rounding approach.
+    #    Rounding up will return fewer expected "false positives" in queries,
+    #       leading to fewer decrypt calls and better performance,
+    #       but it is easier to identify which beacon values encode distinct plaintexts.
+    #    Rounding down will return more expected "false positives" in queries,
+    #       leading to more decrypt calls and worse performance,
+    #       but it is harder to identify which beacon values encode distinct plaintexts.
+    #    We can choose a beacon length between 3 and 6:
+    #     - Closer to 3, we expect more "false positives" to be returned,
+    #       making it harder to identify which beacon values encode distinct plaintexts,
+    #       but leading to more decrypt calls and worse performance
+    #     - Closer to 6, we expect fewer "false positives" returned in queries,
+    #       leading to fewer decrypt calls and better performance,
+    #       but it is easier to identify which beacon values encode distinct plaintexts.
+    #    As an example, we will choose 5.
+    #    Values stored in aws_dbe_b_stateAndHasTestResult will be 5 bits long (0x00 - 0x1f)
+    #    There will be 2^5 = 32 possible HMAC values.
+    #    With a well-distributed dataset (112 values), for a particular beacon we expect
+    #    (112/32) = 3.5 combinations of abbreviation + true/false attribute
+    #    sharing that beacon value.
+    standard_beacon_list = [
+        StandardBeacon(
+            # This name is the same as our virtual field's name above
+            name="stateAndHasTestResult",
+            length=5,
+        )
+    ]
+
+    # 5. Configure Keystore.
+    #    This example expects that you have already set up a KeyStore with a single branch key.
+    #    See the "CreateKeyStoreTableExample" and "CreateKeyStoreKeyExample" files for how to do this.
+    #    After you create a branch key, you should persist its ID for use in this example.
+    keystore = KeyStore(
+        config=KeyStoreConfig(
+            ddb_client=boto3.client("dynamodb"),
+            ddb_table_name=branch_key_ddb_table_name,
+            logical_key_store_name=branch_key_ddb_table_name,
+            kms_client=boto3.client("kms"),
+            kms_configuration=KMSConfigurationKmsKeyArn(value=branch_key_wrapping_kms_key_arn),
+        )
+    )
+
+    # 6. Create BeaconVersion.
+    #    The BeaconVersion inside the list holds the list of beacons on the table.
+    #    The BeaconVersion also stores information about the keystore.
+    #    BeaconVersion must be provided:
+    #      - keyStore: The keystore configured in the previous step.
+    #      - keySource: A configuration for the key source.
+    #        For simple use cases, we can configure a 'singleKeySource' which
+    #        statically configures a single beaconKey. That is the approach this example takes.
+    #        For use cases where you want to use different beacon keys depending on the data
+    #        (for example if your table holds data for multiple tenants, and you want to use
+    #        a different beacon key per tenant), look into configuring a MultiKeyStore:
+    #          https://docs.aws.amazon.com/database-encryption-sdk/latest/devguide/searchable-encryption-multitenant.html
+    #    We also provide our standard beacon list and virtual fields here.
+    beacon_versions = [
+        BeaconVersion(
+            virtual_fields=virtual_field_list,
+            standard_beacons=standard_beacon_list,
+            version=1,  # MUST be 1
+            key_store=keystore,
+            key_source=BeaconKeySourceSingle(
+                SingleKeyStore(
+                    # `key_id` references a beacon key.
+                    # For every branch key we create in the keystore,
+                    # we also create a beacon key.
+                    # This beacon key is not the same as the branch key,
+                    # but is created with the same ID as the branch key.
+                    key_id=branch_key_id,
+                    cache_ttl=6000,
+                )
+            ),
+        )
+    ]
+
+    # 7. Create a Hierarchical Keyring
+    #    This is a KMS keyring that utilizes the keystore table.
+    #    This config defines how items are encrypted and decrypted.
+    #    NOTE: You should configure this to use the same keystore as your search config.
+    mat_prov = AwsCryptographicMaterialProviders(config=MaterialProvidersConfig())
+
+    keyring_input = CreateAwsKmsHierarchicalKeyringInput(
+        branch_key_id=branch_key_id, key_store=keystore, ttl_seconds=6000
+    )
+
+    kms_keyring = mat_prov.create_aws_kms_hierarchical_keyring(input=keyring_input)
+
+    # 8. Configure which attributes are encrypted and/or signed when writing new items.
+    #    For each attribute that may exist on the items we plan to write to our DynamoDbTable,
+    #    we must explicitly configure how they should be treated during item encryption:
+    #      - ENCRYPT_AND_SIGN: The attribute is encrypted and included in the signature
+    #      - SIGN_ONLY: The attribute not encrypted, but is still included in the signature
+    #      - DO_NOTHING: The attribute is not encrypted and not included in the signature
+    #    Any attributes that will be used in beacons must be configured as ENCRYPT_AND_SIGN.
+    attribute_actions = {
+        "customer_id": CryptoAction.SIGN_ONLY,  # Our partition attribute must be SIGN_ONLY
+        "create_time": CryptoAction.SIGN_ONLY,  # Our sort attribute must be SIGN_ONLY
+        "state": CryptoAction.ENCRYPT_AND_SIGN,  # Beaconized attributes must be encrypted
+        "hasTestResult": CryptoAction.ENCRYPT_AND_SIGN,  # Beaconized attributes must be encrypted
+    }
+
+    # 9. Create the DynamoDb Encryption configuration for the table we will be writing to.
+    #    The beaconVersions are added to the search configuration.
+    table_config = DynamoDbTableEncryptionConfig(
+        logical_table_name=ddb_table_name,
+        partition_key_name="customer_id",
+        sort_key_name="create_time",
+        attribute_actions_on_encrypt=attribute_actions,
+        keyring=kms_keyring,
+        search=SearchConfig(write_version=1, versions=beacon_versions),  # MUST be 1
+    )
+
+    table_configs = {ddb_table_name: table_config}
+    tables_config = DynamoDbTablesEncryptionConfig(table_encryption_configs=table_configs)
+
+    # 10. Create test items
+
+    # Create item with hasTestResult=true
+    item_with_has_test_result = {
+        "customer_id": {"S": "ABC-123"},
+        "create_time": {"N": "1681495205"},
+        "state": {"S": "CA"},
+        "hasTestResult": {"BOOL": True},
+    }
+
+    # Create item with hasTestResult=false
+    item_with_no_has_test_result = {
+        "customer_id": {"S": "DEF-456"},
+        "create_time": {"N": "1681495205"},
+        "state": {"S": "CA"},
+        "hasTestResult": {"BOOL": False},
+    }
+
+    # 11. If developing or debugging, verify config by checking virtual field values directly
+    trans = DynamoDbEncryptionTransforms(config=tables_config)
+
+    resolve_input = ResolveAttributesInput(table_name=ddb_table_name, item=item_with_has_test_result, version=1)
+
+    resolve_output = trans.resolve_attributes(input=resolve_input)
+
+    # CompoundBeacons is empty because we have no Compound Beacons configured
+    assert not resolve_output.compound_beacons
+
+    # Verify that VirtualFields has the expected value
+    vf = {"stateAndHasTestResult": "CAt"}
+    assert resolve_output.virtual_fields == vf
+
+    # 12. Create the EncryptedClient
+    ddb_client = boto3.client("dynamodb")
+    encrypted_ddb_client = EncryptedClient(client=ddb_client, encryption_config=tables_config)
+
+    # 13. Put two items into our table using the above client.
+    #     The two items will differ only in their `customer_id` attribute (primary key)
+    #         and their `hasTestResult` attribute.
+    #     We will query against these items to demonstrate how to use our setup above
+    #         to query against our `stateAndHasTestResult` beacon.
+    #     Before the item gets sent to DynamoDb, it will be encrypted
+    #         client-side, according to our configuration.
+    #     Since our configuration includes a beacon on a virtual field named
+    #         `stateAndHasTestResult`, the client will add an attribute
+    #         to the item with name `aws_dbe_b_stateAndHasTestResult`.
+    #         Its value will be an HMAC truncated to as many bits as the
+    #         beacon's `length` parameter; i.e. 5.
+    put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item_with_has_test_result)
+    # Assert PutItem was successful
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    put_response = encrypted_ddb_client.put_item(TableName=ddb_table_name, Item=item_with_no_has_test_result)
+    # Assert PutItem was successful
+    assert put_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+    # 14. Query by stateAndHasTestResult attribute.
+    #     Note that we are constructing the query as if we were querying on plaintext values.
+    #     However, the DDB encryption client will detect that this attribute name has a beacon configured.
+    #     The client will add the beaconized attribute name and attribute value to the query,
+    #         and transform the query to use the beaconized name and value.
+    #     Internally, the client will query for and receive all items with a matching HMAC value in the beacon field.
+    #     This may include a number of "false positives" with different ciphertext, but the same truncated HMAC.
+    #     e.g. if truncate(HMAC("CAt"), 5) == truncate(HMAC("DCf"), 5), the query will return both items.
+    #     The client will decrypt all returned items to determine which ones have the expected attribute values,
+    #         and only surface items with the correct plaintext to the user.
+    #     This procedure is internal to the client and is abstracted away from the user;
+    #     e.g. the user will only see "CAt" and never "DCf", though the actual query returned both.
+    expression_attribute_names = {"#stateAndHasTestResult": "stateAndHasTestResult"}
+
+    # We are querying for the item with `state`="CA" and `hasTestResult`=`true`.
+    # Since we added virtual parts as `state` then `hasTestResult`,
+    #     we must write our query expression in the same order.
+    # We constructed our virtual field as `state`+`hasTestResult`,
+    #     so we add the two parts in that order.
+    # Since we also created a virtual transform that truncated `hasTestResult`
+    #     to its length-1 prefix, i.e. "true" -> "t",
+    #     we write that field as its length-1 prefix in the query.
+    expression_attribute_values = {":stateAndHasTestResult": {"S": "CAt"}}
+
+    # GSIs do not update instantly
+    # so if the results come back empty
+    # we retry after a short sleep
+    for _ in range(10):
+        query_response = encrypted_ddb_client.query(
+            TableName=ddb_table_name,
+            IndexName=GSI_NAME,
+            KeyConditionExpression="#stateAndHasTestResult = :stateAndHasTestResult",
+            ExpressionAttributeNames=expression_attribute_names,
+            ExpressionAttributeValues=expression_attribute_values,
+        )
+
+        # Validate query was returned successfully
+        assert query_response["ResponseMetadata"]["HTTPStatusCode"] == 200
+
+        items = query_response.get("Items", [])
+        # if no results, sleep and try again
+        if not items:
+            time.sleep(0.02)
+            continue
+
+        # Validate only 1 item was returned: the item with the expected attributes
+        assert len(items) == 1
+        returned_item = items[0]
+        # Validate the item has the expected attributes
+        assert returned_item["state"]["S"] == "CA"
+        assert returned_item["hasTestResult"]["BOOL"] is True
+        break
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/cleanup.py b/Examples/runtimes/python/DynamoDBEncryption/test/cleanup.py
new file mode 100644
index 000000000..4ae8a3014
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/cleanup.py
@@ -0,0 +1,82 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""
+Test cleanup utilities for DynamoDB Encryption SDK.
+
+This module provides utilities for cleaning up resources after running tests.
+
+WARNING: Please be careful. This is only a test utility and should NOT be used in production code.
+It is specifically designed for cleaning up test resources after test execution.
+- Running this code on production resources or any data you want to keep could result
+  in cryptographic shredding (permanent loss of access to encrypted data).
+- Only use this on test resources that you are willing to permanently delete.
+- Never run this against any production DynamoDB tables. Ensure you have backups
+  of any important data before running cleanup operations.
+"""
+import boto3
+
+BRANCH_KEY_IDENTIFIER_FIELD = "branch-key-id"
+TYPE_FIELD = "type"
+
+
+def delete_branch_key(
+    identifier: str,
+    table_name: str,
+    ddb_client: boto3.client,
+) -> bool:
+    """
+    Delete all branch key items with the given identifier.
+
+    Args:
+        identifier: Branch key identifier to delete
+        table_name: DynamoDB table name
+        ddb_client: DynamoDB client to use
+
+    Returns:
+        True if all items were deleted, False if more than 100 items exist
+
+    Raises:
+        ValueError: If an item is not a branch key
+
+    """
+    if ddb_client is None:
+        ddb_client = boto3.client("dynamodb")
+
+    # Query for items with matching identifier
+    query_response = ddb_client.query(
+        TableName=table_name,
+        KeyConditionExpression="#pk = :pk",
+        ExpressionAttributeNames={"#pk": BRANCH_KEY_IDENTIFIER_FIELD},
+        ExpressionAttributeValues={":pk": {"S": identifier}},
+    )
+
+    items = query_response.get("Items", [])
+    if not items:
+        return True
+
+    # Create delete requests for each item
+    delete_items = []
+    for item in items:
+        if TYPE_FIELD not in item:
+            raise ValueError("Item is not a branch key")
+
+        delete_item = {
+            "Delete": {
+                "Key": {BRANCH_KEY_IDENTIFIER_FIELD: {"S": identifier}, TYPE_FIELD: item[TYPE_FIELD]},
+                "TableName": table_name,
+            }
+        }
+        delete_items.append(delete_item)
+
+    if not delete_items:
+        return True
+
+    # DynamoDB transactions are limited to 100 items
+    if len(delete_items) > 100:
+        delete_items = delete_items[:100]
+
+    # Execute the delete transaction
+    ddb_client.transact_write_items(TransactItems=delete_items)
+
+    # Return False if we had to truncate the deletion
+    return len(items) <= 100
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/__init__.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/__init__.py
new file mode 100644
index 000000000..fa977e22f
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/__init__.py
@@ -0,0 +1,3 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Stub to allow relative imports of examples from tests."""
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_hierarchical_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_hierarchical_keyring_example.py
new file mode 100644
index 000000000..61cdb10a0
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_hierarchical_keyring_example.py
@@ -0,0 +1,37 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test hierarchical keyring example."""
+import time
+
+import pytest
+
+from ...src.create_keystore_key_example import keystore_create_key
+from ...src.keyring.hierarchical_keyring_example import hierarchical_keyring_get_item_put_item
+from ..cleanup import delete_branch_key
+from ..test_utils import (
+    TEST_DDB_TABLE_NAME,
+    TEST_KEYSTORE_KMS_KEY_ID,
+    TEST_KEYSTORE_NAME,
+    TEST_LOGICAL_KEYSTORE_NAME,
+)
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_hierarchical_keyring_example():
+    """Test hierarchical_keyring_example."""
+    # Create new branch keys for test
+    key_id1 = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID)
+    key_id2 = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID)
+
+    # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood
+    # our test fails due to eventual consistency issues.
+    time.sleep(5)
+
+    hierarchical_keyring_get_item_put_item(
+        TEST_DDB_TABLE_NAME, key_id1, key_id2, TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID
+    )
+
+    # Cleanup Branch Key
+    delete_branch_key(key_id1, TEST_KEYSTORE_NAME, None)
+    delete_branch_key(key_id2, TEST_KEYSTORE_NAME, None)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_ecdh_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_ecdh_keyring_example.py
new file mode 100644
index 000000000..d54bd273e
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_ecdh_keyring_example.py
@@ -0,0 +1,40 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test KMS ECDH keyring examples."""
+import pytest
+
+from ...src.keyring.kms_ecdh_keyring_example import (
+    EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME,
+    EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME,
+    kms_ecdh_discovery_get_item,
+    kms_ecdh_keyring_get_item_put_item,
+    should_get_new_public_keys,
+    write_public_key_pem_for_ecc_key,
+)
+from ..test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT, TEST_KMS_ECDH_KEY_ID_P256_SENDER
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_kms_ecdh_keyring_example_static():
+    """Test kms_ecdh_keyring_example with static configuration."""
+    # You may provide your own ECC public keys at
+    # - EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME
+    # - EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME.
+    # If you provide these, the keys MUST be on curve P256
+    # This must be the public key for the ECC key represented at eccKeyArn
+    # If this file is not present, this will write a UTF-8 encoded PEM file for you.
+    if should_get_new_public_keys():
+        write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_SENDER, EXAMPLE_ECC_PUBLIC_KEY_SENDER_FILENAME)
+        write_public_key_pem_for_ecc_key(TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT, EXAMPLE_ECC_PUBLIC_KEY_RECIPIENT_FILENAME)
+
+    kms_ecdh_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_SENDER)
+
+
+def test_kms_ecdh_keyring_example_discovery():
+    """Test kms_ecdh_keyring_example with discovery configuration."""
+    # In this example you do not need to provide the recipient ECC Public Key.
+    # On initialization, the keyring will call KMS:getPublicKey on the configured
+    # recipientKmsIdentifier set on the keyring. This example uses the previous example
+    # to write an item meant for the recipient.
+    kms_ecdh_discovery_get_item(TEST_DDB_TABLE_NAME, TEST_KMS_ECDH_KEY_ID_P256_RECIPIENT)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_rsa_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_rsa_keyring_example.py
new file mode 100644
index 000000000..908531d5f
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_kms_rsa_keyring_example.py
@@ -0,0 +1,27 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test for the KMS RSA keyring example."""
+import pytest
+
+from ...src.keyring.kms_rsa_keyring_example import (
+    kms_rsa_keyring_example,
+    should_get_new_public_key,
+    write_public_key_pem_for_rsa_key,
+)
+from ..test_utils import (
+    TEST_DDB_TABLE_NAME,
+    TEST_KMS_RSA_KEY_ID,
+)
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_kms_rsa_keyring_example():
+    """Test the KMS RSA keyring example."""
+    # You may provide your own RSA public key at EXAMPLE_RSA_PUBLIC_KEY_FILENAME.
+    # This must be the public key for the RSA key represented at rsa_key_arn.
+    # If this file is not present, this will write a UTF-8 encoded PEM file for you.
+    if should_get_new_public_key():
+        write_public_key_pem_for_rsa_key(TEST_KMS_RSA_KEY_ID)
+
+    kms_rsa_keyring_example(TEST_DDB_TABLE_NAME, TEST_KMS_RSA_KEY_ID)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_discovery_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_discovery_multi_keyring_example.py
new file mode 100644
index 000000000..d2238bfa7
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_discovery_multi_keyring_example.py
@@ -0,0 +1,22 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test MRK discovery multi-keyring example."""
+import pytest
+
+from ...src.keyring.mrk_discovery_multi_keyring_example import multi_mrk_discovery_keyring_get_item_put_item
+from ..test_utils import (
+    TEST_AWS_ACCOUNT_ID,
+    TEST_AWS_REGION,
+    TEST_DDB_TABLE_NAME,
+    TEST_MRK_KEY_ID,
+)
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_mrk_discovery_multi_keyring_example():
+    """Test mrk_discovery_multi_keyring_example."""
+    accounts = [TEST_AWS_ACCOUNT_ID]
+    regions = [TEST_AWS_REGION]
+
+    multi_mrk_discovery_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, TEST_MRK_KEY_ID, accounts, regions)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_multi_keyring_example.py
new file mode 100644
index 000000000..e63ccc323
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_mrk_multi_keyring_example.py
@@ -0,0 +1,21 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test MRK multi-keyring example."""
+import pytest
+
+from ...src.keyring.mrk_multi_keyring_example import multi_mrk_keyring_get_item_put_item
+from ..test_utils import (
+    TEST_DDB_TABLE_NAME,
+    TEST_KMS_KEY_ID,
+    TEST_MRK_KEY_ID,
+    TEST_MRK_REPLICA_KEY_ID_US_EAST_1,
+)
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_mrk_multi_keyring_example():
+    """Test mrk_multi_keyring_example."""
+    multi_mrk_keyring_get_item_put_item(
+        TEST_DDB_TABLE_NAME, TEST_MRK_KEY_ID, TEST_KMS_KEY_ID, TEST_MRK_REPLICA_KEY_ID_US_EAST_1
+    )
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_multi_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_multi_keyring_example.py
new file mode 100644
index 000000000..0db6f3d80
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_multi_keyring_example.py
@@ -0,0 +1,19 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test multi-keyring example."""
+import secrets
+
+import pytest
+
+from ...src.keyring.multi_keyring_example import multi_keyring_get_item_put_item
+from ..test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_multi_keyring_example():
+    """Test multi_keyring_example."""
+    # Generate a new AES key
+    aes_key_bytes = secrets.token_bytes(32)  # 32 bytes = 256 bits
+
+    multi_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID, aes_key_bytes)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_aes_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_aes_keyring_example.py
new file mode 100644
index 000000000..93da5bddf
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_aes_keyring_example.py
@@ -0,0 +1,19 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test raw AES keyring example."""
+import secrets
+
+import pytest
+
+from ...src.keyring.raw_aes_keyring_example import raw_aes_keyring_get_item_put_item
+from ..test_utils import TEST_DDB_TABLE_NAME
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_raw_aes_keyring_example():
+    """Test raw_aes_keyring_example."""
+    # Generate a new AES key
+    aes_key_bytes = secrets.token_bytes(32)  # 32 bytes = 256 bits
+
+    raw_aes_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, aes_key_bytes)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_ecdh_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_ecdh_keyring_example.py
new file mode 100644
index 000000000..6a7676d89
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_ecdh_keyring_example.py
@@ -0,0 +1,74 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test raw ECDH keyring examples."""
+import pytest
+from aws_cryptography_primitives.smithygenerated.aws_cryptography_primitives.models import ECDHCurveSpec
+
+from ...src.keyring.raw_ecdh_keyring_example import (
+    discovery_raw_ecdh_keyring_get_item,
+    ephemeral_raw_ecdh_keyring_put_item,
+    generate_ecc_key_pairs,
+    raw_ecdh_keyring_get_item_put_item,
+    should_generate_new_ecc_key_pairs,
+)
+from ..test_utils import TEST_DDB_TABLE_NAME
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_static_raw_ecdh_keyring_example():
+    """Test raw_ecdh_keyring_example with static configuration."""
+    # You may provide your own ECC Key pairs in the files located at
+    # - EXAMPLE_ECC_PRIVATE_KEY_FILENAME_SENDER
+    # - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT
+    # If you provide this, the keys MUST be on curve P256
+    # If these files are not present, this will generate a pair for you.
+    # For this example we will use the curve P256.
+    if should_generate_new_ecc_key_pairs():
+        generate_ecc_key_pairs()
+
+    # Part of using these keyrings is knowing which curve the keys used in the key agreement
+    # lie on. The keyring will fail if the keys do not lie on the configured curve.
+    raw_ecdh_keyring_get_item_put_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256)
+
+
+def test_ephemeral_raw_ecdh_keyring_example():
+    """Test raw_ecdh_keyring_example with ephemeral configuration."""
+    # You may provide your own ECC Public Key in the files located at
+    # - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT
+    # If you provide this, the keys MUST be on curve P256
+    # If these files are not present, this will generate a pair for you.
+    # For this example we will use the curve P256.
+    if should_generate_new_ecc_key_pairs():
+        generate_ecc_key_pairs()
+
+    # Part of using these keyrings is knowing which curve the keys used in the key agreement
+    # lie on. The keyring will fail if the keys do not lie on the configured curve.
+    ephemeral_raw_ecdh_keyring_put_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256)
+
+
+def test_discovery_raw_ecdh_keyring_example():
+    """Test raw_ecdh_keyring_example with discovery configuration."""
+    # You may provide your own ECC Public Key in the files located at
+    # - EXAMPLE_ECC_PUBLIC_KEY_FILENAME_RECIPIENT
+    # - EXAMPLE_ECC_PRIVATE_KEY_FILENAME_RECIPIENT
+    # If you provide this, the keys MUST be on curve P256
+    # If these files are not present, this will generate a pair for you.
+    # For this example we will use the curve P256.
+    if should_generate_new_ecc_key_pairs():
+        generate_ecc_key_pairs()
+
+    # The discovery configuration is not allowed to encrypt
+    # To understand this example best, we will write a record with the ephemeral configuration
+    # in the previous example. This means that the recipient public key configured on
+    # both keyrings is the same. This means that the other party has the recipient public key
+    # and is writing messages meant only for the owner of the recipient public key to decrypt.
+
+    # In this call we are writing a record that is written with an ephemeral sender key pair.
+    # The recipient will be able to decrypt the message
+    ephemeral_raw_ecdh_keyring_put_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256)
+
+    # In this call we are reading a record that was written with the recipient's public key.
+    # It will use the recipient's private key and the sender's public key stored in the message to
+    # calculate the appropriate shared secret to successfully decrypt the message.
+    discovery_raw_ecdh_keyring_get_item(TEST_DDB_TABLE_NAME, ECDHCurveSpec.ECC_NIST_P256)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_rsa_keyring_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_rsa_keyring_example.py
new file mode 100644
index 000000000..590a7948e
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_raw_rsa_keyring_example.py
@@ -0,0 +1,25 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test for the Raw RSA keyring example."""
+import pytest
+
+from ...src.keyring.raw_rsa_keyring_example import (
+    generate_rsa_key_pair,
+    raw_rsa_keyring_example,
+    should_generate_new_rsa_key_pair,
+)
+from ..test_utils import TEST_DDB_TABLE_NAME
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_raw_rsa_keyring_example():
+    """Test the Raw RSA keyring example."""
+    # You may provide your own RSA key pair in the files located at
+    #  - EXAMPLE_RSA_PRIVATE_KEY_FILENAME
+    #  - EXAMPLE_RSA_PUBLIC_KEY_FILENAME
+    # If these files are not present, this will generate a pair for you
+    if should_generate_new_rsa_key_pair():
+        generate_rsa_key_pair()
+
+    raw_rsa_keyring_example(TEST_DDB_TABLE_NAME)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_shared_cache_across_hierarchical_keyrings_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_shared_cache_across_hierarchical_keyrings_example.py
new file mode 100644
index 000000000..2cfc10fe7
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/keyring/test_shared_cache_across_hierarchical_keyrings_example.py
@@ -0,0 +1,43 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test for the shared cache across hierarchical keyrings example."""
+import time
+
+import pytest
+
+from ...src.create_keystore_key_example import keystore_create_key
+from ...src.keyring.shared_cache_across_hierarchical_keyrings_example import (
+    shared_cache_across_hierarchical_keyrings_example,
+)
+from ..cleanup import delete_branch_key
+from ..test_utils import (
+    TEST_DDB_TABLE_NAME,
+    TEST_KEYSTORE_KMS_KEY_ID,
+    TEST_KEYSTORE_NAME,
+    TEST_LOGICAL_KEYSTORE_NAME,
+    TEST_PARTITION_ID,
+)
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_shared_cache_across_hierarchical_keyrings_example():
+    """Test the shared cache across hierarchical keyrings example."""
+    # Create new branch key for test
+    key_id = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID)
+
+    # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood
+    # our test fails due to eventual consistency issues.
+    time.sleep(5)
+
+    shared_cache_across_hierarchical_keyrings_example(
+        TEST_DDB_TABLE_NAME,
+        key_id,
+        TEST_KEYSTORE_NAME,
+        TEST_LOGICAL_KEYSTORE_NAME,
+        TEST_PARTITION_ID,
+        TEST_KEYSTORE_KMS_KEY_ID,
+    )
+
+    # Cleanup Branch Key
+    delete_branch_key(key_id, TEST_KEYSTORE_NAME, None)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_basic_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_basic_searchable_encryption_example.py
new file mode 100644
index 000000000..0292c26c2
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_basic_searchable_encryption_example.py
@@ -0,0 +1,40 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test basic searchable encryption example."""
+import time
+
+import pytest
+
+from ...src.create_keystore_key_example import keystore_create_key
+from ...src.searchable_encryption.basic_searchable_encryption_example import put_item_query_item_with_beacon
+from ..cleanup import delete_branch_key
+from .searchable_encryption_test_utils import (
+    TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN,
+    TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME,
+    TEST_LOGICAL_KEYSTORE_NAME,
+    UNIT_INSPECTION_TEST_DDB_TABLE_NAME,
+)
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_basic_example():
+    """Test basic searchable encryption example."""
+    # Create new branch key for test
+    key_id = keystore_create_key(
+        TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN
+    )
+
+    # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood
+    # our test fails due to eventual consistency issues.
+    time.sleep(5)
+
+    put_item_query_item_with_beacon(
+        UNIT_INSPECTION_TEST_DDB_TABLE_NAME,
+        key_id,
+        TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN,
+        TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME,
+    )
+
+    # Cleanup Branch Key
+    delete_branch_key(key_id, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, None)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_beacon_styles_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_beacon_styles_searchable_encryption_example.py
new file mode 100644
index 000000000..65e7f929b
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_beacon_styles_searchable_encryption_example.py
@@ -0,0 +1,42 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test beacon styles searchable encryption example."""
+import time
+
+import pytest
+
+from ...src.create_keystore_key_example import keystore_create_key
+from ...src.searchable_encryption.beacon_styles_searchable_encryption_example import (
+    put_item_query_item_with_beacon_styles,
+)
+from ..cleanup import delete_branch_key
+from .searchable_encryption_test_utils import (
+    TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN,
+    TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME,
+    TEST_LOGICAL_KEYSTORE_NAME,
+    UNIT_INSPECTION_TEST_DDB_TABLE_NAME,
+)
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_beacon_styles_item_encrypt_decrypt():
+    """Test beacon styles searchable encryption example."""
+    # Create new branch key for test
+    key_id = keystore_create_key(
+        TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN
+    )
+
+    # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood
+    # our test fails due to eventual consistency issues.
+    time.sleep(5)
+
+    put_item_query_item_with_beacon_styles(
+        UNIT_INSPECTION_TEST_DDB_TABLE_NAME,
+        key_id,
+        TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN,
+        TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME,
+    )
+
+    # Cleanup Branch Key
+    delete_branch_key(key_id, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, None)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_compound_beacon_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_compound_beacon_searchable_encryption_example.py
new file mode 100644
index 000000000..6d05fbf75
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_compound_beacon_searchable_encryption_example.py
@@ -0,0 +1,42 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test compound beacon searchable encryption example."""
+import time
+
+import pytest
+
+from ...src.create_keystore_key_example import keystore_create_key
+from ...src.searchable_encryption.compound_beacon_searchable_encryption_example import (
+    put_item_query_item_with_compound_beacon,
+)
+from ..cleanup import delete_branch_key
+from .searchable_encryption_test_utils import (
+    TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN,
+    TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME,
+    TEST_LOGICAL_KEYSTORE_NAME,
+    UNIT_INSPECTION_TEST_DDB_TABLE_NAME,
+)
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_compound_item_encrypt_decrypt():
+    """Test compound beacon searchable encryption example."""
+    # Create new branch key for test
+    key_id = keystore_create_key(
+        TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN
+    )
+
+    # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood
+    # our test fails due to eventual consistency issues.
+    time.sleep(5)
+
+    put_item_query_item_with_compound_beacon(
+        UNIT_INSPECTION_TEST_DDB_TABLE_NAME,
+        key_id,
+        TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN,
+        TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME,
+    )
+
+    # Cleanup Branch Key
+    delete_branch_key(key_id, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, None)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_virtual_beacon_searchable_encryption_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_virtual_beacon_searchable_encryption_example.py
new file mode 100644
index 000000000..2ecda0a48
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/searchable_encryption/test_virtual_beacon_searchable_encryption_example.py
@@ -0,0 +1,43 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test virtual beacon searchable encryption example."""
+import time
+
+import pytest
+
+from DynamoDBEncryption.src.create_keystore_key_example import keystore_create_key
+from DynamoDBEncryption.src.searchable_encryption.virtual_beacon_searchable_encryption_example import (
+    put_item_query_item_with_virtual_beacon,
+)
+
+from ..cleanup import delete_branch_key
+from .searchable_encryption_test_utils import (
+    SIMPLE_BEACON_TEST_DDB_TABLE_NAME,
+    TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN,
+    TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME,
+    TEST_LOGICAL_KEYSTORE_NAME,
+)
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_virtual_beacon_example():
+    """Test virtual beacon searchable encryption example."""
+    # Create new branch key for test
+    key_id = keystore_create_key(
+        TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN
+    )
+
+    # Key creation is eventually consistent, so wait 5 seconds to decrease the likelihood
+    # our test fails due to eventual consistency issues.
+    time.sleep(5)
+
+    put_item_query_item_with_virtual_beacon(
+        SIMPLE_BEACON_TEST_DDB_TABLE_NAME,
+        key_id,
+        TEST_BRANCH_KEY_WRAPPING_KMS_KEY_ARN,
+        TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME,
+    )
+
+    # Cleanup Branch Key
+    delete_branch_key(key_id, TEST_BRANCH_KEYSTORE_DDB_TABLE_NAME, None)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_key_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_key_example.py
new file mode 100644
index 000000000..10a3c2cad
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_key_example.py
@@ -0,0 +1,20 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test create key store key example."""
+import pytest
+
+from ..src.create_keystore_key_example import keystore_create_key
+from .cleanup import delete_branch_key
+from .test_utils import TEST_KEYSTORE_KMS_KEY_ID, TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_create_keystore_key_example():
+    """Test create_key_store_key_example."""
+    key_id = keystore_create_key(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID)
+
+    assert key_id is not None
+
+    # Cleanup Branch Key
+    delete_branch_key(key_id, TEST_KEYSTORE_NAME, None)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_table_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_table_example.py
new file mode 100644
index 000000000..fd030a0f7
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/test_create_keystore_table_example.py
@@ -0,0 +1,14 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test create key store table example."""
+import pytest
+
+from ..src.create_keystore_table_example import keystore_create_table
+from .test_utils import TEST_KEYSTORE_KMS_KEY_ID, TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_create_keystore_table_example():
+    """Test create_key_store_table_example."""
+    keystore_create_table(TEST_KEYSTORE_NAME, TEST_LOGICAL_KEYSTORE_NAME, TEST_KEYSTORE_KMS_KEY_ID)
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/test_get_encrypted_data_key_description_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/test_get_encrypted_data_key_description_example.py
new file mode 100644
index 000000000..2ed488ebe
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/test_get_encrypted_data_key_description_example.py
@@ -0,0 +1,27 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test get encrypted data key description example."""
+import pytest
+
+from ..src.get_encrypted_data_key_description_example import get_encrypted_data_key_description
+from .test_utils import (
+    TEST_DDB_TABLE_NAME,
+    TEST_KMS_KEY_ID,
+)
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_get_encrypted_data_key_description():
+    """Test get encrypted data key description example."""
+    get_encrypted_data_key_description(
+        TEST_DDB_TABLE_NAME,
+        "partition_key",
+        "BasicPutGetExample",
+        "sort_key",
+        "0",
+        "aws-kms",
+        TEST_KMS_KEY_ID,
+        None,
+        None,
+    )
diff --git a/Examples/runtimes/python/DynamoDBEncryption/test/test_scan_error_example.py b/Examples/runtimes/python/DynamoDBEncryption/test/test_scan_error_example.py
new file mode 100644
index 000000000..ee0c3710d
--- /dev/null
+++ b/Examples/runtimes/python/DynamoDBEncryption/test/test_scan_error_example.py
@@ -0,0 +1,14 @@
+# Copyright Amazon.com Inc. or its affiliates. All Rights Reserved.
+# SPDX-License-Identifier: Apache-2.0
+"""Test scan error example."""
+import pytest
+
+from ..src.scan_error_example import scan_error
+from .test_utils import TEST_DDB_TABLE_NAME, TEST_KMS_KEY_ID
+
+pytestmark = [pytest.mark.examples]
+
+
+def test_scan_error():
+    """Test scan_error."""
+    scan_error(TEST_KMS_KEY_ID, TEST_DDB_TABLE_NAME)