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
forloop - 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:
Or set it as default in ModelConfig:
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
- Atomic updates - Increment, append, and other atomic operations
- Conditions - All condition operators
- Indexes - Query by non-key attributes