Repository files navigation

Beanis - Redis ODM for Humans

Beanis

PyPI versionDownloadsPython versionsLicenseGitHub stars
TestsCoverageCode style: blackPydanticRedis

Stop writing boilerplate Redis code. Focus on your application logic.

Beanis is an async Python ODM (Object-Document Mapper) for Redis that gives you Pydantic models, type safety, and a clean API - while staying fast and working with vanilla Redis.

Why Beanis?

The Problem with Vanilla Redis

Manual serialization - You write json.dumps() and json.loads() everywhere ❌ Type conversions - Strings from Redis need manual float(), int() conversions ❌ Key management - You track "Product:123", "all:Product" keys manually ❌ No validation - Bad data silently corrupts your Redis database ❌ Boilerplate code - 15-20 lines for simple CRUD operations

The Solution: Beanis

Automatic serialization - Nested objects, lists, custom types - all handled ✅ Type safety - Full Pydantic validation + IDE autocomplete ✅ Smart key management - Focus on your data, not Redis internals ✅ Data validation - Catch errors before they hit Redis ✅ Write 70% less code - 5-7 lines for the same operations

AND it's fast: Only 8% overhead vs vanilla Redis

Who Should Use Beanis?

✅ You're building a production app that needs Redis but not the boilerplate ✅ You want type safety and validation without sacrificing performance ✅ You're using vanilla Redis (no RedisJSON/RediSearch modules) ✅ You like Beanie's MongoDB API and want the same for Redis ✅ You're storing complex data (nested objects, NumPy arrays, etc.)

When NOT to Use Beanis?

❌ You need every microsecond of performance (use raw redis-py) ❌ You need RedisJSON/RediSearch features (use Redis OM) ❌ You're only storing simple key-value pairs (use raw redis-py)

Show Me The Code

Basic CRUD Operation

Vanilla Redis (20 lines)Beanis (7 lines)
importjsonimporttimefromredis.asyncioimportRedisredis=Redis(decode_responses=True)
# Insertproduct_data= {
"name": "Tony's Chocolonely",
"price": "5.95",
"category": json.dumps({
"name": "Chocolate",
"description": "Roasted cacao"
})
}
awaitredis.hset("Product:prod_123",
mapping=product_data)
awaitredis.zadd("all:Product",
{"prod_123": time.time()})
# Retrieveraw=awaitredis.hgetall("Product:prod_123")
product= {
"name": raw["name"],
"price": float(raw["price"]),
"category": json.loads(raw["category"])
}
frombeanisimportDocumentfrompydanticimportBaseModelclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strprice: floatcategory: Category# Insertproduct=Product(
name="Tony's Chocolonely",
price=5.95,
category=Category(
name="Chocolate",
description="Roasted cacao"
)
)
awaitproduct.insert()
# Retrievefound=awaitProduct.get(product.id)

Result: Type-safe, validated, 65% less code

Search/Query Operation

Vanilla Redis (25 lines)Beanis (4 lines)
# Find products between $10-50keys=awaitredis.zrangebyscore(
"idx:Product:price",
min=10.0,
max=50.0
)
# Fetch each product using pipelinepipe=redis.pipeline()
forkeyinkeys:
pipe.hgetall(f"Product:{key}")
results=awaitpipe.execute()
# Parse manuallyproducts= []
fordatainresults:
ifdata:
products.append({
"name": data["name"],
"price": float(data["price"]),
"stock": int(data["stock"]),
"category": json.loads(
data.get("category", "{}")
)
})
# Find products between $10-50products=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0
).to_list()

Result:84% less code, fully typed results

Update Operation

Vanilla Redis (10 lines)Beanis (6 lines)
# Update price and stockawaitredis.hset("Product:123", mapping={
"price": "6.95",
"stock": "150"
})
# Atomic incrementnew_stock=awaitredis.hincrby(
"Product:123",
"stock",
-1
)
# Update fieldsawaitproduct.update(
price=6.95,
stock=150
)
# Atomic incrementnew_stock=awaitproduct.increment_field(
"stock", -1
)

Result: Same functionality, cleaner API, type-safe

Batch Operations

Vanilla Redis (14 lines)Beanis (9 lines)
# Insert 100 productspipe=redis.pipeline()
foriinrange(100):
product_id=f"prod_{i}"data= {
"name": f"Product {i}",
"price": str(i*10),
"stock": "100",
"category": json.dumps({
"name": "Category"
})
}
pipe.hset(f"Product:{product_id}",
mapping=data)
pipe.zadd("all:Product",
{product_id: time.time()})
awaitpipe.execute()
# Insert 100 productsproducts= [
Product(
name=f"Product {i}",
price=i*10,
stock=100,
category=Category(name="Category")
)
foriinrange(100)
]
awaitProduct.insert_many(products)

Result:35% less code, no manual key management

Installation

PIP

pip install beanis

Poetry

poetry add beanis

Quick Start

importasynciofromtypingimportOptionalfromredis.asyncioimportRedisfrompydanticimportBaseModelfrombeanisimportDocument, init_beanis, IndexedclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strdescription: Optional[str] =Noneprice: Indexed(float) # Indexed for range queriescategory: Categorystock: int=0classSettings:
name="products"asyncdefmain():
# Initialize Redis clientclient=Redis(host="localhost", port=6379, db=0, decode_responses=True)
# Initialize Beanisawaitinit_beanis(database=client, document_models=[Product])
# Create a productchocolate=Category(
name="Chocolate",
description="A preparation of roasted and ground cacao seeds."
)
product=Product(
name="Tony's Chocolonely",
price=5.95,
category=chocolate,
stock=100
)
# Insert into Redisawaitproduct.insert()
# Retrieve by IDfound=awaitProduct.get(product.id)
print(f"Found: {found.name} - ${found.price}")
# Query by price rangeaffordable=awaitProduct.find(
Product.price<10.0
).to_list()
print(f"Affordable products: {len(affordable)}")
# Update specific fieldsawaitproduct.update(price=6.95, stock=150)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", -1)
print(f"Stock after sale: {new_stock}")
# Get all productsall_products=awaitProduct.all()
print(f"Total products: {len(all_products)}")
# Deleteawaitproduct.delete_self()
awaitclient.close()
if__name__=="__main__":
asyncio.run(main())

Core Features

🚀 Type Safety & Validation

Beanis uses Pydantic models, giving you automatic validation and type checking:

classProduct(Document):
name: strprice: float# Must be a numberstock: int# Must be an integercategory: Category# Must be a valid Category object# This will raise a validation error BEFORE hitting Redisproduct=Product(
name="Invalid",
price="not a number", # ❌ ValidationError!stock=100
)

⚡ High Performance

Beanis is optimized for speed with minimal overhead:

  • 8% overhead vs vanilla Redis (benchmarked)
  • Uses msgspec for ultra-fast JSON parsing (2x faster than orjson)
  • Skips Pydantic validation on reads by default (data from Redis is trusted)
  • Efficient pipeline usage for batch operations

Benchmark Results (Get by ID):

  • Vanilla Redis: 1.00x (baseline)
  • Beanis: 1.08x (only 8% slower)
  • Redis OM: 1.20x (20% slower)

🎯 Pythonic API

Familiar Beanie-style interface for MongoDB developers:

# Query with Pythonic operatorsproducts=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0,
Product.stock>0
).to_list()
# Chaining operationsexpensive=awaitProduct.find(
Product.price>100
).sort(Product.price).limit(10).to_list()
# Batch operationsawaitProduct.insert_many([product1, product2, product3])
products=awaitProduct.get_many([id1, id2, id3])
awaitProduct.delete_many([id1, id2])

📦 Store Anything

Beanis handles complex types automatically:

Built-in support:

  • Nested Pydantic models
  • Lists, dicts, tuples, sets
  • Decimal, UUID, Enum
  • datetime, date, time, timedelta

Custom types via encoders:

frombeanisimportDocument, register_typeimportnumpyasnp# NumPy arrays work automatically (auto-registered)classMLModel(Document):
name: strweights: Any# Stores np.ndarray!model=MLModel(name="v1", weights=np.random.rand(100, 100))
awaitmodel.insert() # Just works!# Custom typesregister_type(
MyCustomType,
encoder=lambdaobj: str(obj),
decoder=lambdas: MyCustomType.from_string(s)
)

See tutorial on how to create them!

🔧 Production Ready Features

TTL Support:

# Insert with TTLawaitproduct.insert(ttl=3600) # Expires in 1 hour# Set TTL on existing documentawaitproduct.set_ttl(7200)
ttl=awaitproduct.get_ttl()
awaitproduct.persist() # Remove TTL

Event Hooks:

frombeanisimportbefore_event, after_event, Insert, UpdateclassProduct(Document):
name: strprice: float@before_event(Insert)asyncdefvalidate_price(self):
ifself.price<0:
raiseValueError("Price cannot be negative")
@after_event(Insert)asyncdeflog_creation(self):
print(f"Created product: {self.name}")

Field-Level Operations:

# Get/set single field without loading entire documentprice=awaitproduct.get_field("price")
awaitproduct.set_field("stock", 200)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", 5)

Document Tracking:

# Get all documents (sorted by insertion time)all_products=awaitProduct.all()
# Paginationpage1=awaitProduct.all(limit=10)
page2=awaitProduct.all(skip=10, limit=10)
# Count and delete allcount=awaitProduct.count()
awaitProduct.delete_all()

Comparison

FeatureVanilla RedisBeanisRedis OM
Code volume100%30%50%
Type safetyManualAutomaticAutomatic
Performance100%108%120%
Vanilla Redis❌ Requires modules
ValidationManualAutomaticAutomatic
API StyleRedis commandsPythonicRedis OM
Learning curveMediumEasyMedium
Nested objectsManualAutomaticAutomatic
Custom typesManualEasyLimited
Event hooks
All DBs (0-15)❌ DB 0 only

Choosing the Right Tool

Choose Vanilla Redis when:

  • Every microsecond matters (high-frequency trading, etc.)
  • Simple key-value storage
  • You're a Redis expert and don't need abstractions

Choose Beanis when: ⭐

  • Building production applications with complex data models
  • Want type safety + performance (8% overhead is acceptable)
  • Using vanilla Redis (no RedisJSON/RediSearch modules)
  • Need to store nested objects, custom types, NumPy arrays, etc.
  • Coming from MongoDB/Beanie and want familiar API
  • Want event hooks for validation and lifecycle management

Choose Redis OM when:

  • You need RedisJSON/RediSearch features
  • Don't mind installing Redis modules
  • Want Redis Stack integration
  • Need advanced full-text search

Requirements

  • Python 3.8+
  • Redis 5.0+
  • Pydantic 1.10+ or 2.0+

Testing

# Run tests
pytest
# Run with coverage
pytest --cov=beanis
# Run specific test
pytest tests/test_core.py::test_insert_and_get

Credits

Beanis is a fork of Beanie - the amazing MongoDB ODM created by Roman Right and contributors.

We took the Beanie codebase and completely reimagined it for Redis, replacing MongoDB operations with Redis commands while preserving the elegant API design. If you're using MongoDB, check out the original Beanie - it's awesome!

Special thanks to:

  • Roman Right and the Beanie community for creating the foundation
  • All Beanie contributors whose code inspired this project
  • The Redis and Pydantic teams for their excellent libraries

Beanie

License

Apache License 2.0

About

Asynchronous Python ODM for Redis

Resources

Code of conduct

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

Beanis - Redis ODM for Humans

Beanis

PyPI versionDownloadsPython versionsLicenseGitHub stars
TestsCoverageCode style: blackPydanticRedis

Stop writing boilerplate Redis code. Focus on your application logic.

Beanis is an async Python ODM (Object-Document Mapper) for Redis that gives you Pydantic models, type safety, and a clean API - while staying fast and working with vanilla Redis.

Why Beanis?

The Problem with Vanilla Redis

Manual serialization - You write json.dumps() and json.loads() everywhere ❌ Type conversions - Strings from Redis need manual float(), int() conversions ❌ Key management - You track "Product:123", "all:Product" keys manually ❌ No validation - Bad data silently corrupts your Redis database ❌ Boilerplate code - 15-20 lines for simple CRUD operations

The Solution: Beanis

Automatic serialization - Nested objects, lists, custom types - all handled ✅ Type safety - Full Pydantic validation + IDE autocomplete ✅ Smart key management - Focus on your data, not Redis internals ✅ Data validation - Catch errors before they hit Redis ✅ Write 70% less code - 5-7 lines for the same operations

AND it's fast: Only 8% overhead vs vanilla Redis

Who Should Use Beanis?

✅ You're building a production app that needs Redis but not the boilerplate ✅ You want type safety and validation without sacrificing performance ✅ You're using vanilla Redis (no RedisJSON/RediSearch modules) ✅ You like Beanie's MongoDB API and want the same for Redis ✅ You're storing complex data (nested objects, NumPy arrays, etc.)

When NOT to Use Beanis?

❌ You need every microsecond of performance (use raw redis-py) ❌ You need RedisJSON/RediSearch features (use Redis OM) ❌ You're only storing simple key-value pairs (use raw redis-py)

Show Me The Code

Basic CRUD Operation

Vanilla Redis (20 lines)Beanis (7 lines)
importjsonimporttimefromredis.asyncioimportRedisredis=Redis(decode_responses=True)
# Insertproduct_data= {
"name": "Tony's Chocolonely",
"price": "5.95",
"category": json.dumps({
"name": "Chocolate",
"description": "Roasted cacao"
})
}
awaitredis.hset("Product:prod_123",
mapping=product_data)
awaitredis.zadd("all:Product",
{"prod_123": time.time()})
# Retrieveraw=awaitredis.hgetall("Product:prod_123")
product= {
"name": raw["name"],
"price": float(raw["price"]),
"category": json.loads(raw["category"])
}
frombeanisimportDocumentfrompydanticimportBaseModelclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strprice: floatcategory: Category# Insertproduct=Product(
name="Tony's Chocolonely",
price=5.95,
category=Category(
name="Chocolate",
description="Roasted cacao"
)
)
awaitproduct.insert()
# Retrievefound=awaitProduct.get(product.id)

Result: Type-safe, validated, 65% less code

Search/Query Operation

Vanilla Redis (25 lines)Beanis (4 lines)
# Find products between $10-50keys=awaitredis.zrangebyscore(
"idx:Product:price",
min=10.0,
max=50.0
)
# Fetch each product using pipelinepipe=redis.pipeline()
forkeyinkeys:
pipe.hgetall(f"Product:{key}")
results=awaitpipe.execute()
# Parse manuallyproducts= []
fordatainresults:
ifdata:
products.append({
"name": data["name"],
"price": float(data["price"]),
"stock": int(data["stock"]),
"category": json.loads(
data.get("category", "{}")
)
})
# Find products between $10-50products=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0
).to_list()

Result:84% less code, fully typed results

Update Operation

Vanilla Redis (10 lines)Beanis (6 lines)
# Update price and stockawaitredis.hset("Product:123", mapping={
"price": "6.95",
"stock": "150"
})
# Atomic incrementnew_stock=awaitredis.hincrby(
"Product:123",
"stock",
-1
)
# Update fieldsawaitproduct.update(
price=6.95,
stock=150
)
# Atomic incrementnew_stock=awaitproduct.increment_field(
"stock", -1
)

Result: Same functionality, cleaner API, type-safe

Batch Operations

Vanilla Redis (14 lines)Beanis (9 lines)
# Insert 100 productspipe=redis.pipeline()
foriinrange(100):
product_id=f"prod_{i}"data= {
"name": f"Product {i}",
"price": str(i*10),
"stock": "100",
"category": json.dumps({
"name": "Category"
})
}
pipe.hset(f"Product:{product_id}",
mapping=data)
pipe.zadd("all:Product",
{product_id: time.time()})
awaitpipe.execute()
# Insert 100 productsproducts= [
Product(
name=f"Product {i}",
price=i*10,
stock=100,
category=Category(name="Category")
)
foriinrange(100)
]
awaitProduct.insert_many(products)

Result:35% less code, no manual key management

Installation

PIP

pip install beanis

Poetry

poetry add beanis

Quick Start

importasynciofromtypingimportOptionalfromredis.asyncioimportRedisfrompydanticimportBaseModelfrombeanisimportDocument, init_beanis, IndexedclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strdescription: Optional[str] =Noneprice: Indexed(float) # Indexed for range queriescategory: Categorystock: int=0classSettings:
name="products"asyncdefmain():
# Initialize Redis clientclient=Redis(host="localhost", port=6379, db=0, decode_responses=True)
# Initialize Beanisawaitinit_beanis(database=client, document_models=[Product])
# Create a productchocolate=Category(
name="Chocolate",
description="A preparation of roasted and ground cacao seeds."
)
product=Product(
name="Tony's Chocolonely",
price=5.95,
category=chocolate,
stock=100
)
# Insert into Redisawaitproduct.insert()
# Retrieve by IDfound=awaitProduct.get(product.id)
print(f"Found: {found.name} - ${found.price}")
# Query by price rangeaffordable=awaitProduct.find(
Product.price<10.0
).to_list()
print(f"Affordable products: {len(affordable)}")
# Update specific fieldsawaitproduct.update(price=6.95, stock=150)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", -1)
print(f"Stock after sale: {new_stock}")
# Get all productsall_products=awaitProduct.all()
print(f"Total products: {len(all_products)}")
# Deleteawaitproduct.delete_self()
awaitclient.close()
if__name__=="__main__":
asyncio.run(main())

Core Features

🚀 Type Safety & Validation

Beanis uses Pydantic models, giving you automatic validation and type checking:

classProduct(Document):
name: strprice: float# Must be a numberstock: int# Must be an integercategory: Category# Must be a valid Category object# This will raise a validation error BEFORE hitting Redisproduct=Product(
name="Invalid",
price="not a number", # ❌ ValidationError!stock=100
)

⚡ High Performance

Beanis is optimized for speed with minimal overhead:

  • 8% overhead vs vanilla Redis (benchmarked)
  • Uses msgspec for ultra-fast JSON parsing (2x faster than orjson)
  • Skips Pydantic validation on reads by default (data from Redis is trusted)
  • Efficient pipeline usage for batch operations

Benchmark Results (Get by ID):

  • Vanilla Redis: 1.00x (baseline)
  • Beanis: 1.08x (only 8% slower)
  • Redis OM: 1.20x (20% slower)

🎯 Pythonic API

Familiar Beanie-style interface for MongoDB developers:

# Query with Pythonic operatorsproducts=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0,
Product.stock>0
).to_list()
# Chaining operationsexpensive=awaitProduct.find(
Product.price>100
).sort(Product.price).limit(10).to_list()
# Batch operationsawaitProduct.insert_many([product1, product2, product3])
products=awaitProduct.get_many([id1, id2, id3])
awaitProduct.delete_many([id1, id2])

📦 Store Anything

Beanis handles complex types automatically:

Built-in support:

  • Nested Pydantic models
  • Lists, dicts, tuples, sets
  • Decimal, UUID, Enum
  • datetime, date, time, timedelta

Custom types via encoders:

frombeanisimportDocument, register_typeimportnumpyasnp# NumPy arrays work automatically (auto-registered)classMLModel(Document):
name: strweights: Any# Stores np.ndarray!model=MLModel(name="v1", weights=np.random.rand(100, 100))
awaitmodel.insert() # Just works!# Custom typesregister_type(
MyCustomType,
encoder=lambdaobj: str(obj),
decoder=lambdas: MyCustomType.from_string(s)
)

See tutorial on how to create them!

🔧 Production Ready Features

TTL Support:

# Insert with TTLawaitproduct.insert(ttl=3600) # Expires in 1 hour# Set TTL on existing documentawaitproduct.set_ttl(7200)
ttl=awaitproduct.get_ttl()
awaitproduct.persist() # Remove TTL

Event Hooks:

frombeanisimportbefore_event, after_event, Insert, UpdateclassProduct(Document):
name: strprice: float@before_event(Insert)asyncdefvalidate_price(self):
ifself.price<0:
raiseValueError("Price cannot be negative")
@after_event(Insert)asyncdeflog_creation(self):
print(f"Created product: {self.name}")

Field-Level Operations:

# Get/set single field without loading entire documentprice=awaitproduct.get_field("price")
awaitproduct.set_field("stock", 200)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", 5)

Document Tracking:

# Get all documents (sorted by insertion time)all_products=awaitProduct.all()
# Paginationpage1=awaitProduct.all(limit=10)
page2=awaitProduct.all(skip=10, limit=10)
# Count and delete allcount=awaitProduct.count()
awaitProduct.delete_all()

Comparison

FeatureVanilla RedisBeanisRedis OM
Code volume100%30%50%
Type safetyManualAutomaticAutomatic
Performance100%108%120%
Vanilla Redis❌ Requires modules
ValidationManualAutomaticAutomatic
API StyleRedis commandsPythonicRedis OM
Learning curveMediumEasyMedium
Nested objectsManualAutomaticAutomatic
Custom typesManualEasyLimited
Event hooks
All DBs (0-15)❌ DB 0 only

Choosing the Right Tool

Choose Vanilla Redis when:

  • Every microsecond matters (high-frequency trading, etc.)
  • Simple key-value storage
  • You're a Redis expert and don't need abstractions

Choose Beanis when: ⭐

  • Building production applications with complex data models
  • Want type safety + performance (8% overhead is acceptable)
  • Using vanilla Redis (no RedisJSON/RediSearch modules)
  • Need to store nested objects, custom types, NumPy arrays, etc.
  • Coming from MongoDB/Beanie and want familiar API
  • Want event hooks for validation and lifecycle management

Choose Redis OM when:

  • You need RedisJSON/RediSearch features
  • Don't mind installing Redis modules
  • Want Redis Stack integration
  • Need advanced full-text search

Requirements

  • Python 3.8+
  • Redis 5.0+
  • Pydantic 1.10+ or 2.0+

Testing

# Run tests
pytest
# Run with coverage
pytest --cov=beanis
# Run specific test
pytest tests/test_core.py::test_insert_and_get

Credits

Beanis is a fork of Beanie - the amazing MongoDB ODM created by Roman Right and contributors.

We took the Beanie codebase and completely reimagined it for Redis, replacing MongoDB operations with Redis commands while preserving the elegant API design. If you're using MongoDB, check out the original Beanie - it's awesome!

Special thanks to:

  • Roman Right and the Beanie community for creating the foundation
  • All Beanie contributors whose code inspired this project
  • The Redis and Pydantic teams for their excellent libraries

Beanie

License

Apache License 2.0

About

Asynchronous Python ODM for Redis

Resources

Code of conduct

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Beanis - Redis ODM for Humans

Beanis

PyPI versionDownloadsPython versionsLicenseGitHub stars
TestsCoverageCode style: blackPydanticRedis

Stop writing boilerplate Redis code. Focus on your application logic.

Beanis is an async Python ODM (Object-Document Mapper) for Redis that gives you Pydantic models, type safety, and a clean API - while staying fast and working with vanilla Redis.

Why Beanis?

The Problem with Vanilla Redis

Manual serialization - You write json.dumps() and json.loads() everywhere ❌ Type conversions - Strings from Redis need manual float(), int() conversions ❌ Key management - You track "Product:123", "all:Product" keys manually ❌ No validation - Bad data silently corrupts your Redis database ❌ Boilerplate code - 15-20 lines for simple CRUD operations

The Solution: Beanis

Automatic serialization - Nested objects, lists, custom types - all handled ✅ Type safety - Full Pydantic validation + IDE autocomplete ✅ Smart key management - Focus on your data, not Redis internals ✅ Data validation - Catch errors before they hit Redis ✅ Write 70% less code - 5-7 lines for the same operations

AND it's fast: Only 8% overhead vs vanilla Redis

Who Should Use Beanis?

✅ You're building a production app that needs Redis but not the boilerplate ✅ You want type safety and validation without sacrificing performance ✅ You're using vanilla Redis (no RedisJSON/RediSearch modules) ✅ You like Beanie's MongoDB API and want the same for Redis ✅ You're storing complex data (nested objects, NumPy arrays, etc.)

When NOT to Use Beanis?

❌ You need every microsecond of performance (use raw redis-py) ❌ You need RedisJSON/RediSearch features (use Redis OM) ❌ You're only storing simple key-value pairs (use raw redis-py)

Show Me The Code

Basic CRUD Operation

Vanilla Redis (20 lines)Beanis (7 lines)
importjsonimporttimefromredis.asyncioimportRedisredis=Redis(decode_responses=True)
# Insertproduct_data= {
"name": "Tony's Chocolonely",
"price": "5.95",
"category": json.dumps({
"name": "Chocolate",
"description": "Roasted cacao"
})
}
awaitredis.hset("Product:prod_123",
mapping=product_data)
awaitredis.zadd("all:Product",
{"prod_123": time.time()})
# Retrieveraw=awaitredis.hgetall("Product:prod_123")
product= {
"name": raw["name"],
"price": float(raw["price"]),
"category": json.loads(raw["category"])
}
frombeanisimportDocumentfrompydanticimportBaseModelclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strprice: floatcategory: Category# Insertproduct=Product(
name="Tony's Chocolonely",
price=5.95,
category=Category(
name="Chocolate",
description="Roasted cacao"
)
)
awaitproduct.insert()
# Retrievefound=awaitProduct.get(product.id)

Result: Type-safe, validated, 65% less code

Search/Query Operation

Vanilla Redis (25 lines)Beanis (4 lines)
# Find products between $10-50keys=awaitredis.zrangebyscore(
"idx:Product:price",
min=10.0,
max=50.0
)
# Fetch each product using pipelinepipe=redis.pipeline()
forkeyinkeys:
pipe.hgetall(f"Product:{key}")
results=awaitpipe.execute()
# Parse manuallyproducts= []
fordatainresults:
ifdata:
products.append({
"name": data["name"],
"price": float(data["price"]),
"stock": int(data["stock"]),
"category": json.loads(
data.get("category", "{}")
)
})
# Find products between $10-50products=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0
).to_list()

Result:84% less code, fully typed results

Update Operation

Vanilla Redis (10 lines)Beanis (6 lines)
# Update price and stockawaitredis.hset("Product:123", mapping={
"price": "6.95",
"stock": "150"
})
# Atomic incrementnew_stock=awaitredis.hincrby(
"Product:123",
"stock",
-1
)
# Update fieldsawaitproduct.update(
price=6.95,
stock=150
)
# Atomic incrementnew_stock=awaitproduct.increment_field(
"stock", -1
)

Result: Same functionality, cleaner API, type-safe

Batch Operations

Vanilla Redis (14 lines)Beanis (9 lines)
# Insert 100 productspipe=redis.pipeline()
foriinrange(100):
product_id=f"prod_{i}"data= {
"name": f"Product {i}",
"price": str(i*10),
"stock": "100",
"category": json.dumps({
"name": "Category"
})
}
pipe.hset(f"Product:{product_id}",
mapping=data)
pipe.zadd("all:Product",
{product_id: time.time()})
awaitpipe.execute()
# Insert 100 productsproducts= [
Product(
name=f"Product {i}",
price=i*10,
stock=100,
category=Category(name="Category")
)
foriinrange(100)
]
awaitProduct.insert_many(products)

Result:35% less code, no manual key management

Installation

PIP

pip install beanis

Poetry

poetry add beanis

Quick Start

importasynciofromtypingimportOptionalfromredis.asyncioimportRedisfrompydanticimportBaseModelfrombeanisimportDocument, init_beanis, IndexedclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strdescription: Optional[str] =Noneprice: Indexed(float) # Indexed for range queriescategory: Categorystock: int=0classSettings:
name="products"asyncdefmain():
# Initialize Redis clientclient=Redis(host="localhost", port=6379, db=0, decode_responses=True)
# Initialize Beanisawaitinit_beanis(database=client, document_models=[Product])
# Create a productchocolate=Category(
name="Chocolate",
description="A preparation of roasted and ground cacao seeds."
)
product=Product(
name="Tony's Chocolonely",
price=5.95,
category=chocolate,
stock=100
)
# Insert into Redisawaitproduct.insert()
# Retrieve by IDfound=awaitProduct.get(product.id)
print(f"Found: {found.name} - ${found.price}")
# Query by price rangeaffordable=awaitProduct.find(
Product.price<10.0
).to_list()
print(f"Affordable products: {len(affordable)}")
# Update specific fieldsawaitproduct.update(price=6.95, stock=150)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", -1)
print(f"Stock after sale: {new_stock}")
# Get all productsall_products=awaitProduct.all()
print(f"Total products: {len(all_products)}")
# Deleteawaitproduct.delete_self()
awaitclient.close()
if__name__=="__main__":
asyncio.run(main())

Core Features

🚀 Type Safety & Validation

Beanis uses Pydantic models, giving you automatic validation and type checking:

classProduct(Document):
name: strprice: float# Must be a numberstock: int# Must be an integercategory: Category# Must be a valid Category object# This will raise a validation error BEFORE hitting Redisproduct=Product(
name="Invalid",
price="not a number", # ❌ ValidationError!stock=100
)

⚡ High Performance

Beanis is optimized for speed with minimal overhead:

  • 8% overhead vs vanilla Redis (benchmarked)
  • Uses msgspec for ultra-fast JSON parsing (2x faster than orjson)
  • Skips Pydantic validation on reads by default (data from Redis is trusted)
  • Efficient pipeline usage for batch operations

Benchmark Results (Get by ID):

  • Vanilla Redis: 1.00x (baseline)
  • Beanis: 1.08x (only 8% slower)
  • Redis OM: 1.20x (20% slower)

🎯 Pythonic API

Familiar Beanie-style interface for MongoDB developers:

# Query with Pythonic operatorsproducts=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0,
Product.stock>0
).to_list()
# Chaining operationsexpensive=awaitProduct.find(
Product.price>100
).sort(Product.price).limit(10).to_list()
# Batch operationsawaitProduct.insert_many([product1, product2, product3])
products=awaitProduct.get_many([id1, id2, id3])
awaitProduct.delete_many([id1, id2])

📦 Store Anything

Beanis handles complex types automatically:

Built-in support:

  • Nested Pydantic models
  • Lists, dicts, tuples, sets
  • Decimal, UUID, Enum
  • datetime, date, time, timedelta

Custom types via encoders:

frombeanisimportDocument, register_typeimportnumpyasnp# NumPy arrays work automatically (auto-registered)classMLModel(Document):
name: strweights: Any# Stores np.ndarray!model=MLModel(name="v1", weights=np.random.rand(100, 100))
awaitmodel.insert() # Just works!# Custom typesregister_type(
MyCustomType,
encoder=lambdaobj: str(obj),
decoder=lambdas: MyCustomType.from_string(s)
)

See tutorial on how to create them!

🔧 Production Ready Features

TTL Support:

# Insert with TTLawaitproduct.insert(ttl=3600) # Expires in 1 hour# Set TTL on existing documentawaitproduct.set_ttl(7200)
ttl=awaitproduct.get_ttl()
awaitproduct.persist() # Remove TTL

Event Hooks:

frombeanisimportbefore_event, after_event, Insert, UpdateclassProduct(Document):
name: strprice: float@before_event(Insert)asyncdefvalidate_price(self):
ifself.price<0:
raiseValueError("Price cannot be negative")
@after_event(Insert)asyncdeflog_creation(self):
print(f"Created product: {self.name}")

Field-Level Operations:

# Get/set single field without loading entire documentprice=awaitproduct.get_field("price")
awaitproduct.set_field("stock", 200)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", 5)

Document Tracking:

# Get all documents (sorted by insertion time)all_products=awaitProduct.all()
# Paginationpage1=awaitProduct.all(limit=10)
page2=awaitProduct.all(skip=10, limit=10)
# Count and delete allcount=awaitProduct.count()
awaitProduct.delete_all()

Comparison

FeatureVanilla RedisBeanisRedis OM
Code volume100%30%50%
Type safetyManualAutomaticAutomatic
Performance100%108%120%
Vanilla Redis❌ Requires modules
ValidationManualAutomaticAutomatic
API StyleRedis commandsPythonicRedis OM
Learning curveMediumEasyMedium
Nested objectsManualAutomaticAutomatic
Custom typesManualEasyLimited
Event hooks
All DBs (0-15)❌ DB 0 only

Choosing the Right Tool

Choose Vanilla Redis when:

  • Every microsecond matters (high-frequency trading, etc.)
  • Simple key-value storage
  • You're a Redis expert and don't need abstractions

Choose Beanis when: ⭐

  • Building production applications with complex data models
  • Want type safety + performance (8% overhead is acceptable)
  • Using vanilla Redis (no RedisJSON/RediSearch modules)
  • Need to store nested objects, custom types, NumPy arrays, etc.
  • Coming from MongoDB/Beanie and want familiar API
  • Want event hooks for validation and lifecycle management

Choose Redis OM when:

  • You need RedisJSON/RediSearch features
  • Don't mind installing Redis modules
  • Want Redis Stack integration
  • Need advanced full-text search

Requirements

  • Python 3.8+
  • Redis 5.0+
  • Pydantic 1.10+ or 2.0+

Testing

# Run tests
pytest
# Run with coverage
pytest --cov=beanis
# Run specific test
pytest tests/test_core.py::test_insert_and_get

Credits

Beanis is a fork of Beanie - the amazing MongoDB ODM created by Roman Right and contributors.

We took the Beanie codebase and completely reimagined it for Redis, replacing MongoDB operations with Redis commands while preserving the elegant API design. If you're using MongoDB, check out the original Beanie - it's awesome!

Special thanks to:

  • Roman Right and the Beanie community for creating the foundation
  • All Beanie contributors whose code inspired this project
  • The Redis and Pydantic teams for their excellent libraries

Beanie

License

Apache License 2.0

About

Asynchronous Python ODM for Redis

Resources

Code of conduct

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Beanis - Redis ODM for Humans

Beanis

PyPI versionDownloadsPython versionsLicenseGitHub stars
TestsCoverageCode style: blackPydanticRedis

Stop writing boilerplate Redis code. Focus on your application logic.

Beanis is an async Python ODM (Object-Document Mapper) for Redis that gives you Pydantic models, type safety, and a clean API - while staying fast and working with vanilla Redis.

Why Beanis?

The Problem with Vanilla Redis

Manual serialization - You write json.dumps() and json.loads() everywhere ❌ Type conversions - Strings from Redis need manual float(), int() conversions ❌ Key management - You track "Product:123", "all:Product" keys manually ❌ No validation - Bad data silently corrupts your Redis database ❌ Boilerplate code - 15-20 lines for simple CRUD operations

The Solution: Beanis

Automatic serialization - Nested objects, lists, custom types - all handled ✅ Type safety - Full Pydantic validation + IDE autocomplete ✅ Smart key management - Focus on your data, not Redis internals ✅ Data validation - Catch errors before they hit Redis ✅ Write 70% less code - 5-7 lines for the same operations

AND it's fast: Only 8% overhead vs vanilla Redis

Who Should Use Beanis?

✅ You're building a production app that needs Redis but not the boilerplate ✅ You want type safety and validation without sacrificing performance ✅ You're using vanilla Redis (no RedisJSON/RediSearch modules) ✅ You like Beanie's MongoDB API and want the same for Redis ✅ You're storing complex data (nested objects, NumPy arrays, etc.)

When NOT to Use Beanis?

❌ You need every microsecond of performance (use raw redis-py) ❌ You need RedisJSON/RediSearch features (use Redis OM) ❌ You're only storing simple key-value pairs (use raw redis-py)

Show Me The Code

Basic CRUD Operation

Vanilla Redis (20 lines)Beanis (7 lines)
importjsonimporttimefromredis.asyncioimportRedisredis=Redis(decode_responses=True)
# Insertproduct_data= {
"name": "Tony's Chocolonely",
"price": "5.95",
"category": json.dumps({
"name": "Chocolate",
"description": "Roasted cacao"
})
}
awaitredis.hset("Product:prod_123",
mapping=product_data)
awaitredis.zadd("all:Product",
{"prod_123": time.time()})
# Retrieveraw=awaitredis.hgetall("Product:prod_123")
product= {
"name": raw["name"],
"price": float(raw["price"]),
"category": json.loads(raw["category"])
}
frombeanisimportDocumentfrompydanticimportBaseModelclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strprice: floatcategory: Category# Insertproduct=Product(
name="Tony's Chocolonely",
price=5.95,
category=Category(
name="Chocolate",
description="Roasted cacao"
)
)
awaitproduct.insert()
# Retrievefound=awaitProduct.get(product.id)

Result: Type-safe, validated, 65% less code

Search/Query Operation

Vanilla Redis (25 lines)Beanis (4 lines)
# Find products between $10-50keys=awaitredis.zrangebyscore(
"idx:Product:price",
min=10.0,
max=50.0
)
# Fetch each product using pipelinepipe=redis.pipeline()
forkeyinkeys:
pipe.hgetall(f"Product:{key}")
results=awaitpipe.execute()
# Parse manuallyproducts= []
fordatainresults:
ifdata:
products.append({
"name": data["name"],
"price": float(data["price"]),
"stock": int(data["stock"]),
"category": json.loads(
data.get("category", "{}")
)
})
# Find products between $10-50products=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0
).to_list()

Result:84% less code, fully typed results

Update Operation

Vanilla Redis (10 lines)Beanis (6 lines)
# Update price and stockawaitredis.hset("Product:123", mapping={
"price": "6.95",
"stock": "150"
})
# Atomic incrementnew_stock=awaitredis.hincrby(
"Product:123",
"stock",
-1
)
# Update fieldsawaitproduct.update(
price=6.95,
stock=150
)
# Atomic incrementnew_stock=awaitproduct.increment_field(
"stock", -1
)

Result: Same functionality, cleaner API, type-safe

Batch Operations

Vanilla Redis (14 lines)Beanis (9 lines)
# Insert 100 productspipe=redis.pipeline()
foriinrange(100):
product_id=f"prod_{i}"data= {
"name": f"Product {i}",
"price": str(i*10),
"stock": "100",
"category": json.dumps({
"name": "Category"
})
}
pipe.hset(f"Product:{product_id}",
mapping=data)
pipe.zadd("all:Product",
{product_id: time.time()})
awaitpipe.execute()
# Insert 100 productsproducts= [
Product(
name=f"Product {i}",
price=i*10,
stock=100,
category=Category(name="Category")
)
foriinrange(100)
]
awaitProduct.insert_many(products)

Result:35% less code, no manual key management

Installation

PIP

pip install beanis

Poetry

poetry add beanis

Quick Start

importasynciofromtypingimportOptionalfromredis.asyncioimportRedisfrompydanticimportBaseModelfrombeanisimportDocument, init_beanis, IndexedclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strdescription: Optional[str] =Noneprice: Indexed(float) # Indexed for range queriescategory: Categorystock: int=0classSettings:
name="products"asyncdefmain():
# Initialize Redis clientclient=Redis(host="localhost", port=6379, db=0, decode_responses=True)
# Initialize Beanisawaitinit_beanis(database=client, document_models=[Product])
# Create a productchocolate=Category(
name="Chocolate",
description="A preparation of roasted and ground cacao seeds."
)
product=Product(
name="Tony's Chocolonely",
price=5.95,
category=chocolate,
stock=100
)
# Insert into Redisawaitproduct.insert()
# Retrieve by IDfound=awaitProduct.get(product.id)
print(f"Found: {found.name} - ${found.price}")
# Query by price rangeaffordable=awaitProduct.find(
Product.price<10.0
).to_list()
print(f"Affordable products: {len(affordable)}")
# Update specific fieldsawaitproduct.update(price=6.95, stock=150)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", -1)
print(f"Stock after sale: {new_stock}")
# Get all productsall_products=awaitProduct.all()
print(f"Total products: {len(all_products)}")
# Deleteawaitproduct.delete_self()
awaitclient.close()
if__name__=="__main__":
asyncio.run(main())

Core Features

🚀 Type Safety & Validation

Beanis uses Pydantic models, giving you automatic validation and type checking:

classProduct(Document):
name: strprice: float# Must be a numberstock: int# Must be an integercategory: Category# Must be a valid Category object# This will raise a validation error BEFORE hitting Redisproduct=Product(
name="Invalid",
price="not a number", # ❌ ValidationError!stock=100
)

⚡ High Performance

Beanis is optimized for speed with minimal overhead:

  • 8% overhead vs vanilla Redis (benchmarked)
  • Uses msgspec for ultra-fast JSON parsing (2x faster than orjson)
  • Skips Pydantic validation on reads by default (data from Redis is trusted)
  • Efficient pipeline usage for batch operations

Benchmark Results (Get by ID):

  • Vanilla Redis: 1.00x (baseline)
  • Beanis: 1.08x (only 8% slower)
  • Redis OM: 1.20x (20% slower)

🎯 Pythonic API

Familiar Beanie-style interface for MongoDB developers:

# Query with Pythonic operatorsproducts=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0,
Product.stock>0
).to_list()
# Chaining operationsexpensive=awaitProduct.find(
Product.price>100
).sort(Product.price).limit(10).to_list()
# Batch operationsawaitProduct.insert_many([product1, product2, product3])
products=awaitProduct.get_many([id1, id2, id3])
awaitProduct.delete_many([id1, id2])

📦 Store Anything

Beanis handles complex types automatically:

Built-in support:

  • Nested Pydantic models
  • Lists, dicts, tuples, sets
  • Decimal, UUID, Enum
  • datetime, date, time, timedelta

Custom types via encoders:

frombeanisimportDocument, register_typeimportnumpyasnp# NumPy arrays work automatically (auto-registered)classMLModel(Document):
name: strweights: Any# Stores np.ndarray!model=MLModel(name="v1", weights=np.random.rand(100, 100))
awaitmodel.insert() # Just works!# Custom typesregister_type(
MyCustomType,
encoder=lambdaobj: str(obj),
decoder=lambdas: MyCustomType.from_string(s)
)

See tutorial on how to create them!

🔧 Production Ready Features

TTL Support:

# Insert with TTLawaitproduct.insert(ttl=3600) # Expires in 1 hour# Set TTL on existing documentawaitproduct.set_ttl(7200)
ttl=awaitproduct.get_ttl()
awaitproduct.persist() # Remove TTL

Event Hooks:

frombeanisimportbefore_event, after_event, Insert, UpdateclassProduct(Document):
name: strprice: float@before_event(Insert)asyncdefvalidate_price(self):
ifself.price<0:
raiseValueError("Price cannot be negative")
@after_event(Insert)asyncdeflog_creation(self):
print(f"Created product: {self.name}")

Field-Level Operations:

# Get/set single field without loading entire documentprice=awaitproduct.get_field("price")
awaitproduct.set_field("stock", 200)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", 5)

Document Tracking:

# Get all documents (sorted by insertion time)all_products=awaitProduct.all()
# Paginationpage1=awaitProduct.all(limit=10)
page2=awaitProduct.all(skip=10, limit=10)
# Count and delete allcount=awaitProduct.count()
awaitProduct.delete_all()

Comparison

FeatureVanilla RedisBeanisRedis OM
Code volume100%30%50%
Type safetyManualAutomaticAutomatic
Performance100%108%120%
Vanilla Redis❌ Requires modules
ValidationManualAutomaticAutomatic
API StyleRedis commandsPythonicRedis OM
Learning curveMediumEasyMedium
Nested objectsManualAutomaticAutomatic
Custom typesManualEasyLimited
Event hooks
All DBs (0-15)❌ DB 0 only

Choosing the Right Tool

Choose Vanilla Redis when:

  • Every microsecond matters (high-frequency trading, etc.)
  • Simple key-value storage
  • You're a Redis expert and don't need abstractions

Choose Beanis when: ⭐

  • Building production applications with complex data models
  • Want type safety + performance (8% overhead is acceptable)
  • Using vanilla Redis (no RedisJSON/RediSearch modules)
  • Need to store nested objects, custom types, NumPy arrays, etc.
  • Coming from MongoDB/Beanie and want familiar API
  • Want event hooks for validation and lifecycle management

Choose Redis OM when:

  • You need RedisJSON/RediSearch features
  • Don't mind installing Redis modules
  • Want Redis Stack integration
  • Need advanced full-text search

Requirements

  • Python 3.8+
  • Redis 5.0+
  • Pydantic 1.10+ or 2.0+

Testing

# Run tests
pytest
# Run with coverage
pytest --cov=beanis
# Run specific test
pytest tests/test_core.py::test_insert_and_get

Credits

Beanis is a fork of Beanie - the amazing MongoDB ODM created by Roman Right and contributors.

We took the Beanie codebase and completely reimagined it for Redis, replacing MongoDB operations with Redis commands while preserving the elegant API design. If you're using MongoDB, check out the original Beanie - it's awesome!

Special thanks to:

  • Roman Right and the Beanie community for creating the foundation
  • All Beanie contributors whose code inspired this project
  • The Redis and Pydantic teams for their excellent libraries

Beanie

License

Apache License 2.0

About

Asynchronous Python ODM for Redis

Resources

Code of conduct

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

Beanis - Redis ODM for Humans

Beanis

PyPI versionDownloadsPython versionsLicenseGitHub stars
TestsCoverageCode style: blackPydanticRedis

Stop writing boilerplate Redis code. Focus on your application logic.

Beanis is an async Python ODM (Object-Document Mapper) for Redis that gives you Pydantic models, type safety, and a clean API - while staying fast and working with vanilla Redis.

Why Beanis?

The Problem with Vanilla Redis

Manual serialization - You write json.dumps() and json.loads() everywhere ❌ Type conversions - Strings from Redis need manual float(), int() conversions ❌ Key management - You track "Product:123", "all:Product" keys manually ❌ No validation - Bad data silently corrupts your Redis database ❌ Boilerplate code - 15-20 lines for simple CRUD operations

The Solution: Beanis

Automatic serialization - Nested objects, lists, custom types - all handled ✅ Type safety - Full Pydantic validation + IDE autocomplete ✅ Smart key management - Focus on your data, not Redis internals ✅ Data validation - Catch errors before they hit Redis ✅ Write 70% less code - 5-7 lines for the same operations

AND it's fast: Only 8% overhead vs vanilla Redis

Who Should Use Beanis?

✅ You're building a production app that needs Redis but not the boilerplate ✅ You want type safety and validation without sacrificing performance ✅ You're using vanilla Redis (no RedisJSON/RediSearch modules) ✅ You like Beanie's MongoDB API and want the same for Redis ✅ You're storing complex data (nested objects, NumPy arrays, etc.)

When NOT to Use Beanis?

❌ You need every microsecond of performance (use raw redis-py) ❌ You need RedisJSON/RediSearch features (use Redis OM) ❌ You're only storing simple key-value pairs (use raw redis-py)

Show Me The Code

Basic CRUD Operation

Vanilla Redis (20 lines)Beanis (7 lines)
importjsonimporttimefromredis.asyncioimportRedisredis=Redis(decode_responses=True)
# Insertproduct_data= {
"name": "Tony's Chocolonely",
"price": "5.95",
"category": json.dumps({
"name": "Chocolate",
"description": "Roasted cacao"
})
}
awaitredis.hset("Product:prod_123",
mapping=product_data)
awaitredis.zadd("all:Product",
{"prod_123": time.time()})
# Retrieveraw=awaitredis.hgetall("Product:prod_123")
product= {
"name": raw["name"],
"price": float(raw["price"]),
"category": json.loads(raw["category"])
}
frombeanisimportDocumentfrompydanticimportBaseModelclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strprice: floatcategory: Category# Insertproduct=Product(
name="Tony's Chocolonely",
price=5.95,
category=Category(
name="Chocolate",
description="Roasted cacao"
)
)
awaitproduct.insert()
# Retrievefound=awaitProduct.get(product.id)

Result: Type-safe, validated, 65% less code

Search/Query Operation

Vanilla Redis (25 lines)Beanis (4 lines)
# Find products between $10-50keys=awaitredis.zrangebyscore(
"idx:Product:price",
min=10.0,
max=50.0
)
# Fetch each product using pipelinepipe=redis.pipeline()
forkeyinkeys:
pipe.hgetall(f"Product:{key}")
results=awaitpipe.execute()
# Parse manuallyproducts= []
fordatainresults:
ifdata:
products.append({
"name": data["name"],
"price": float(data["price"]),
"stock": int(data["stock"]),
"category": json.loads(
data.get("category", "{}")
)
})
# Find products between $10-50products=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0
).to_list()

Result:84% less code, fully typed results

Update Operation

Vanilla Redis (10 lines)Beanis (6 lines)
# Update price and stockawaitredis.hset("Product:123", mapping={
"price": "6.95",
"stock": "150"
})
# Atomic incrementnew_stock=awaitredis.hincrby(
"Product:123",
"stock",
-1
)
# Update fieldsawaitproduct.update(
price=6.95,
stock=150
)
# Atomic incrementnew_stock=awaitproduct.increment_field(
"stock", -1
)

Result: Same functionality, cleaner API, type-safe

Batch Operations

Vanilla Redis (14 lines)Beanis (9 lines)
# Insert 100 productspipe=redis.pipeline()
foriinrange(100):
product_id=f"prod_{i}"data= {
"name": f"Product {i}",
"price": str(i*10),
"stock": "100",
"category": json.dumps({
"name": "Category"
})
}
pipe.hset(f"Product:{product_id}",
mapping=data)
pipe.zadd("all:Product",
{product_id: time.time()})
awaitpipe.execute()
# Insert 100 productsproducts= [
Product(
name=f"Product {i}",
price=i*10,
stock=100,
category=Category(name="Category")
)
foriinrange(100)
]
awaitProduct.insert_many(products)

Result:35% less code, no manual key management

Installation

PIP

pip install beanis

Poetry

poetry add beanis

Quick Start

importasynciofromtypingimportOptionalfromredis.asyncioimportRedisfrompydanticimportBaseModelfrombeanisimportDocument, init_beanis, IndexedclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strdescription: Optional[str] =Noneprice: Indexed(float) # Indexed for range queriescategory: Categorystock: int=0classSettings:
name="products"asyncdefmain():
# Initialize Redis clientclient=Redis(host="localhost", port=6379, db=0, decode_responses=True)
# Initialize Beanisawaitinit_beanis(database=client, document_models=[Product])
# Create a productchocolate=Category(
name="Chocolate",
description="A preparation of roasted and ground cacao seeds."
)
product=Product(
name="Tony's Chocolonely",
price=5.95,
category=chocolate,
stock=100
)
# Insert into Redisawaitproduct.insert()
# Retrieve by IDfound=awaitProduct.get(product.id)
print(f"Found: {found.name} - ${found.price}")
# Query by price rangeaffordable=awaitProduct.find(
Product.price<10.0
).to_list()
print(f"Affordable products: {len(affordable)}")
# Update specific fieldsawaitproduct.update(price=6.95, stock=150)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", -1)
print(f"Stock after sale: {new_stock}")
# Get all productsall_products=awaitProduct.all()
print(f"Total products: {len(all_products)}")
# Deleteawaitproduct.delete_self()
awaitclient.close()
if__name__=="__main__":
asyncio.run(main())

Core Features

🚀 Type Safety & Validation

Beanis uses Pydantic models, giving you automatic validation and type checking:

classProduct(Document):
name: strprice: float# Must be a numberstock: int# Must be an integercategory: Category# Must be a valid Category object# This will raise a validation error BEFORE hitting Redisproduct=Product(
name="Invalid",
price="not a number", # ❌ ValidationError!stock=100
)

⚡ High Performance

Beanis is optimized for speed with minimal overhead:

  • 8% overhead vs vanilla Redis (benchmarked)
  • Uses msgspec for ultra-fast JSON parsing (2x faster than orjson)
  • Skips Pydantic validation on reads by default (data from Redis is trusted)
  • Efficient pipeline usage for batch operations

Benchmark Results (Get by ID):

  • Vanilla Redis: 1.00x (baseline)
  • Beanis: 1.08x (only 8% slower)
  • Redis OM: 1.20x (20% slower)

🎯 Pythonic API

Familiar Beanie-style interface for MongoDB developers:

# Query with Pythonic operatorsproducts=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0,
Product.stock>0
).to_list()
# Chaining operationsexpensive=awaitProduct.find(
Product.price>100
).sort(Product.price).limit(10).to_list()
# Batch operationsawaitProduct.insert_many([product1, product2, product3])
products=awaitProduct.get_many([id1, id2, id3])
awaitProduct.delete_many([id1, id2])

📦 Store Anything

Beanis handles complex types automatically:

Built-in support:

  • Nested Pydantic models
  • Lists, dicts, tuples, sets
  • Decimal, UUID, Enum
  • datetime, date, time, timedelta

Custom types via encoders:

frombeanisimportDocument, register_typeimportnumpyasnp# NumPy arrays work automatically (auto-registered)classMLModel(Document):
name: strweights: Any# Stores np.ndarray!model=MLModel(name="v1", weights=np.random.rand(100, 100))
awaitmodel.insert() # Just works!# Custom typesregister_type(
MyCustomType,
encoder=lambdaobj: str(obj),
decoder=lambdas: MyCustomType.from_string(s)
)

See tutorial on how to create them!

🔧 Production Ready Features

TTL Support:

# Insert with TTLawaitproduct.insert(ttl=3600) # Expires in 1 hour# Set TTL on existing documentawaitproduct.set_ttl(7200)
ttl=awaitproduct.get_ttl()
awaitproduct.persist() # Remove TTL

Event Hooks:

frombeanisimportbefore_event, after_event, Insert, UpdateclassProduct(Document):
name: strprice: float@before_event(Insert)asyncdefvalidate_price(self):
ifself.price<0:
raiseValueError("Price cannot be negative")
@after_event(Insert)asyncdeflog_creation(self):
print(f"Created product: {self.name}")

Field-Level Operations:

# Get/set single field without loading entire documentprice=awaitproduct.get_field("price")
awaitproduct.set_field("stock", 200)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", 5)

Document Tracking:

# Get all documents (sorted by insertion time)all_products=awaitProduct.all()
# Paginationpage1=awaitProduct.all(limit=10)
page2=awaitProduct.all(skip=10, limit=10)
# Count and delete allcount=awaitProduct.count()
awaitProduct.delete_all()

Comparison

FeatureVanilla RedisBeanisRedis OM
Code volume100%30%50%
Type safetyManualAutomaticAutomatic
Performance100%108%120%
Vanilla Redis❌ Requires modules
ValidationManualAutomaticAutomatic
API StyleRedis commandsPythonicRedis OM
Learning curveMediumEasyMedium
Nested objectsManualAutomaticAutomatic
Custom typesManualEasyLimited
Event hooks
All DBs (0-15)❌ DB 0 only

Choosing the Right Tool

Choose Vanilla Redis when:

  • Every microsecond matters (high-frequency trading, etc.)
  • Simple key-value storage
  • You're a Redis expert and don't need abstractions

Choose Beanis when: ⭐

  • Building production applications with complex data models
  • Want type safety + performance (8% overhead is acceptable)
  • Using vanilla Redis (no RedisJSON/RediSearch modules)
  • Need to store nested objects, custom types, NumPy arrays, etc.
  • Coming from MongoDB/Beanie and want familiar API
  • Want event hooks for validation and lifecycle management

Choose Redis OM when:

  • You need RedisJSON/RediSearch features
  • Don't mind installing Redis modules
  • Want Redis Stack integration
  • Need advanced full-text search

Requirements

  • Python 3.8+
  • Redis 5.0+
  • Pydantic 1.10+ or 2.0+

Testing

# Run tests
pytest
# Run with coverage
pytest --cov=beanis
# Run specific test
pytest tests/test_core.py::test_insert_and_get

Credits

Beanis is a fork of Beanie - the amazing MongoDB ODM created by Roman Right and contributors.

We took the Beanie codebase and completely reimagined it for Redis, replacing MongoDB operations with Redis commands while preserving the elegant API design. If you're using MongoDB, check out the original Beanie - it's awesome!

Special thanks to:

  • Roman Right and the Beanie community for creating the foundation
  • All Beanie contributors whose code inspired this project
  • The Redis and Pydantic teams for their excellent libraries

Beanie

License

Apache License 2.0

About

Asynchronous Python ODM for Redis

Resources

Code of conduct

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Beanis - Redis ODM for Humans

Beanis

PyPI versionDownloadsPython versionsLicenseGitHub stars
TestsCoverageCode style: blackPydanticRedis

Stop writing boilerplate Redis code. Focus on your application logic.

Beanis is an async Python ODM (Object-Document Mapper) for Redis that gives you Pydantic models, type safety, and a clean API - while staying fast and working with vanilla Redis.

Why Beanis?

The Problem with Vanilla Redis

Manual serialization - You write json.dumps() and json.loads() everywhere ❌ Type conversions - Strings from Redis need manual float(), int() conversions ❌ Key management - You track "Product:123", "all:Product" keys manually ❌ No validation - Bad data silently corrupts your Redis database ❌ Boilerplate code - 15-20 lines for simple CRUD operations

The Solution: Beanis

Automatic serialization - Nested objects, lists, custom types - all handled ✅ Type safety - Full Pydantic validation + IDE autocomplete ✅ Smart key management - Focus on your data, not Redis internals ✅ Data validation - Catch errors before they hit Redis ✅ Write 70% less code - 5-7 lines for the same operations

AND it's fast: Only 8% overhead vs vanilla Redis

Who Should Use Beanis?

✅ You're building a production app that needs Redis but not the boilerplate ✅ You want type safety and validation without sacrificing performance ✅ You're using vanilla Redis (no RedisJSON/RediSearch modules) ✅ You like Beanie's MongoDB API and want the same for Redis ✅ You're storing complex data (nested objects, NumPy arrays, etc.)

When NOT to Use Beanis?

❌ You need every microsecond of performance (use raw redis-py) ❌ You need RedisJSON/RediSearch features (use Redis OM) ❌ You're only storing simple key-value pairs (use raw redis-py)

Show Me The Code

Basic CRUD Operation

Vanilla Redis (20 lines)Beanis (7 lines)
importjsonimporttimefromredis.asyncioimportRedisredis=Redis(decode_responses=True)
# Insertproduct_data= {
"name": "Tony's Chocolonely",
"price": "5.95",
"category": json.dumps({
"name": "Chocolate",
"description": "Roasted cacao"
})
}
awaitredis.hset("Product:prod_123",
mapping=product_data)
awaitredis.zadd("all:Product",
{"prod_123": time.time()})
# Retrieveraw=awaitredis.hgetall("Product:prod_123")
product= {
"name": raw["name"],
"price": float(raw["price"]),
"category": json.loads(raw["category"])
}
frombeanisimportDocumentfrompydanticimportBaseModelclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strprice: floatcategory: Category# Insertproduct=Product(
name="Tony's Chocolonely",
price=5.95,
category=Category(
name="Chocolate",
description="Roasted cacao"
)
)
awaitproduct.insert()
# Retrievefound=awaitProduct.get(product.id)

Result: Type-safe, validated, 65% less code

Search/Query Operation

Vanilla Redis (25 lines)Beanis (4 lines)
# Find products between $10-50keys=awaitredis.zrangebyscore(
"idx:Product:price",
min=10.0,
max=50.0
)
# Fetch each product using pipelinepipe=redis.pipeline()
forkeyinkeys:
pipe.hgetall(f"Product:{key}")
results=awaitpipe.execute()
# Parse manuallyproducts= []
fordatainresults:
ifdata:
products.append({
"name": data["name"],
"price": float(data["price"]),
"stock": int(data["stock"]),
"category": json.loads(
data.get("category", "{}")
)
})
# Find products between $10-50products=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0
).to_list()

Result:84% less code, fully typed results

Update Operation

Vanilla Redis (10 lines)Beanis (6 lines)
# Update price and stockawaitredis.hset("Product:123", mapping={
"price": "6.95",
"stock": "150"
})
# Atomic incrementnew_stock=awaitredis.hincrby(
"Product:123",
"stock",
-1
)
# Update fieldsawaitproduct.update(
price=6.95,
stock=150
)
# Atomic incrementnew_stock=awaitproduct.increment_field(
"stock", -1
)

Result: Same functionality, cleaner API, type-safe

Batch Operations

Vanilla Redis (14 lines)Beanis (9 lines)
# Insert 100 productspipe=redis.pipeline()
foriinrange(100):
product_id=f"prod_{i}"data= {
"name": f"Product {i}",
"price": str(i*10),
"stock": "100",
"category": json.dumps({
"name": "Category"
})
}
pipe.hset(f"Product:{product_id}",
mapping=data)
pipe.zadd("all:Product",
{product_id: time.time()})
awaitpipe.execute()
# Insert 100 productsproducts= [
Product(
name=f"Product {i}",
price=i*10,
stock=100,
category=Category(name="Category")
)
foriinrange(100)
]
awaitProduct.insert_many(products)

Result:35% less code, no manual key management

Installation

PIP

pip install beanis

Poetry

poetry add beanis

Quick Start

importasynciofromtypingimportOptionalfromredis.asyncioimportRedisfrompydanticimportBaseModelfrombeanisimportDocument, init_beanis, IndexedclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strdescription: Optional[str] =Noneprice: Indexed(float) # Indexed for range queriescategory: Categorystock: int=0classSettings:
name="products"asyncdefmain():
# Initialize Redis clientclient=Redis(host="localhost", port=6379, db=0, decode_responses=True)
# Initialize Beanisawaitinit_beanis(database=client, document_models=[Product])
# Create a productchocolate=Category(
name="Chocolate",
description="A preparation of roasted and ground cacao seeds."
)
product=Product(
name="Tony's Chocolonely",
price=5.95,
category=chocolate,
stock=100
)
# Insert into Redisawaitproduct.insert()
# Retrieve by IDfound=awaitProduct.get(product.id)
print(f"Found: {found.name} - ${found.price}")
# Query by price rangeaffordable=awaitProduct.find(
Product.price<10.0
).to_list()
print(f"Affordable products: {len(affordable)}")
# Update specific fieldsawaitproduct.update(price=6.95, stock=150)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", -1)
print(f"Stock after sale: {new_stock}")
# Get all productsall_products=awaitProduct.all()
print(f"Total products: {len(all_products)}")
# Deleteawaitproduct.delete_self()
awaitclient.close()
if__name__=="__main__":
asyncio.run(main())

Core Features

🚀 Type Safety & Validation

Beanis uses Pydantic models, giving you automatic validation and type checking:

classProduct(Document):
name: strprice: float# Must be a numberstock: int# Must be an integercategory: Category# Must be a valid Category object# This will raise a validation error BEFORE hitting Redisproduct=Product(
name="Invalid",
price="not a number", # ❌ ValidationError!stock=100
)

⚡ High Performance

Beanis is optimized for speed with minimal overhead:

  • 8% overhead vs vanilla Redis (benchmarked)
  • Uses msgspec for ultra-fast JSON parsing (2x faster than orjson)
  • Skips Pydantic validation on reads by default (data from Redis is trusted)
  • Efficient pipeline usage for batch operations

Benchmark Results (Get by ID):

  • Vanilla Redis: 1.00x (baseline)
  • Beanis: 1.08x (only 8% slower)
  • Redis OM: 1.20x (20% slower)

🎯 Pythonic API

Familiar Beanie-style interface for MongoDB developers:

# Query with Pythonic operatorsproducts=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0,
Product.stock>0
).to_list()
# Chaining operationsexpensive=awaitProduct.find(
Product.price>100
).sort(Product.price).limit(10).to_list()
# Batch operationsawaitProduct.insert_many([product1, product2, product3])
products=awaitProduct.get_many([id1, id2, id3])
awaitProduct.delete_many([id1, id2])

📦 Store Anything

Beanis handles complex types automatically:

Built-in support:

  • Nested Pydantic models
  • Lists, dicts, tuples, sets
  • Decimal, UUID, Enum
  • datetime, date, time, timedelta

Custom types via encoders:

frombeanisimportDocument, register_typeimportnumpyasnp# NumPy arrays work automatically (auto-registered)classMLModel(Document):
name: strweights: Any# Stores np.ndarray!model=MLModel(name="v1", weights=np.random.rand(100, 100))
awaitmodel.insert() # Just works!# Custom typesregister_type(
MyCustomType,
encoder=lambdaobj: str(obj),
decoder=lambdas: MyCustomType.from_string(s)
)

See tutorial on how to create them!

🔧 Production Ready Features

TTL Support:

# Insert with TTLawaitproduct.insert(ttl=3600) # Expires in 1 hour# Set TTL on existing documentawaitproduct.set_ttl(7200)
ttl=awaitproduct.get_ttl()
awaitproduct.persist() # Remove TTL

Event Hooks:

frombeanisimportbefore_event, after_event, Insert, UpdateclassProduct(Document):
name: strprice: float@before_event(Insert)asyncdefvalidate_price(self):
ifself.price<0:
raiseValueError("Price cannot be negative")
@after_event(Insert)asyncdeflog_creation(self):
print(f"Created product: {self.name}")

Field-Level Operations:

# Get/set single field without loading entire documentprice=awaitproduct.get_field("price")
awaitproduct.set_field("stock", 200)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", 5)

Document Tracking:

# Get all documents (sorted by insertion time)all_products=awaitProduct.all()
# Paginationpage1=awaitProduct.all(limit=10)
page2=awaitProduct.all(skip=10, limit=10)
# Count and delete allcount=awaitProduct.count()
awaitProduct.delete_all()

Comparison

FeatureVanilla RedisBeanisRedis OM
Code volume100%30%50%
Type safetyManualAutomaticAutomatic
Performance100%108%120%
Vanilla Redis❌ Requires modules
ValidationManualAutomaticAutomatic
API StyleRedis commandsPythonicRedis OM
Learning curveMediumEasyMedium
Nested objectsManualAutomaticAutomatic
Custom typesManualEasyLimited
Event hooks
All DBs (0-15)❌ DB 0 only

Choosing the Right Tool

Choose Vanilla Redis when:

  • Every microsecond matters (high-frequency trading, etc.)
  • Simple key-value storage
  • You're a Redis expert and don't need abstractions

Choose Beanis when: ⭐

  • Building production applications with complex data models
  • Want type safety + performance (8% overhead is acceptable)
  • Using vanilla Redis (no RedisJSON/RediSearch modules)
  • Need to store nested objects, custom types, NumPy arrays, etc.
  • Coming from MongoDB/Beanie and want familiar API
  • Want event hooks for validation and lifecycle management

Choose Redis OM when:

  • You need RedisJSON/RediSearch features
  • Don't mind installing Redis modules
  • Want Redis Stack integration
  • Need advanced full-text search

Requirements

  • Python 3.8+
  • Redis 5.0+
  • Pydantic 1.10+ or 2.0+

Testing

# Run tests
pytest
# Run with coverage
pytest --cov=beanis
# Run specific test
pytest tests/test_core.py::test_insert_and_get

Credits

Beanis is a fork of Beanie - the amazing MongoDB ODM created by Roman Right and contributors.

We took the Beanie codebase and completely reimagined it for Redis, replacing MongoDB operations with Redis commands while preserving the elegant API design. If you're using MongoDB, check out the original Beanie - it's awesome!

Special thanks to:

  • Roman Right and the Beanie community for creating the foundation
  • All Beanie contributors whose code inspired this project
  • The Redis and Pydantic teams for their excellent libraries

Beanie

License

Apache License 2.0

About

Asynchronous Python ODM for Redis

Resources

Code of conduct

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

Beanis - Redis ODM for Humans

Beanis

PyPI versionDownloadsPython versionsLicenseGitHub stars
TestsCoverageCode style: blackPydanticRedis

Stop writing boilerplate Redis code. Focus on your application logic.

Beanis is an async Python ODM (Object-Document Mapper) for Redis that gives you Pydantic models, type safety, and a clean API - while staying fast and working with vanilla Redis.

Why Beanis?

The Problem with Vanilla Redis

Manual serialization - You write json.dumps() and json.loads() everywhere ❌ Type conversions - Strings from Redis need manual float(), int() conversions ❌ Key management - You track "Product:123", "all:Product" keys manually ❌ No validation - Bad data silently corrupts your Redis database ❌ Boilerplate code - 15-20 lines for simple CRUD operations

The Solution: Beanis

Automatic serialization - Nested objects, lists, custom types - all handled ✅ Type safety - Full Pydantic validation + IDE autocomplete ✅ Smart key management - Focus on your data, not Redis internals ✅ Data validation - Catch errors before they hit Redis ✅ Write 70% less code - 5-7 lines for the same operations

AND it's fast: Only 8% overhead vs vanilla Redis

Who Should Use Beanis?

✅ You're building a production app that needs Redis but not the boilerplate ✅ You want type safety and validation without sacrificing performance ✅ You're using vanilla Redis (no RedisJSON/RediSearch modules) ✅ You like Beanie's MongoDB API and want the same for Redis ✅ You're storing complex data (nested objects, NumPy arrays, etc.)

When NOT to Use Beanis?

❌ You need every microsecond of performance (use raw redis-py) ❌ You need RedisJSON/RediSearch features (use Redis OM) ❌ You're only storing simple key-value pairs (use raw redis-py)

Show Me The Code

Basic CRUD Operation

Vanilla Redis (20 lines)Beanis (7 lines)
importjsonimporttimefromredis.asyncioimportRedisredis=Redis(decode_responses=True)
# Insertproduct_data= {
"name": "Tony's Chocolonely",
"price": "5.95",
"category": json.dumps({
"name": "Chocolate",
"description": "Roasted cacao"
})
}
awaitredis.hset("Product:prod_123",
mapping=product_data)
awaitredis.zadd("all:Product",
{"prod_123": time.time()})
# Retrieveraw=awaitredis.hgetall("Product:prod_123")
product= {
"name": raw["name"],
"price": float(raw["price"]),
"category": json.loads(raw["category"])
}
frombeanisimportDocumentfrompydanticimportBaseModelclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strprice: floatcategory: Category# Insertproduct=Product(
name="Tony's Chocolonely",
price=5.95,
category=Category(
name="Chocolate",
description="Roasted cacao"
)
)
awaitproduct.insert()
# Retrievefound=awaitProduct.get(product.id)

Result: Type-safe, validated, 65% less code

Search/Query Operation

Vanilla Redis (25 lines)Beanis (4 lines)
# Find products between $10-50keys=awaitredis.zrangebyscore(
"idx:Product:price",
min=10.0,
max=50.0
)
# Fetch each product using pipelinepipe=redis.pipeline()
forkeyinkeys:
pipe.hgetall(f"Product:{key}")
results=awaitpipe.execute()
# Parse manuallyproducts= []
fordatainresults:
ifdata:
products.append({
"name": data["name"],
"price": float(data["price"]),
"stock": int(data["stock"]),
"category": json.loads(
data.get("category", "{}")
)
})
# Find products between $10-50products=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0
).to_list()

Result:84% less code, fully typed results

Update Operation

Vanilla Redis (10 lines)Beanis (6 lines)
# Update price and stockawaitredis.hset("Product:123", mapping={
"price": "6.95",
"stock": "150"
})
# Atomic incrementnew_stock=awaitredis.hincrby(
"Product:123",
"stock",
-1
)
# Update fieldsawaitproduct.update(
price=6.95,
stock=150
)
# Atomic incrementnew_stock=awaitproduct.increment_field(
"stock", -1
)

Result: Same functionality, cleaner API, type-safe

Batch Operations

Vanilla Redis (14 lines)Beanis (9 lines)
# Insert 100 productspipe=redis.pipeline()
foriinrange(100):
product_id=f"prod_{i}"data= {
"name": f"Product {i}",
"price": str(i*10),
"stock": "100",
"category": json.dumps({
"name": "Category"
})
}
pipe.hset(f"Product:{product_id}",
mapping=data)
pipe.zadd("all:Product",
{product_id: time.time()})
awaitpipe.execute()
# Insert 100 productsproducts= [
Product(
name=f"Product {i}",
price=i*10,
stock=100,
category=Category(name="Category")
)
foriinrange(100)
]
awaitProduct.insert_many(products)

Result:35% less code, no manual key management

Installation

PIP

pip install beanis

Poetry

poetry add beanis

Quick Start

importasynciofromtypingimportOptionalfromredis.asyncioimportRedisfrompydanticimportBaseModelfrombeanisimportDocument, init_beanis, IndexedclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strdescription: Optional[str] =Noneprice: Indexed(float) # Indexed for range queriescategory: Categorystock: int=0classSettings:
name="products"asyncdefmain():
# Initialize Redis clientclient=Redis(host="localhost", port=6379, db=0, decode_responses=True)
# Initialize Beanisawaitinit_beanis(database=client, document_models=[Product])
# Create a productchocolate=Category(
name="Chocolate",
description="A preparation of roasted and ground cacao seeds."
)
product=Product(
name="Tony's Chocolonely",
price=5.95,
category=chocolate,
stock=100
)
# Insert into Redisawaitproduct.insert()
# Retrieve by IDfound=awaitProduct.get(product.id)
print(f"Found: {found.name} - ${found.price}")
# Query by price rangeaffordable=awaitProduct.find(
Product.price<10.0
).to_list()
print(f"Affordable products: {len(affordable)}")
# Update specific fieldsawaitproduct.update(price=6.95, stock=150)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", -1)
print(f"Stock after sale: {new_stock}")
# Get all productsall_products=awaitProduct.all()
print(f"Total products: {len(all_products)}")
# Deleteawaitproduct.delete_self()
awaitclient.close()
if__name__=="__main__":
asyncio.run(main())

Core Features

🚀 Type Safety & Validation

Beanis uses Pydantic models, giving you automatic validation and type checking:

classProduct(Document):
name: strprice: float# Must be a numberstock: int# Must be an integercategory: Category# Must be a valid Category object# This will raise a validation error BEFORE hitting Redisproduct=Product(
name="Invalid",
price="not a number", # ❌ ValidationError!stock=100
)

⚡ High Performance

Beanis is optimized for speed with minimal overhead:

  • 8% overhead vs vanilla Redis (benchmarked)
  • Uses msgspec for ultra-fast JSON parsing (2x faster than orjson)
  • Skips Pydantic validation on reads by default (data from Redis is trusted)
  • Efficient pipeline usage for batch operations

Benchmark Results (Get by ID):

  • Vanilla Redis: 1.00x (baseline)
  • Beanis: 1.08x (only 8% slower)
  • Redis OM: 1.20x (20% slower)

🎯 Pythonic API

Familiar Beanie-style interface for MongoDB developers:

# Query with Pythonic operatorsproducts=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0,
Product.stock>0
).to_list()
# Chaining operationsexpensive=awaitProduct.find(
Product.price>100
).sort(Product.price).limit(10).to_list()
# Batch operationsawaitProduct.insert_many([product1, product2, product3])
products=awaitProduct.get_many([id1, id2, id3])
awaitProduct.delete_many([id1, id2])

📦 Store Anything

Beanis handles complex types automatically:

Built-in support:

  • Nested Pydantic models
  • Lists, dicts, tuples, sets
  • Decimal, UUID, Enum
  • datetime, date, time, timedelta

Custom types via encoders:

frombeanisimportDocument, register_typeimportnumpyasnp# NumPy arrays work automatically (auto-registered)classMLModel(Document):
name: strweights: Any# Stores np.ndarray!model=MLModel(name="v1", weights=np.random.rand(100, 100))
awaitmodel.insert() # Just works!# Custom typesregister_type(
MyCustomType,
encoder=lambdaobj: str(obj),
decoder=lambdas: MyCustomType.from_string(s)
)

See tutorial on how to create them!

🔧 Production Ready Features

TTL Support:

# Insert with TTLawaitproduct.insert(ttl=3600) # Expires in 1 hour# Set TTL on existing documentawaitproduct.set_ttl(7200)
ttl=awaitproduct.get_ttl()
awaitproduct.persist() # Remove TTL

Event Hooks:

frombeanisimportbefore_event, after_event, Insert, UpdateclassProduct(Document):
name: strprice: float@before_event(Insert)asyncdefvalidate_price(self):
ifself.price<0:
raiseValueError("Price cannot be negative")
@after_event(Insert)asyncdeflog_creation(self):
print(f"Created product: {self.name}")

Field-Level Operations:

# Get/set single field without loading entire documentprice=awaitproduct.get_field("price")
awaitproduct.set_field("stock", 200)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", 5)

Document Tracking:

# Get all documents (sorted by insertion time)all_products=awaitProduct.all()
# Paginationpage1=awaitProduct.all(limit=10)
page2=awaitProduct.all(skip=10, limit=10)
# Count and delete allcount=awaitProduct.count()
awaitProduct.delete_all()

Comparison

FeatureVanilla RedisBeanisRedis OM
Code volume100%30%50%
Type safetyManualAutomaticAutomatic
Performance100%108%120%
Vanilla Redis❌ Requires modules
ValidationManualAutomaticAutomatic
API StyleRedis commandsPythonicRedis OM
Learning curveMediumEasyMedium
Nested objectsManualAutomaticAutomatic
Custom typesManualEasyLimited
Event hooks
All DBs (0-15)❌ DB 0 only

Choosing the Right Tool

Choose Vanilla Redis when:

  • Every microsecond matters (high-frequency trading, etc.)
  • Simple key-value storage
  • You're a Redis expert and don't need abstractions

Choose Beanis when: ⭐

  • Building production applications with complex data models
  • Want type safety + performance (8% overhead is acceptable)
  • Using vanilla Redis (no RedisJSON/RediSearch modules)
  • Need to store nested objects, custom types, NumPy arrays, etc.
  • Coming from MongoDB/Beanie and want familiar API
  • Want event hooks for validation and lifecycle management

Choose Redis OM when:

  • You need RedisJSON/RediSearch features
  • Don't mind installing Redis modules
  • Want Redis Stack integration
  • Need advanced full-text search

Requirements

  • Python 3.8+
  • Redis 5.0+
  • Pydantic 1.10+ or 2.0+

Testing

# Run tests
pytest
# Run with coverage
pytest --cov=beanis
# Run specific test
pytest tests/test_core.py::test_insert_and_get

Credits

Beanis is a fork of Beanie - the amazing MongoDB ODM created by Roman Right and contributors.

We took the Beanie codebase and completely reimagined it for Redis, replacing MongoDB operations with Redis commands while preserving the elegant API design. If you're using MongoDB, check out the original Beanie - it's awesome!

Special thanks to:

  • Roman Right and the Beanie community for creating the foundation
  • All Beanie contributors whose code inspired this project
  • The Redis and Pydantic teams for their excellent libraries

Beanie

License

Apache License 2.0

About

Asynchronous Python ODM for Redis

Resources

Code of conduct

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

Beanis - Redis ODM for Humans

Beanis

PyPI versionDownloadsPython versionsLicenseGitHub stars
TestsCoverageCode style: blackPydanticRedis

Stop writing boilerplate Redis code. Focus on your application logic.

Beanis is an async Python ODM (Object-Document Mapper) for Redis that gives you Pydantic models, type safety, and a clean API - while staying fast and working with vanilla Redis.

Why Beanis?

The Problem with Vanilla Redis

Manual serialization - You write json.dumps() and json.loads() everywhere ❌ Type conversions - Strings from Redis need manual float(), int() conversions ❌ Key management - You track "Product:123", "all:Product" keys manually ❌ No validation - Bad data silently corrupts your Redis database ❌ Boilerplate code - 15-20 lines for simple CRUD operations

The Solution: Beanis

Automatic serialization - Nested objects, lists, custom types - all handled ✅ Type safety - Full Pydantic validation + IDE autocomplete ✅ Smart key management - Focus on your data, not Redis internals ✅ Data validation - Catch errors before they hit Redis ✅ Write 70% less code - 5-7 lines for the same operations

AND it's fast: Only 8% overhead vs vanilla Redis

Who Should Use Beanis?

✅ You're building a production app that needs Redis but not the boilerplate ✅ You want type safety and validation without sacrificing performance ✅ You're using vanilla Redis (no RedisJSON/RediSearch modules) ✅ You like Beanie's MongoDB API and want the same for Redis ✅ You're storing complex data (nested objects, NumPy arrays, etc.)

When NOT to Use Beanis?

❌ You need every microsecond of performance (use raw redis-py) ❌ You need RedisJSON/RediSearch features (use Redis OM) ❌ You're only storing simple key-value pairs (use raw redis-py)

Show Me The Code

Basic CRUD Operation

Vanilla Redis (20 lines)Beanis (7 lines)
importjsonimporttimefromredis.asyncioimportRedisredis=Redis(decode_responses=True)
# Insertproduct_data= {
"name": "Tony's Chocolonely",
"price": "5.95",
"category": json.dumps({
"name": "Chocolate",
"description": "Roasted cacao"
})
}
awaitredis.hset("Product:prod_123",
mapping=product_data)
awaitredis.zadd("all:Product",
{"prod_123": time.time()})
# Retrieveraw=awaitredis.hgetall("Product:prod_123")
product= {
"name": raw["name"],
"price": float(raw["price"]),
"category": json.loads(raw["category"])
}
frombeanisimportDocumentfrompydanticimportBaseModelclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strprice: floatcategory: Category# Insertproduct=Product(
name="Tony's Chocolonely",
price=5.95,
category=Category(
name="Chocolate",
description="Roasted cacao"
)
)
awaitproduct.insert()
# Retrievefound=awaitProduct.get(product.id)

Result: Type-safe, validated, 65% less code

Search/Query Operation

Vanilla Redis (25 lines)Beanis (4 lines)
# Find products between $10-50keys=awaitredis.zrangebyscore(
"idx:Product:price",
min=10.0,
max=50.0
)
# Fetch each product using pipelinepipe=redis.pipeline()
forkeyinkeys:
pipe.hgetall(f"Product:{key}")
results=awaitpipe.execute()
# Parse manuallyproducts= []
fordatainresults:
ifdata:
products.append({
"name": data["name"],
"price": float(data["price"]),
"stock": int(data["stock"]),
"category": json.loads(
data.get("category", "{}")
)
})
# Find products between $10-50products=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0
).to_list()

Result:84% less code, fully typed results

Update Operation

Vanilla Redis (10 lines)Beanis (6 lines)
# Update price and stockawaitredis.hset("Product:123", mapping={
"price": "6.95",
"stock": "150"
})
# Atomic incrementnew_stock=awaitredis.hincrby(
"Product:123",
"stock",
-1
)
# Update fieldsawaitproduct.update(
price=6.95,
stock=150
)
# Atomic incrementnew_stock=awaitproduct.increment_field(
"stock", -1
)

Result: Same functionality, cleaner API, type-safe

Batch Operations

Vanilla Redis (14 lines)Beanis (9 lines)
# Insert 100 productspipe=redis.pipeline()
foriinrange(100):
product_id=f"prod_{i}"data= {
"name": f"Product {i}",
"price": str(i*10),
"stock": "100",
"category": json.dumps({
"name": "Category"
})
}
pipe.hset(f"Product:{product_id}",
mapping=data)
pipe.zadd("all:Product",
{product_id: time.time()})
awaitpipe.execute()
# Insert 100 productsproducts= [
Product(
name=f"Product {i}",
price=i*10,
stock=100,
category=Category(name="Category")
)
foriinrange(100)
]
awaitProduct.insert_many(products)

Result:35% less code, no manual key management

Installation

PIP

pip install beanis

Poetry

poetry add beanis

Quick Start

importasynciofromtypingimportOptionalfromredis.asyncioimportRedisfrompydanticimportBaseModelfrombeanisimportDocument, init_beanis, IndexedclassCategory(BaseModel):
name: strdescription: strclassProduct(Document):
name: strdescription: Optional[str] =Noneprice: Indexed(float) # Indexed for range queriescategory: Categorystock: int=0classSettings:
name="products"asyncdefmain():
# Initialize Redis clientclient=Redis(host="localhost", port=6379, db=0, decode_responses=True)
# Initialize Beanisawaitinit_beanis(database=client, document_models=[Product])
# Create a productchocolate=Category(
name="Chocolate",
description="A preparation of roasted and ground cacao seeds."
)
product=Product(
name="Tony's Chocolonely",
price=5.95,
category=chocolate,
stock=100
)
# Insert into Redisawaitproduct.insert()
# Retrieve by IDfound=awaitProduct.get(product.id)
print(f"Found: {found.name} - ${found.price}")
# Query by price rangeaffordable=awaitProduct.find(
Product.price<10.0
).to_list()
print(f"Affordable products: {len(affordable)}")
# Update specific fieldsawaitproduct.update(price=6.95, stock=150)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", -1)
print(f"Stock after sale: {new_stock}")
# Get all productsall_products=awaitProduct.all()
print(f"Total products: {len(all_products)}")
# Deleteawaitproduct.delete_self()
awaitclient.close()
if__name__=="__main__":
asyncio.run(main())

Core Features

🚀 Type Safety & Validation

Beanis uses Pydantic models, giving you automatic validation and type checking:

classProduct(Document):
name: strprice: float# Must be a numberstock: int# Must be an integercategory: Category# Must be a valid Category object# This will raise a validation error BEFORE hitting Redisproduct=Product(
name="Invalid",
price="not a number", # ❌ ValidationError!stock=100
)

⚡ High Performance

Beanis is optimized for speed with minimal overhead:

  • 8% overhead vs vanilla Redis (benchmarked)
  • Uses msgspec for ultra-fast JSON parsing (2x faster than orjson)
  • Skips Pydantic validation on reads by default (data from Redis is trusted)
  • Efficient pipeline usage for batch operations

Benchmark Results (Get by ID):

  • Vanilla Redis: 1.00x (baseline)
  • Beanis: 1.08x (only 8% slower)
  • Redis OM: 1.20x (20% slower)

🎯 Pythonic API

Familiar Beanie-style interface for MongoDB developers:

# Query with Pythonic operatorsproducts=awaitProduct.find(
Product.price>=10.0,
Product.price<=50.0,
Product.stock>0
).to_list()
# Chaining operationsexpensive=awaitProduct.find(
Product.price>100
).sort(Product.price).limit(10).to_list()
# Batch operationsawaitProduct.insert_many([product1, product2, product3])
products=awaitProduct.get_many([id1, id2, id3])
awaitProduct.delete_many([id1, id2])

📦 Store Anything

Beanis handles complex types automatically:

Built-in support:

  • Nested Pydantic models
  • Lists, dicts, tuples, sets
  • Decimal, UUID, Enum
  • datetime, date, time, timedelta

Custom types via encoders:

frombeanisimportDocument, register_typeimportnumpyasnp# NumPy arrays work automatically (auto-registered)classMLModel(Document):
name: strweights: Any# Stores np.ndarray!model=MLModel(name="v1", weights=np.random.rand(100, 100))
awaitmodel.insert() # Just works!# Custom typesregister_type(
MyCustomType,
encoder=lambdaobj: str(obj),
decoder=lambdas: MyCustomType.from_string(s)
)

See tutorial on how to create them!

🔧 Production Ready Features

TTL Support:

# Insert with TTLawaitproduct.insert(ttl=3600) # Expires in 1 hour# Set TTL on existing documentawaitproduct.set_ttl(7200)
ttl=awaitproduct.get_ttl()
awaitproduct.persist() # Remove TTL

Event Hooks:

frombeanisimportbefore_event, after_event, Insert, UpdateclassProduct(Document):
name: strprice: float@before_event(Insert)asyncdefvalidate_price(self):
ifself.price<0:
raiseValueError("Price cannot be negative")
@after_event(Insert)asyncdeflog_creation(self):
print(f"Created product: {self.name}")

Field-Level Operations:

# Get/set single field without loading entire documentprice=awaitproduct.get_field("price")
awaitproduct.set_field("stock", 200)
# Atomic incrementnew_stock=awaitproduct.increment_field("stock", 5)

Document Tracking:

# Get all documents (sorted by insertion time)all_products=awaitProduct.all()
# Paginationpage1=awaitProduct.all(limit=10)
page2=awaitProduct.all(skip=10, limit=10)
# Count and delete allcount=awaitProduct.count()
awaitProduct.delete_all()

Comparison

FeatureVanilla RedisBeanisRedis OM
Code volume100%30%50%
Type safetyManualAutomaticAutomatic
Performance100%108%120%
Vanilla Redis❌ Requires modules
ValidationManualAutomaticAutomatic
API StyleRedis commandsPythonicRedis OM
Learning curveMediumEasyMedium
Nested objectsManualAutomaticAutomatic
Custom typesManualEasyLimited
Event hooks
All DBs (0-15)❌ DB 0 only

Choosing the Right Tool

Choose Vanilla Redis when:

  • Every microsecond matters (high-frequency trading, etc.)
  • Simple key-value storage
  • You're a Redis expert and don't need abstractions

Choose Beanis when: ⭐

  • Building production applications with complex data models
  • Want type safety + performance (8% overhead is acceptable)
  • Using vanilla Redis (no RedisJSON/RediSearch modules)
  • Need to store nested objects, custom types, NumPy arrays, etc.
  • Coming from MongoDB/Beanie and want familiar API
  • Want event hooks for validation and lifecycle management

Choose Redis OM when:

  • You need RedisJSON/RediSearch features
  • Don't mind installing Redis modules
  • Want Redis Stack integration
  • Need advanced full-text search

Requirements

  • Python 3.8+
  • Redis 5.0+
  • Pydantic 1.10+ or 2.0+

Testing

# Run tests
pytest
# Run with coverage
pytest --cov=beanis
# Run specific test
pytest tests/test_core.py::test_insert_and_get

Credits

Beanis is a fork of Beanie - the amazing MongoDB ODM created by Roman Right and contributors.

We took the Beanie codebase and completely reimagined it for Redis, replacing MongoDB operations with Redis commands while preserving the elegant API design. If you're using MongoDB, check out the original Beanie - it's awesome!

Special thanks to:

  • Roman Right and the Beanie community for creating the foundation
  • All Beanie contributors whose code inspired this project
  • The Redis and Pydantic teams for their excellent libraries

Beanie

License

Apache License 2.0

About

Asynchronous Python ODM for Redis

Resources

Code of conduct

Stars

9 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages