Field projections
Fetch only the fields you need from DynamoDB.
Why use field projections?
When you query or scan, DynamoDB returns all attributes by default. With projections, you tell DynamoDB which fields to return.
Benefits:
- Less data over the network
- Faster deserialization
- Smaller objects in memory
Note
Projections reduce data transfer, not RCU cost. DynamoDB still reads the full item from disk. RCU is based on item size on disk, not what's returned.
Key features
- Fetch specific fields from query and scan
- Works with Model API and Client API
- Supports nested attributes with dot notation
- Handles reserved words automatically
Getting started
Model API
Use the fields parameter on query and scan:
import asyncio
from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
name = StringAttribute()
email = StringAttribute()
age = NumberAttribute()
address = StringAttribute()
async def main():
# Query with specific fields - only fetches name and email
async for user in User.query(partition_key="USER#123", fields=["name", "email"]):
print(user.name, user.email)
# user.age and user.address will be None
asyncio.run(main())
Works the same for scan:
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)
name = StringAttribute()
status = StringAttribute()
async def main():
# Scan with fields - useful for reports or exports
async for user in User.scan(fields=["pk", "status"]):
print(user.pk, user.status)
asyncio.run(main())
Client API
Use projection for get_item or projection_expression for query/scan:
"""Client API projection examples."""
import asyncio
from pydynox import DynamoDBClient
client = DynamoDBClient()
async def main():
# get_item with projection - pass a list of field names
item = await client.get_item(
"users",
{"pk": "USER#1"},
projection=["name", "email"],
)
# Returns only: {"pk": "USER#1", "name": "John", "email": "john@example.com"}
print(item)
# query with projection_expression
async for item in client.query(
"users",
key_condition_expression="pk = :pk",
projection_expression="#n, email",
expression_attribute_names={"#n": "name"},
expression_attribute_values={":pk": "USER#123"},
):
print(item.get("email"))
asyncio.run(main())
Advanced
Nested attributes
Access nested fields with dot notation:
"""Nested attribute projection example."""
import asyncio
from pydynox import DynamoDBClient
client = DynamoDBClient()
async def main():
# Nested attributes use dot notation
item = await client.get_item(
"users",
{"pk": "USER#1"},
projection=["name", "address.city", "address.zip"],
)
# Returns: {"pk": "USER#1", "name": "John", "address": {"city": "NYC", "zip": "10001"}}
print(item)
asyncio.run(main())
Reserved words
Reserved words like name and status are handled automatically. The library creates placeholders for you:
# "name" is a reserved word in DynamoDB
# This works without issues:
async for user in User.query(pk="USER#123", fields=["name", "status"]):
print(user.name)
Sync usage
Use sync_query and sync_scan for sync code:
for user in User.sync_query(pk="USER#123", fields=["name"]):
print(user.name)
for user in User.sync_scan(fields=["pk", "status"]):
print(user.status)
Parameters
Model.query / Model.scan
| Parameter | Type | Default | Description |
|---|---|---|---|
fields |
list[str] | None | List of field names to fetch |
client.get_item
| Parameter | Type | Default | Description |
|---|---|---|---|
projection |
list[str] | None | List of field names to fetch |
client.query / client.scan
| Parameter | Type | Default | Description |
|---|---|---|---|
projection_expression |
str | None | DynamoDB projection expression |
When to use
- Large items where you only need a few fields
- High-traffic queries where bandwidth matters
- Lambda functions where you want to minimize data transfer
- Reports or exports that need specific columns
Tip
Projections are most useful when your items are large (>1KB) and you only need a few fields. For small items, the savings are minimal.
Testing your code
Use the pydynox_memory_backend fixture to test projections without DynamoDB:
import pytest
from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute
class User(Model):
model_config = ModelConfig(table="users")
pk = StringAttribute(partition_key=True)
name = StringAttribute()
email = StringAttribute()
age = NumberAttribute()
@pytest.mark.asyncio
async def test_query_with_projection(pydynox_memory_backend):
"""Test query returns only projected fields."""
await User(pk="USER#1", name="Alice", email="alice@example.com", age=30).save()
await User(pk="USER#1", name="Bob", email="bob@example.com", age=25).save()
results = [u async for u in User.query(partition_key="USER#1", fields=["name"])]
assert len(results) == 2
for user in results:
assert user.name is not None
# Non-projected fields are None
assert user.email is None
assert user.age is None
@pytest.mark.asyncio
async def test_scan_with_projection(pydynox_memory_backend):
"""Test scan returns only projected fields."""
await User(pk="USER#1", name="Alice", email="alice@example.com", age=30).save()
results = [u async for u in User.scan(fields=["pk", "name"])]
assert len(results) == 1
assert results[0].pk == "USER#1"
assert results[0].name == "Alice"
assert results[0].email is None
See the Testing guide for more on pydynox_memory_backend.