While GAE offers a pretty good selection of default properties to go with your models, there are pretty amazing things you can do with custom properties. In this post, we'll take a look at how to implement a custom property for storing Python's Decimal type.

When creating a new custom property, you first have to decide on the base property you will subclass. When deciding what base to use, consider things like whether you need the property to be indexed, or whether you want to do comparison lookups (e.g., >=). For this example, we will use a ndb.IntegerProperty as base, since we want to be able to compare values in our queries, not just store and retrieve them.


from google.appengine.ext import ndb


class DecimalProperty(ndb.IntegerProperty):
pass

There are three things you need to do with your values. Validation goes without saying. You also need to decide how you will convert the value to one that the base property understands (int in this case), and how to convert from the base value to our custom value (Decimal in our case). Let's take a look at each step.

Custom validation

As I mentioned earlier, this is going to be very simple validation. If the Decimal class raises an exception, validation will fail. You can use a proper validator that checks the incoming value and raises an appropriate exception (e.g., BadValueError), but I'll leave that as an exercise for you.


from decimal import Decimal

class DecimalProperty(ndb.IntegerProperty):

def _validate(self, value):
return Decimal(value)

Conversion to base value

To convert a value to its base type, you need to implement a _to_base_type method. Our implementation will convert the value to an integer using the int() function.


class DecimalProperty(ndb.IntegerProperty):

def _validate(self, value):
return Decimal(value)

def _to_base_type(self, value):
return int(value)

Note that value in this context is the Decimal value.

Conversion from base value

The final step is to define _from_base_type method, which is called when the data is retrieved from the datastore. We will just convert it to Decimal.


class DecimalProperty(ndb.IntegerProperty):

def _validate(self, value):
return Decimal(value)

def _to_base_type(self, value):
return int(value)

def _from_base_type(self, value):
return Decimal(value)

What about precision?

If you've been paying attention so far, you'd notice that this implementation suffers from one big issue. Since we're converting everything to integer, the values are rounded in a very bad way, and the property won't take into account any floating points. To correct this, we will first modify the property to accept precision argument with a sane default (say, 2 digits after floating point).


class DecimalProperty(ndb.IntegerProperty):

float_prec = 2

def __init__(self, float_prec=None, **kwargs):
if float_prec is not None:
self.float_prec = float_prec
super(DecimalProperty, self).__init__(**kwargs)

def _validate(self, value):
return Decimal(value)

def _to_base_type(self, value):
return int(value)

def _from_base_type(self, value):
return Decimal(value)

Now with this parameter, we can adjust the precision of the actual values stored in the datastore:


class DecimalProperty(ndb.IntegerProperty):

float_prec = 2

def __init__(self, float_prec=None, **kwargs):
if float_prec is not None:
self.float_prec = float_prec
super(DecimalProperty, self).__init__(**kwargs)

def _validate(self, value):
return Decimal(value)

def _to_base_type(self, value):
return int(round(value * (10 ** self.float_prec)))

def _from_base_type(self, value):
return Decimal(value) / (10 ** self.float_prec)

Conclusion

With this we have a complete working implementation (save for any typos that I might have made) that will store and retrieve our Decimal values. Using ndb.IntegerProperty as the base class also has other benefits. The way NDB works, we get all the comparison operators for free, and they work with our scaled precision without a problem. With a model like this:


class MyModel(ndb.Model):
dec = DecimalProperty()

you can do the usual:


MyModel.query(MyModel.dec>='12.4')
MyModel.query(MyModel.dec=='0.4')

The only caveat is that you need to specify a bigger precision if you expect lots of digits after the floating point since, unlike the Decimal type, this implementation does not support arbitrary precision. If comparison operators aren't needed, but you do need arbitrary precision, you can base the implementation on ndb.StringProperty instead. The == operator should work fine even in that case, and you won't have to worry about precision.

You can read more about writing custom properties in the Google AppEngine documentation.