Skip to content

Query

Query items from DynamoDB using typed conditions. Returns model instances with full type hints.

Tip

For large result sets, you might want to use as_dict=True. See as_dict.

Key features

  • Type-safe queries with model attributes
  • Range key conditions (begins_with, between, comparisons)
  • Filter conditions on any attribute
  • Automatic pagination
  • Ascending/descending sort order
  • Async support

Getting started

Basic query

Use Model.query() to fetch items by hash key:

from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute


class Order(Model):
    model_config = ModelConfig(table="orders")
    pk = StringAttribute(hash_key=True)
    sk = StringAttribute(range_key=True)
    total = NumberAttribute()
    status = StringAttribute()


# Query all orders for a customer
for order in Order.query(hash_key="CUSTOMER#123"):
    print(f"Order: {order.sk}, Total: {order.total}")

# Get first result only
first_order = Order.query(hash_key="CUSTOMER#123").first()
if first_order:
    print(f"First order: {first_order.sk}")

# Collect all results into a list
orders = list(Order.query(hash_key="CUSTOMER#123"))
print(f"Found {len(orders)} orders")

The query returns a ModelQueryResult that you can:

  • Iterate with for loop
  • Get first result with .first()
  • Collect all with list()

Range key conditions

Filter by sort key using attribute conditions:

from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute


class Order(Model):
    model_config = ModelConfig(table="orders")
    pk = StringAttribute(hash_key=True)
    sk = StringAttribute(range_key=True)
    total = NumberAttribute()
    status = StringAttribute()


# Query orders that start with "ORDER#"
for order in Order.query(
    hash_key="CUSTOMER#123",
    range_key_condition=Order.sk.begins_with("ORDER#"),
):
    print(f"Order: {order.sk}")

# Query orders between two sort keys
for order in Order.query(
    hash_key="CUSTOMER#123",
    range_key_condition=Order.sk.between("ORDER#001", "ORDER#010"),
):
    print(f"Order: {order.sk}")

# Query orders greater than a sort key
for order in Order.query(
    hash_key="CUSTOMER#123",
    range_key_condition=Order.sk > "ORDER#005",
):
    print(f"Order: {order.sk}")

Available range key conditions:

Condition Example Description
begins_with Order.sk.begins_with("ORDER#") Sort key starts with prefix
between Order.sk.between("A", "Z") Sort key in range
= Order.sk == "ORDER#001" Exact match
< Order.sk < "ORDER#100" Less than
<= Order.sk <= "ORDER#100" Less than or equal
> Order.sk > "ORDER#001" Greater than
>= Order.sk >= "ORDER#001" Greater than or equal

Tip

Range key conditions are efficient. DynamoDB uses them to limit the items it reads.

Filter conditions

Filter results by any attribute:

from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute


class Order(Model):
    model_config = ModelConfig(table="orders")
    pk = StringAttribute(hash_key=True)
    sk = StringAttribute(range_key=True)
    total = NumberAttribute()
    status = StringAttribute()


# Filter by status
for order in Order.query(
    hash_key="CUSTOMER#123",
    filter_condition=Order.status == "shipped",
):
    print(f"Shipped order: {order.sk}")

# Filter by total amount
for order in Order.query(
    hash_key="CUSTOMER#123",
    filter_condition=Order.total >= 100,
):
    print(f"Large order: {order.sk}, Total: {order.total}")

# Combine multiple filters with & (AND)
for order in Order.query(
    hash_key="CUSTOMER#123",
    filter_condition=(Order.status == "shipped") & (Order.total > 50),
):
    print(f"Shipped large order: {order.sk}")

# Combine filters with | (OR)
for order in Order.query(
    hash_key="CUSTOMER#123",
    filter_condition=(Order.status == "shipped") | (Order.status == "delivered"),
):
    print(f"Completed order: {order.sk}")

Warning

Filter conditions are applied after DynamoDB reads the items. You still pay for the read capacity of filtered-out items.

Sorting and limit

Control sort order and page size:

from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute


class Order(Model):
    model_config = ModelConfig(table="orders")
    pk = StringAttribute(hash_key=True)
    sk = StringAttribute(range_key=True)
    total = NumberAttribute()
    status = StringAttribute()


# Ascending order (default)
for order in Order.query(
    hash_key="CUSTOMER#123",
    scan_index_forward=True,
):
    print(f"Order: {order.sk}")

# Descending order
for order in Order.query(
    hash_key="CUSTOMER#123",
    scan_index_forward=False,
):
    print(f"Order: {order.sk}")

# Get the 5 most recent orders (descending)
recent_orders = list(
    Order.query(
        hash_key="CUSTOMER#123",
        scan_index_forward=False,
        limit=5,
    )
)

for order in recent_orders:
    print(f"Recent order: {order.sk}")
Parameter Default Description
scan_index_forward True True = ascending, False = descending
limit None Items per page (iterator fetches all pages)

Advanced

Pagination

By default, the iterator fetches all pages automatically. For manual control:

from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute


class Order(Model):
    model_config = ModelConfig(table="orders")
    pk = StringAttribute(hash_key=True)
    sk = StringAttribute(range_key=True)
    total = NumberAttribute()
    status = StringAttribute()


# Automatic pagination - iterator fetches all pages
for order in Order.query(hash_key="CUSTOMER#123"):
    print(f"Order: {order.sk}")

# Manual pagination - control page size
result = Order.query(hash_key="CUSTOMER#123", limit=10)

# Process first page
page_count = 0
for order in result:
    print(f"Order: {order.sk}")
    page_count += 1
    if page_count >= 10:
        break

# Check if there are more pages
if result.last_evaluated_key:
    print("More pages available")

    # Fetch next page
    next_result = Order.query(
        hash_key="CUSTOMER#123",
        limit=10,
        last_evaluated_key=result.last_evaluated_key,
    )
    for order in next_result:
        print(f"Next page order: {order.sk}")

Use last_evaluated_key to:

  • Implement "load more" buttons
  • Process large datasets in batches
  • Resume interrupted queries

Consistent reads

For strongly consistent reads:

orders = list(
    Order.query(
        hash_key="CUSTOMER#123",
        consistent_read=True,
    )
)

Or set it as default in ModelConfig:

class Order(Model):
    model_config = ModelConfig(table="orders", consistent_read=True)

Metrics

Access query metrics after iteration:

result = Order.query(hash_key="CUSTOMER#123")
orders = list(result)

print(f"Duration: {result.metrics.duration_ms}ms")
print(f"RCU consumed: {result.metrics.consumed_rcu}")
print(f"Items returned: {result.metrics.items_count}")

Async queries

Use async_query() for async code:

import asyncio

from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute


class Order(Model):
    model_config = ModelConfig(table="orders")
    pk = StringAttribute(hash_key=True)
    sk = StringAttribute(range_key=True)
    total = NumberAttribute()
    status = StringAttribute()


async def main():
    # Async iteration
    async for order in Order.async_query(hash_key="CUSTOMER#123"):
        print(f"Order: {order.sk}")

    # Get first result
    first = await Order.async_query(hash_key="CUSTOMER#123").first()
    if first:
        print(f"First order: {first.sk}")

    # Collect all results
    orders = [order async for order in Order.async_query(hash_key="CUSTOMER#123")]
    print(f"Found {len(orders)} orders")

    # With conditions
    shipped = [
        order
        async for order in Order.async_query(
            hash_key="CUSTOMER#123",
            filter_condition=Order.status == "shipped",
        )
    ]
    print(f"Found {len(shipped)} shipped orders")


asyncio.run(main())

Return dicts instead of models

By default, query returns Model instances. Each item from DynamoDB is converted to a Python object with all the Model methods and hooks.

This conversion has a cost. Python object creation is slow compared to Rust. For queries that return many items (hundreds or thousands), this becomes a bottleneck.

Use as_dict=True to skip Model instantiation and get plain dicts:

"""Query returning dicts instead of Model instances."""

from pydynox import Model, ModelConfig
from pydynox.attributes import NumberAttribute, StringAttribute


class Order(Model):
    model_config = ModelConfig(table="orders")
    pk = StringAttribute(hash_key=True)
    sk = StringAttribute(range_key=True)
    total = NumberAttribute()
    status = StringAttribute()


# Return dicts instead of Model instances
for order in Order.query(hash_key="CUSTOMER#123", as_dict=True):
    # order is a plain dict, not an Order instance
    print(order.get("sk"), order.get("total"))

# Useful for read-only operations where you don't need Model methods
orders = list(Order.query(hash_key="CUSTOMER#123", as_dict=True))
print(f"Found {len(orders)} orders as dicts")

When to use as_dict=True:

  • Read-only operations where you don't need .save(), .delete(), or hooks
  • Queries returning many items (100+)
  • Performance-critical code paths
  • Data export or transformation pipelines

Trade-offs:

Model instances as_dict=True
Speed Slower (Python object creation) Faster (plain dicts)
Methods .save(), .delete(), .update() None
Hooks after_load runs No hooks
Type hints Full IDE support Dict access
Validation Attribute types enforced Raw DynamoDB types

Why this happens

This is how Python works. Creating class instances is expensive. Rust handles the DynamoDB call and deserialization fast, but Python must create each Model object. There's no way around this in Python itself.

Query parameters

Parameter Type Default Description
hash_key Any Required Hash key value
range_key_condition Condition None Condition on sort key
filter_condition Condition None Filter on any attribute
limit int None Items per page
scan_index_forward bool True Sort order
consistent_read bool None Strongly consistent read
last_evaluated_key dict None Start key for pagination
as_dict bool False Return dicts instead of Model instances

Query vs GSI query

Use Model.query() when querying by the table's hash key.

Use GSI query when querying by a different attribute:

# Table query - by pk
for order in Order.query(hash_key="CUSTOMER#123"):
    print(order.sk)

# GSI query - by status
for order in Order.status_index.query(status="shipped"):
    print(order.pk)

Next steps