Smart save
By default, pydynox tracks which fields changed and only sends those to DynamoDB. This saves WCU (Write Capacity Units) when updating large items.
Why this matters
DynamoDB charges for every byte you write. If you have a 10KB item and change one field, sending all 10KB wastes 9 WCU.
pydynox optimizes this. It only sends the changed field. One field = 1 WCU instead of 10.
This adds up fast:
- 1 million updates/month on 4KB items
- Without smart save: 4M WCU = $50/month
- With smart save (200B average change): 200K WCU = $2.50/month
- Savings: $47.50/month
How it works
When you load an item from DynamoDB, pydynox stores a snapshot of the original values. When you call save(), it compares current values with the original and only sends the changed fields using UpdateItem.
"""Basic smart save example - only changed fields are sent to DynamoDB."""
import asyncio
from pydynox import Model, ModelConfig
from pydynox.attributes import StringAttribute
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
sk = StringAttribute(sort_key=True)
name = StringAttribute()
email = StringAttribute()
bio = StringAttribute()
async def main():
# Load a 4KB item with 20 fields
user = await User.get(pk="USER#1", sk="PROFILE")
if user:
# Change one field
user.name = "New Name"
# Only sends 'name' to DynamoDB (not all 4KB)
await user.save()
if __name__ == "__main__":
asyncio.run(main())
Cost savings
DynamoDB charges 1 WCU per 1KB written.
| Item size | Fields changed | Without smart save | With smart save | Savings |
|---|---|---|---|---|
| 1KB | 1 field (50B) | 1 WCU | 1 WCU | 0% |
| 4KB | 1 field (50B) | 4 WCU | 1 WCU | 75% |
| 10KB | 1 field (50B) | 10 WCU | 1 WCU | 90% |
| 4KB | 2KB of fields | 4 WCU | 2 WCU | 50% |
Measure WCU consumed
Run this example to see the WCU difference between smart save and full replace:
"""Compare WCU consumed: smart save vs full replace."""
import asyncio
from pydynox import Model, ModelConfig
from pydynox.attributes import StringAttribute
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
sk = StringAttribute(sort_key=True)
name = StringAttribute()
email = StringAttribute()
bio = StringAttribute()
address = StringAttribute()
phone = StringAttribute()
company = StringAttribute()
async def main():
# Create a large item (~2KB)
user = User(
pk="USER#wcu",
sk="PROFILE",
name="John Doe",
email="john@example.com",
bio="A" * 1000, # 1KB of data
address="123 Main St, City, Country",
phone="+1-555-0123",
company="Acme Corp",
)
await user.save()
# Reload to enable change tracking
user = await User.get(pk="USER#wcu", sk="PROFILE")
if not user:
return
# Test 1: Smart save (only changed field)
User.reset_metrics()
user.name = "Jane Doe"
await user.save()
smart_metrics = User.get_total_metrics()
# Test 2: Full replace (all fields)
User.reset_metrics()
user.name = "Bob Smith"
await user.save(full_replace=True)
full_metrics = User.get_total_metrics()
# Results
print("=== WCU Comparison ===")
print(f"Smart save (UpdateItem): {smart_metrics.total_wcu} WCU")
print(f"Full replace (PutItem): {full_metrics.total_wcu} WCU")
print(f"Savings: {full_metrics.total_wcu - smart_metrics.total_wcu} WCU")
# Cleanup
await user.delete()
if __name__ == "__main__":
asyncio.run(main())
Example output:
Check if item changed
Use is_dirty and changed_fields to see what changed:
"""Check if item changed using is_dirty and changed_fields."""
import asyncio
from pydynox import Model, ModelConfig
from pydynox.attributes import StringAttribute
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
sk = StringAttribute(sort_key=True)
name = StringAttribute()
email = StringAttribute()
async def main():
user = await User.get(pk="USER#1", sk="PROFILE")
if user:
print(user.is_dirty) # False
user.name = "New Name"
print(user.is_dirty) # True
print(user.changed_fields) # ["name"]
user.email = "new@example.com"
print(user.changed_fields) # ["name", "email"]
if __name__ == "__main__":
asyncio.run(main())
Force full replace
If you need to replace the entire item (using PutItem instead of UpdateItem), use full_replace=True:
"""Force full replace using PutItem instead of UpdateItem."""
import asyncio
from pydynox import Model, ModelConfig
from pydynox.attributes import StringAttribute
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
sk = StringAttribute(sort_key=True)
name = StringAttribute()
async def main():
user = await User.get(pk="USER#1", sk="PROFILE")
if user:
user.name = "New Name"
# Forces PutItem with all fields
await user.save(full_replace=True)
if __name__ == "__main__":
asyncio.run(main())
Use this when:
- You want to remove fields that are not in the model
- You need
PutItembehavior for some reason
New items
New items (not loaded from DynamoDB) always use PutItem:
"""New items always use PutItem, then smart save kicks in."""
import asyncio
from pydynox import Model, ModelConfig
from pydynox.attributes import StringAttribute
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
sk = StringAttribute(sort_key=True)
name = StringAttribute()
async def main():
# New item - uses PutItem
user = User(pk="USER#new", sk="PROFILE", name="John")
await user.save() # PutItem
# After save, tracking is enabled
user.name = "Jane"
await user.save() # UpdateItem (smart save)
# Cleanup
await user.delete()
if __name__ == "__main__":
asyncio.run(main())
With conditions
Smart save works with conditions:
"""Smart save works with conditions."""
import asyncio
from pydynox import Model, ModelConfig
from pydynox.attributes import StringAttribute
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
sk = StringAttribute(sort_key=True)
status = StringAttribute()
async def main():
user = await User.get(pk="USER#1", sk="PROFILE")
if user:
user.status = "active"
# UpdateItem with condition
try:
await user.save(condition=User.status == "pending")
except Exception:
# Condition failed - status was not "pending"
pass
if __name__ == "__main__":
asyncio.run(main())
With optimistic locking
Smart save works with version attributes:
"""Smart save works with optimistic locking (version attribute)."""
import asyncio
from pydynox import Model, ModelConfig
from pydynox.attributes import StringAttribute, VersionAttribute
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
sk = StringAttribute(sort_key=True)
name = StringAttribute()
version = VersionAttribute()
async def main():
user = await User.get(pk="USER#1", sk="PROFILE")
if user:
user.name = "New Name"
# UpdateItem with version check
await user.save()
if __name__ == "__main__":
asyncio.run(main())