Field aliases
Use readable Python names while storing short names in DynamoDB.
Why use aliases?
DynamoDB charges for storage. Attribute names are stored with every item. Long names add up fast.
Without aliases, you have two bad options:
- Use short names in code (
em,fn) - hard to read - Use long names in DynamoDB (
email,first_name) - costs more
Aliases give you both: readable code and cheap storage.
Key features
- Use
aliasparameter on any attribute - Automatic translation on save and load
- Works with conditions, updates, queries, and scans
- Works with indexes (GSI and LSI)
- Zero performance cost - translation happens in Python at setup time
Getting started
Add alias to any attribute. The alias is the name stored in DynamoDB.
from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute
# Use readable Python names, store short names in DynamoDB
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
sk = StringAttribute(sort_key=True)
email = StringAttribute(alias="em")
first_name = StringAttribute(alias="fn")
last_name = StringAttribute(alias="ln")
age = NumberAttribute(alias="a")
# Code is readable
user = User(
pk="USER#1",
sk="PROFILE",
email="john@example.com",
first_name="John",
last_name="Doe",
age=30,
)
# to_dict() uses alias names (what DynamoDB stores)
d = user.to_dict()
print(d)
# {"pk": "USER#1", "sk": "PROFILE", "em": "john@example.com", "fn": "John", "ln": "Doe", "a": 30}
assert d["em"] == "john@example.com"
assert d["fn"] == "John"
assert "email" not in d # Python name is NOT in the dict
The alias parameter is optional. Fields without it use the Python name as-is.
CRUD operations
All CRUD operations work with Python names. The alias translation is automatic.
import asyncio
from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute
class Product(Model):
model_config = ModelConfig(table="products")
pk = StringAttribute(partition_key=True)
product_name = StringAttribute(alias="pn")
price = NumberAttribute(alias="pr")
stock = NumberAttribute(alias="stk")
async def main():
# Save — Python names in code, short names in DynamoDB
laptop = Product(pk="PROD#1", product_name="Laptop", price=999, stock=10)
await laptop.save()
# Get — alias names are translated back to Python names
loaded = await Product.get(pk="PROD#1")
assert loaded is not None
print(f"Name: {loaded.product_name}") # "Laptop"
print(f"Price: {loaded.price}") # 999
# Update — use Python names
await loaded.update(price=899)
# Verify
updated = await Product.get(pk="PROD#1")
assert updated is not None
print(f"New price: {updated.price}") # 899
asyncio.run(main())
You never need to think about alias names in your code. Use Python names everywhere.
Conditions and updates
Conditions and atomic updates also use aliases automatically.
import asyncio
from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute
class Product(Model):
model_config = ModelConfig(table="products")
pk = StringAttribute(partition_key=True)
product_name = StringAttribute(alias="pn")
price = NumberAttribute(alias="pr")
stock = NumberAttribute(alias="stk")
async def main():
product = Product(pk="PROD#COND", product_name="Mouse", price=25, stock=100)
await product.save()
# Conditions use Python names — alias is handled automatically
await product.update(
stock=99,
condition=Product.stock > 0,
)
# DynamoDB sees: SET #n0 = :v0 WHERE #n1 > :v1
# Where #n0 = "stk", #n1 = "stk"
# exists / not_exists
await product.save(condition=Product.product_name.exists())
# begins_with
products = [
p
async for p in Product.scan(
filter_condition=Product.product_name.begins_with("Mo"),
)
]
print(f"Found {len(products)} products starting with 'Mo'")
asyncio.run(main())
When you write Product.stock > 0, pydynox translates stock to stk in the DynamoDB expression. You always use the Python name.
Key aliases
You can alias key attributes too (partition key and sort key):
class Event(Model):
model_config = ModelConfig(table="events")
pk = StringAttribute(partition_key=True, alias="p")
sk = StringAttribute(sort_key=True, alias="s")
event_type = StringAttribute(alias="et")
This saves bytes on every item since keys are always present.
Warning
If you alias key attributes, the DynamoDB table must use the alias names as key definitions. When using Model.create_table(), this is handled automatically.
Indexes
Aliases work with GSI and LSI definitions. The index key definitions use the alias name automatically.
from pydynox.indexes import GlobalSecondaryIndex
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
sk = StringAttribute(sort_key=True)
email = StringAttribute(alias="em")
email_index = GlobalSecondaryIndex(
index_name="email-index",
partition_key="email", # Use Python name here
)
When creating the table, the GSI key schema uses em (the alias). When querying, you use the Python name email.
How it works
At class definition time, pydynox builds two lookup dicts:
_py_to_dynamo- maps Python names to DynamoDB names (e.g.,{"email": "em"})_dynamo_to_py- maps DynamoDB names back to Python names (e.g.,{"em": "email"})
These dicts are used by:
to_dict()- translates Python to DynamoDB (on save)from_dict()- translates DynamoDB to Python (on load)- Conditions - uses alias in expression paths
- Atomic updates - uses alias in expression paths
- Projections - translates field names to aliases
Fields without an alias are not in these dicts. They use the Python name directly.
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
alias |
str | None | None | DynamoDB attribute name. If set, this name is used in DynamoDB instead of the Python name. |
Testing
Use MemoryBackend to test models with aliases. No DynamoDB needed.
from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute
from pydynox.testing import MemoryBackend
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
sk = StringAttribute(sort_key=True)
email = StringAttribute(alias="em")
age = NumberAttribute(alias="a")
@MemoryBackend()
def test_alias_roundtrip():
user = User(pk="USER#1", sk="PROFILE", email="test@example.com", age=25)
user.sync_save()
loaded = User.sync_get(pk="USER#1", sk="PROFILE")
assert loaded is not None
assert loaded.email == "test@example.com"
assert loaded.age == 25
@MemoryBackend()
def test_alias_to_dict():
user = User(pk="USER#1", sk="PROFILE", email="test@example.com", age=25)
d = user.to_dict()
# Alias names in the dict
assert d["em"] == "test@example.com"
assert d["a"] == 25
# Python names NOT in the dict
assert "email" not in d
assert "age" not in d
test_alias_roundtrip()
test_alias_to_dict()
print("All alias tests passed!")
Tips
- Pick short but meaningful aliases:
emfor email,fnfor first_name - Be consistent: use the same alias pattern across models
- Document your aliases in a comment or table for your team
- Aliases are most useful for high-volume tables where storage costs matter
Next steps
- Attributes - All attribute types and parameters
- Conditions - Filter and condition expressions
- Indexes - GSI and LSI with aliases
- Size calculator - See how much you save