Skip to content

Redis Set

๐Ÿงช Beta Feature
RedisSet is currently experimental. The API may change in future releases based on feedback.
Only JSON-serializable value types are supported: str, int, float, and bool.

RedisSet is an unordered, unique-member collection backed by a Redis SET. Items appear only once and have no defined order.

Unlike regular Redis types (RedisStr, RedisList, etc.), RedisSet stores its data in a separate Redis key โ€” not inline with the model's JSON. It is a pure Redis proxy: every mutation goes straight to Redis and the local mirror is never used to overwrite server state.

from pydantic import Field
from rapyer import AtomicRedisModel
from rapyer.types import RedisSet


class Article(AtomicRedisModel):
    title: str = "untitled"
    tags: RedisSet[str] = Field(default_factory=RedisSet)

Plain set[...] annotations are auto-converted to RedisSet as well:

class Article(AtomicRedisModel):
    tags: set[str] = Field(default_factory=set)  # becomes RedisSet[str]

Basic Usage

article = Article(title="Hello Redis")
await article.asave()

await article.tags.aadd("python")
await article.tags.aadd("redis")
await article.tags.aadd("python")  # idempotent โ€” set still has 2 members

await article.tags.acontains("python")  # True
await article.tags.asize()              # 2
await article.tags.amembers()           # {"python", "redis"}

Generic Type Support

RedisSet accepts any JSON-serializable type as its generic parameter:

class Numbers(AtomicRedisModel):
    values: RedisSet[int] = Field(default_factory=RedisSet)

class Flags(AtomicRedisModel):
    bits: RedisSet[bool] = Field(default_factory=RedisSet)

Async Operations

Operation Method Description
add await article.tags.aadd(value) Add a single member
add many await article.tags.aadd_many(values) Add multiple members in one command
remove await article.tags.aremove(value) Remove a member (returns bool)
pop await article.tags.apop() Remove and return a random member
clear await article.tags.aclear() Remove all members
contains await article.tags.acontains(value) Check membership
members await article.tags.amembers() Return all members as a Python set
size await article.tags.asize() Return the number of members

Set Algebra Across Models

aunion, aintersect, and adifference operate against other RedisSet fields and run entirely on the Redis side:

a = Article(title="A")
b = Article(title="B")
await a.asave()
await b.asave()
await a.tags.aadd_many(["alpha", "beta", "gamma"])
await b.tags.aadd_many(["gamma", "delta"])

await a.tags.aunion(b.tags)        # {"alpha", "beta", "gamma", "delta"}
await a.tags.aintersect(b.tags)    # {"gamma"}
await a.tags.adifference(b.tags)   # {"alpha", "beta"}

All three accept any number of other RedisSet operands.

Pipeline (Sync) Operations

Inside a pipeline context, the standard Python set mutators are batched into atomic Redis commands:

async with article.apipeline():
    article.tags.add("python")
    article.tags.update(["redis", "asyncio"])
    article.tags.discard("legacy")
    article.tags |= {"backend"}

Supported sync mutators:

  • add(value), update(*iterables)
  • remove(value), discard(value), clear()
  • difference_update, intersection_update, symmetric_difference_update
  • |=, &=, -=, ^=

No sync pop()

RedisSet does not support sync pop(). Use await tags.apop() for an atomic Redis-side pick.

remove inside a pipeline does not raise

Outside a pipeline, remove(value) matches Python's set.remove and raises KeyError for a missing member. Inside apipeline() the local mirror is not authoritative, so remove behaves like discard and silently queues SREM.

Optional Fields

RedisSet fields can be optional:

class Worker(AtomicRedisModel):
    name: str = "default"
    tags: Optional[RedisSet[str]] = None

Assign a value after init to start using it:

worker = Worker()
await worker.asave()

worker.tags = RedisSet()
await worker.tags.aadd("ready")

How It Works

RedisSet is a special field type โ€” it stores data in a separate Redis key derived from the parent model's key:

__rapyer_special__:{ModelName}:{pk}:tags

This means:

  • Save/delete are automatic โ€” asave() and adelete() on the parent model handle the set's Redis key and TTL.
  • TTL is inherited โ€” if the parent model has a TTL configured, the set's key gets the same expiration.
  • No inline state โ€” every async operation hits Redis directly.

Cannot use aupdate() with special fields

Special fields manage their own Redis storage and cannot be passed to aupdate(). Use the field's own methods instead.

# โŒ This raises UpdateAtomicModelError
await article.aupdate(tags={"x"})

# โœ… Use the field's methods directly
await article.tags.aadd("x")