Skip to content

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.