Skip to content

Conditions

Conditions let you add rules to save() and delete() operations. The operation only happens if the condition is true. If not, DynamoDB raises an error.

This is useful for:

  • Preventing overwrites (only save if item doesn't exist)
  • Optimistic locking (only update if version matches)
  • Business rules (only withdraw if balance is sufficient)

Key features

  • Use Python operators (==, >, <) on model attributes
  • Combine with & (AND), | (OR), ~ (NOT)
  • Functions like exists(), begins_with(), between()
  • Build conditions dynamically from user input

Getting started

Prevent overwriting existing items

The most common use case. Only save if the item doesn't exist yet:

"""Prevent overwriting existing items."""

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


class User(Model):
    model_config = ModelConfig(table="users")

    pk = StringAttribute(hash_key=True)
    email = StringAttribute()
    name = StringAttribute()
    age = NumberAttribute()


# Only save if the item doesn't exist yet
user = User(pk="USER#123", email="john@example.com", name="John", age=30)
user.save(condition=User.pk.does_not_exist())

# If USER#123 already exists, this raises ConditionCheckFailedError

Without this condition, save() would silently overwrite any existing item with the same key. With the condition, you get a ConditionCheckFailedError if the item already exists.

Safe delete

Only delete if certain conditions are met:

"""Safe delete - only delete if conditions are met."""

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


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

    pk = StringAttribute(hash_key=True)
    status = StringAttribute()
    total = NumberAttribute()


# Only delete if order is in "draft" status
order = Order.get(pk="ORDER#123")
order.delete(condition=Order.status == "draft")

# Can't delete orders that are already processed

This prevents accidental deletion of orders that are already being processed.

Advanced

Optimistic locking

When multiple processes might update the same item, use a version field to prevent lost updates:

"""Optimistic locking - only update if version matches."""

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


class Product(Model):
    model_config = ModelConfig(table="products")

    pk = StringAttribute(hash_key=True)
    name = StringAttribute()
    price = NumberAttribute()
    version = NumberAttribute()


# Get current product
product = Product.get(pk="PROD#123")
current_version = product.version

# Update with version check
product.price = 29.99
product.version = current_version + 1
product.save(condition=Product.version == current_version)

# If someone else updated the product, version won't match
# and ConditionCheckFailedError is raised

How it works:

  1. Read the item and note the current version
  2. Make your changes and increment the version
  3. Save with a condition that the version still matches
  4. If someone else updated it first, the condition fails

This is safer than "last write wins" because you know when conflicts happen.

Complex business rules

Combine multiple conditions for complex validation:

"""Complex business rules with combined conditions."""

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


class Account(Model):
    model_config = ModelConfig(table="accounts")

    pk = StringAttribute(hash_key=True)
    balance = NumberAttribute()
    status = StringAttribute()
    verified = BooleanAttribute()


# Only allow withdrawal if:
# - Account is active AND verified
# - Balance is sufficient
account = Account.get(pk="ACC#123")
withdrawal = 100

condition = (
    (Account.status == "active")
    & (Account.verified == True)  # noqa: E712
    & (Account.balance >= withdrawal)
)

account.balance = account.balance - withdrawal
account.save(condition=condition)

The withdrawal only happens if all three conditions are true. If any fails, the whole operation is rejected.

Dynamic filters

Build conditions at runtime based on user input:

"""Build conditions dynamically from user input."""

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


class Product(Model):
    model_config = ModelConfig(table="products")

    pk = StringAttribute(hash_key=True)
    category = StringAttribute()
    price = NumberAttribute()
    brand = StringAttribute()


def build_product_filter(
    category: str | None = None,
    max_price: float | None = None,
    brand: str | None = None,
):
    """Build filter from optional parameters."""
    conditions = []

    if category:
        conditions.append(Product.category == category)
    if max_price:
        conditions.append(Product.price <= max_price)
    if brand:
        conditions.append(Product.brand == brand)

    if len(conditions) == 0:
        return None
    if len(conditions) == 1:
        return conditions[0]

    return And(*conditions)


# User searches for electronics under $500
filter_cond = build_product_filter(category="electronics", max_price=500)

Use And() and Or() from pydynox.conditions when you have a list of conditions to combine.

Operators

Comparison operators

Operator Example Description
== User.status == "active" Equal
!= User.status != "deleted" Not equal
> User.age > 18 Greater than
>= User.age >= 21 Greater or equal
< User.age < 65 Less than
<= User.age <= 30 Less or equal

Combining operators

Operator Example Description
& (a > 1) & (b < 2) Both must be true
\| (a == 1) \| (a == 2) Either can be true
~ ~a.exists() Negates the condition

Warning

Always use parentheses when combining conditions. Python's operator precedence may not work as expected.

Function conditions

Function Example Description
exists() User.email.exists() Attribute exists
does_not_exist() User.pk.does_not_exist() Attribute doesn't exist
begins_with() User.sk.begins_with("ORDER#") String starts with prefix
contains() User.tags.contains("vip") List contains value
between() User.age.between(18, 65) Value in range (inclusive)
is_in() User.status.is_in("a", "b") Value in list

Nested attributes

Access nested map keys and list indexes:

# Map access
User.address["city"] == "NYC"

# List access
User.tags[0] == "premium"

# Deep nesting
User.metadata["preferences"]["theme"] == "dark"

Error handling

When a condition fails, DynamoDB raises ConditionCheckFailedError:

from pydynox.exceptions import ConditionCheckFailedError

try:
    user.save(condition=User.pk.does_not_exist())
except ConditionCheckFailedError:
    print("User already exists")

Type hints

Use Condition for type hints:

from pydynox import Condition

def apply_filter(cond: Condition) -> None:
    ...