Writing Property Subclasses Stay organized with collections Save and categorize content based on your preferences.
TheProperty class is designed to be subclassed.However, it is normally easier to subclass an existingProperty subclass.
All specialProperty attributes,even those considered 'public',have names starting with an underscore.This is becauseStructuredPropertyuses the non-underscore attribute namespace to refer to nestedProperty names; this is essential for specifying queries onsubproperties.
TheProperty class and its predefined subclasses allowsubclassing using composable (or stackable) validation andconversion APIs. These require some terminology definitions:
- Auser value is a value such as would be set and accessed by the application code using standard attributes on the entity.
- Abase value is a value such as would be serialized to and deserialized from the Datastore.
AProperty subclass that implements a specifictransformation between user values and serializable values shouldimplement two methods,_to_base_type() and_from_base_type().These shouldnot call theirsuper() method.This is what is meant by composable (or stackable) APIs.
The API supportsstacking classes with ever more sophisticateduser-base conversions: the user-to-base conversiongoes from more sophisticated to less sophisticated, while thebase-to-user conversion goes from less sophisticated to moresophisticated. For example, see the relationship betweenBlobProperty,TextProperty,andStringProperty.For example,TextProperty inherits fromBlobProperty; its code is pretty simple because itinherits most of the behavior it needs.
In addition to_to_base_type() and_from_base_type(), the_validate() method is also a composable API.
The validation API distinguishes betweenlax andstrict uservalues. The set of lax values is a superset of the set of strictvalues. The_validate() method takes a lax value and if necessaryconverts it to a strict value. This means that when setting theproperty value, lax values are accepted, while when getting theproperty value, only strict values will be returned. If noconversion is needed,_validate() may return None. If the argumentis outside the set of accepted lax values,_validate() should raisean exception, preferablyTypeError ordatastore_errors.BadValueError.
The_validate(),_to_base_type(),and_from_base_type() donot need to handle:
None: They will not be called withNone(and if they return None, this means that the value does not need conversion).- Repeated values: The infrastructure takes care of calling
_from_base_type()or_to_base_type()for each list item in a repeated value. - Distinguishing user values from base values: The infrastructure handles this by calling the composable APIs.
- Comparisons: The comparison operations call
_to_base_type()on their operand. - Distinguishing between user and base values: the infrastructure guarantees that
_from_base_type()will be called with an (unwrapped) base value, and that_to_base_type()will be called with a user value.
For example, suppose you need to store really long integers.The standardIntegerProperty only supports (signed)64-bit integers.Your property might store a longer integer as a string; it would begood to have the property class handle the conversion.An application using your property class might look something like
fromdatetimeimportdateimportmy_models...classMyModel(ndb.Model):name=ndb.StringProperty()abc=LongIntegerProperty(default=0)xyz=LongIntegerProperty(repeated=True)...# Create an entity and write it to the Datastore.entity=my_models.MyModel(name='booh',xyz=[10**100,6**666])assertentity.abc==0key=entity.put()...# Read an entity back from the Datastore and update it.entity=key.get()entity.abc+=1entity.xyz.append(entity.abc//3)entity.put()...# Query for a MyModel entity whose xyz contains 6**666.# (NOTE: using ordering operations don't work, but == does.)results=my_models.MyModel.query(my_models.MyModel.xyz==6**666).fetch(10)This looks simple and straightforward. It also demonstrates the use of some standard property options (default, repeated); as the author ofLongIntegerProperty, you will be glad to hear you don't have to write any "boilerplate" to get those working. It's easier to define a subclass of another property, for example:
classLongIntegerProperty(ndb.StringProperty):def_validate(self,value):ifnotisinstance(value,(int,long)):raiseTypeError('expected an integer, got%s'%repr(value))def_to_base_type(self,value):returnstr(value)# Doesn't matter if it's an int or a longdef_from_base_type(self,value):returnlong(value)# Always return a longWhen you set a property value on an entity, e.g.ent.abc = 42, your_validate() method is called, and (if it doesn't raise an exception) the value is stored on the entity. When you write the entity to the Datastore, your_to_base_type() method is called, converting the value to the string. Then that value is serialized by the base class,StringProperty. The inverse chain of events happens when the entity is read back from the Datastore. TheStringProperty andProperty classes together take care of the other details, such as serializing the and deserializing the string, setting the default, and handling repeated property values.
In this example, supporting inequalities (i.e. queries using <, <=, >, >=) requires more work. The following example implementation imposes a maximum size of integer and stores values as fixed-length strings:
classBoundedLongIntegerProperty(ndb.StringProperty):def__init__(self,bits,**kwds):assertisinstance(bits,int)assertbits >0andbits%4==0# Make it simple to use hexsuper(BoundedLongIntegerProperty,self).__init__(**kwds)self._bits=bitsdef_validate(self,value):assert-(2**(self._bits-1)) <=value <2**(self._bits-1)def_to_base_type(self,value):# convert from signed -> unsignedifvalue <0:value+=2**self._bitsassert0 <=value <2**self._bits# Return number as a zero-padded hex string with correct number of# digits:return'%0*x'%(self._bits//4,value)def_from_base_type(self,value):value=int(value,16)ifvalue >=2**(self._bits-1):value-=2**self._bitsreturnvalueThis can be used in the same way asLongIntegerProperty except that you must pass the number of bits to the property constructor, e.g.BoundedLongIntegerProperty(1024).
You can subclass other property types in similar ways.
This approach also works for storing structured data. Suppose you have aFuzzyDate Python class that represents a date range; it uses fieldsfirst andlast to store the date range's beginning and end:
fromdatetimeimportdate...classFuzzyDate(object):def__init__(self,first,last=None):assertisinstance(first,date)assertlastisNoneorisinstance(last,date)self.first=firstself.last=lastorfirstYou can create aFuzzyDateProperty that derives fromStructuredProperty. Unfortunately, the latter doesn't work with plain old Python classes; it needs aModel subclass. So define a Model subclass as an intermediate representation;
classFuzzyDateModel(ndb.Model):first=ndb.DateProperty()last=ndb.DateProperty()Next, construct a subclass ofStructuredPropertythat hardcodes the modelclass argument to beFuzzyDateModel,and defines_to_base_type() and_from_base_type()methods to convert betweenFuzzyDate andFuzzyDateModel:
classFuzzyDateProperty(ndb.StructuredProperty):def__init__(self,**kwds):super(FuzzyDateProperty,self).__init__(FuzzyDateModel,**kwds)def_validate(self,value):assertisinstance(value,FuzzyDate)def_to_base_type(self,value):returnFuzzyDateModel(first=value.first,last=value.last)def_from_base_type(self,value):returnFuzzyDate(value.first,value.last)An application might use this class like so:
classHistoricPerson(ndb.Model):name=ndb.StringProperty()birth=FuzzyDateProperty()death=FuzzyDateProperty()# Parallel lists:event_dates=FuzzyDateProperty(repeated=True)event_names=ndb.StringProperty(repeated=True)...columbus=my_models.HistoricPerson(name='Christopher Columbus',birth=my_models.FuzzyDate(date(1451,8,22),date(1451,10,31)),death=my_models.FuzzyDate(date(1506,5,20)),event_dates=[my_models.FuzzyDate(date(1492,1,1),date(1492,12,31))],event_names=['Discovery of America'])columbus.put()# Query for historic people born no later than 1451.results=my_models.HistoricPerson.query(my_models.HistoricPerson.birth.last <=date(1451,12,31)).fetch()Suppose you want to accept plaindate objects inaddition toFuzzyDate objects as the values forFuzzyDateProperty. To do this, modify the_validate()method as follows:
def_validate(self,value):ifisinstance(value,date):returnFuzzyDate(value)# Must return the converted value!# Otherwise, return None and leave validation to the base classYou could instead subclassFuzzyDateProperty as follows(assumingFuzzyDateProperty._validate() is as shown above).
classMaybeFuzzyDateProperty(FuzzyDateProperty):def_validate(self,value):ifisinstance(value,date):returnFuzzyDate(value)# Must return the converted value!# Otherwise, return None and leave validation to the base classWhen you assign a value to aMaybeFuzzyDateProperty field, bothMaybeFuzzyDateProperty._validate() andFuzzyDateProperty._validate() are invoked, in that order. The same applies to_to_base_type() and_from_base_type(): the methods in in superclass and subclass are implicitly combined. (Don't usesuper to control inherited behavior for this. For these three methods, the interaction is subtle andsuper doesn't do what you want.)
Except as otherwise noted, the content of this page is licensed under theCreative Commons Attribution 4.0 License, and code samples are licensed under theApache 2.0 License. For details, see theGoogle Developers Site Policies. Java is a registered trademark of Oracle and/or its affiliates.
Last updated 2025-12-15 UTC.