You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
google-cloud-ndb: Expando dynamic property updates are not persisted by put() #18204
If I assign a dynamic property on an Expando and put(), the first write sticks. Changing that same property later and put() again looks fine (no error; in-process it even reads as the new value) but a refetch from datastore still has the old number.
Declared properties on the class (e.g. FloatProperty) update correctly. This also worked on the old App Engine ndb.
Expando.__setattr__ looks like the cause. If the name is already in _properties it calls super().__setattr__. For a class-level Property that hits the descriptor and updates _values. For a dynamic name there is no descriptor on the class, so it lands in __dict__ and put() serializes the stale _values.
Is this expected? I couldn't find it documented. The old runtime always went through _set_value for non-class names.
Your read of Expando.__setattr__ is right. Opened #18301.
The specific culprit is the third condition — it delegates to object.__setattr__ whenever the name is already in _properties, but delegating only works for names with a class level descriptor, since the descriptor is what writes _values. A dynamic name has none, so it goes to __dict__ and _values keeps the old value.
That's also why it read back correctly in process: __dict__ shadows __getattr__, so the attribute looked updated while put() was still serializing the original.
Activity
CaptainAni187 commentedon Sep 8, 2026
Your read of
Expando.__setattr__is right. Opened #18301.The specific culprit is the third condition — it delegates to
object.__setattr__whenever the name is already in_properties, but delegating only works for names with a class level descriptor, since the descriptor is what writes_values. A dynamic name has none, so it goes to__dict__and_valueskeeps the old value.That's also why it read back correctly in process:
__dict__shadows__getattr__, so the attribute looked updated whileput()was still serializing the original.Reproduces without Datastore:
The fix narrows that check to class level descriptors, matching what
__delattr__just below already does.