Custom Attributes

Attributes in PynamoDB are classes that are serialized to and from DynamoDB attributes. PynamoDB provides attribute classes for all DynamoDB data types, as defined in the DynamoDB documentation. Higher level attribute types (internally stored as a DynamoDB data types) can be defined with PynamoDB. Two such types are included with PynamoDB for convenience: JSONAttribute and UTCDateTimeAttribute.

Attribute Methods

All Attribute classes must define three methods, serialize, deserialize and get_value. The serialize method takes a Python value and converts it into a format that can be stored into DynamoDB. The get_value method reads the serialized value out of the DynamoDB record. This raw value is then passed to the deserialize method. The deserialize method then converts it back into its value in Python. Additionally, a class attribute called attr_type is required for PynamoDB to know which DynamoDB data type the attribute is stored as. The get_value method is provided to help when migrating from one attribute type to another, specifically with the BooleanAttribute type. If you’re writing your own attribute and the attr_type has not changed you can simply use the base Attribute implementation of get_value.

Writing your own attribute

You can write your own attribute class which defines the necessary methods like this:

from pynamodb.attributes import Attribute
from pynamodb.constants import BINARY

class CustomAttribute(Attribute):
    """
    A custom model attribute
    """

    # This tells PynamoDB that the attribute is stored in DynamoDB as a binary
    # attribute
    attr_type = BINARY

    def serialize(self, value):
        # convert the value to binary and return it

    def deserialize(self, value):
        # convert the value from binary back into whatever type you require

Custom Attribute Example

The example below shows how to write a custom attribute that will pickle a customized class. The attribute itself is stored in DynamoDB as a binary attribute. The pickle module is used to serialize and deserialize the attribute. In this example, it is not necessary to define attr_type because the PickleAttribute class is inheriting from BinaryAttribute which has already defined it.

import pickle
from pynamodb.attributes import BinaryAttribute, UnicodeAttribute
from pynamodb.models import Model


class Color(object):
    """
    This class is used to demonstrate the PickleAttribute below
    """
    def __init__(self, name):
        self.name = name

    def __str__(self):
        return "<Color: {}>".format(self.name)


class PickleAttribute(BinaryAttribute):
    """
    This class will serializer/deserialize any picklable Python object.
    The value will be stored as a binary attribute in DynamoDB.
    """
    def serialize(self, value):
        """
        The super class takes the binary string returned from pickle.dumps
        and encodes it for storage in DynamoDB
        """
        return super(PickleAttribute, self).serialize(pickle.dumps(value))

    def deserialize(self, value):
        return pickle.loads(super(PickleAttribute, self).deserialize(value))


class CustomAttributeModel(Model):
    """
    A model with a custom attribute
    """
    class Meta:
        host = 'http://localhost:8000'
        table_name = 'custom_attr'
        read_capacity_units = 1
        write_capacity_units = 1

    id = UnicodeAttribute(hash_key=True)
    obj = PickleAttribute()

Now we can use our custom attribute to round trip any object that can be pickled.

>>>instance = CustomAttributeModel()
>>>instance.obj = Color('red')
>>>instance.id = 'red'
>>>instance.save()

>>>instance = CustomAttributeModel.get('red')
>>>print(instance.obj)
<Color: red>

List Attributes

DynamoDB list attributes are simply lists of other attributes. DynamoDB asserts no requirements about the types embedded within the list. Creating an untyped list is done like so:

from pynamodb.attributes import ListAttribute, NumberAttribute, UnicodeAttribute

class GroceryList(Model):
    class Meta:
        table_name = 'GroceryListModel'

    store_name = UnicodeAttribute(hash_key=True)
    groceries = ListAttribute()

# Example usage:

GroceryList(store_name='Haight Street Market',
            groceries=['bread', 1, 'butter', 6, 'milk', 1])

PynamoDB can provide type safety if it is required. Currently PynamoDB does not allow type checks on anything other than subclasses of Attribute. We’re working on adding more generic type checking in a future version. When defining your model use the of= kwarg and pass in a class. PynamoDB will check that all items in the list are of the type you require.

from pynamodb.attributes import ListAttribute, NumberAttribute


class OfficeEmployeeMap(MapAttribute):
    office_employee_id = NumberAttribute()
    person = UnicodeAttribute()


class Office(Model):
    class Meta:
        table_name = 'OfficeModel'
    office_id = NumberAttribute(hash_key=True)
    employees = ListAttribute(of=OfficeEmployeeMap)

# Example usage:

emp1 = OfficeEmployeeMap(
    office_employee_id=123,
    person='justin'
)
emp2 = OfficeEmployeeMap(
    office_employee_id=125,
    person='lita'
)
emp4 = OfficeEmployeeMap(
    office_employee_id=126,
    person='garrett'
)

Office(
    office_id=3,
    employees=[emp1, emp2, emp3]
).save()  # persists

Office(
    office_id=3,
    employees=['justin', 'lita', 'garrett']
).save()  # raises ValueError

Map Attributes

DynamoDB map attributes are objects embedded inside of top level models. See the examples here. When implementing your own MapAttribute you can simply extend MapAttribute and ignore writing serialization code. These attributes can then be used inside of Model classes just like any other attribute.

from pynamodb.attributes import MapAttribute, UnicodeAttribute

class CarInfoMap(MapAttribute):
    make = UnicodeAttribute(null=False)
    model = UnicodeAttribute(null=True)

As with a model and its top-level attributes, a PynamoDB MapAttribute will ignore sub-attributes it does not know about during deserialization. As a result, if the item in DynamoDB contains sub-attributes not declared as properties of the corresponding MapAttribute, save() will cause those sub-attributes to be deleted.

DynamicMapAttribute is a subclass of MapAttribute which allows you to mix and match defined attributes and undefined attributes.

from pynamodb.attributes import DynamicMapAttribute, UnicodeAttribute

class CarInfo(DynamicMapAttribute):
    make = UnicodeAttribute(null=False)
    model = UnicodeAttribute(null=True)

car = CarInfo(make='Make-A', model='Model-A', year=1975)
other_car = CarInfo(make='Make-A', model='Model-A', year=1975, seats=3)