mrkeyoor.com_
Wed 05 Aug 05:06 UTC
PyPIDataupdated 05 Aug 2026

redis

redis-py is the official Python client for the Redis key-value store (this page covers the PyPI package named redis; there is a separate npm package with the same name for Node.js). You install it, point it at a Redis server, and every Redis command becomes a Python method: r.set, r.get, r.hset, and so on. It ships sync and asyncio clients, connection pooling, pipelines, pub/sub, streams, and cluster support in one package, and it is the client that Celery, RQ, django-redis, and most other Python tooling build on.

Verdict

If you talk to Redis from Python, this is the client; it is official, everywhere, and actively maintained. The real decision is not the library but whether you run Redis or Valkey on the server side.

API stability4/5Command methods map one to one to Redis and rarely change, but recent majors arrived quickly (6.x then 8.x) with a RESP3 wire default and a response-shape migration flag to track
Docs3/5readthedocs covers the API and has good asyncio and connection examples, but conceptual guides are split between redis.io and readthedocs, and migration notes assume you already know the RESP2/RESP3 story
Maintenance5/5Official Redis Ltd project, pushed within the last day as of this review, regular releases tracking new server versions up to Redis 8.x
Ecosystem5/5The default Redis client for Python; Celery, RQ, django-redis, and most caching and queue tooling in the ecosystem sit on top of it

Use it if

  • You need caching, sessions, rate limiting, or counters backed by a Redis (or Redis-compatible) server
  • Your stack already depends on it indirectly through Celery, RQ, or django-redis and you want direct access too
  • You have an asyncio codebase; redis.asyncio mirrors the sync API almost method for method
  • You want pub/sub, distributed locks, or streams without adding a full message broker
Skip it if

Setup reality

pip install redis takes seconds; the friction is everything around it. You need a running server (docker run -p 6379:6379 redis works). Responses come back as bytes unless you remember decode_responses=True, which bites almost every new user. Since 8.0 the wire protocol defaults to RESP3 while keeping legacy response shapes, and new projects are told to set legacy_responses=False, so there is now a compatibility flag to think about. Old tutorials still show hmset and the separate aioredis package, both long gone. For speed you want the hiredis extra, which is a compiled parser and one more thing in your lockfile.

Patterns

Connect and read strings backconnect-get-set

import redis

r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
r.set("greeting", "hello")
print(r.get("greeting"))  # 'hello'

Without decode_responses=True every reply is bytes (b'hello'), which is the single most common surprise with this library.

Connect from a URLconnect-from-url

import redis

r = redis.Redis.from_url("redis://:s3cret@cache.internal:6379/1", decode_responses=True)

The database number rides in the path (/1). Use rediss:// (double s) for TLS; plain redis:// sends the password unencrypted.

asyncio clientasync-client

import asyncio
import redis.asyncio as redis

async def main():
    r = redis.Redis(decode_responses=True)
    await r.set("k", "v")
    print(await r.get("k"))
    await r.aclose()

asyncio.run(main())

The old standalone aioredis package was merged into redis-py years ago; ignore tutorials that import aioredis. Close with aclose(), not close().

Batch commands in a pipelinepipeline-batch

pipe = r.pipeline()
pipe.set("a", 1)
pipe.incr("a")
pipe.get("a")
results = pipe.execute()  # [True, 2, '2']

pipeline() wraps commands in MULTI/EXEC by default (transaction=True); pass transaction=False if you only want to save round trips without atomicity.

Store a dict as a hashhash-mapping

r.hset("user:42", mapping={"name": "Keyoor", "plan": "pro"})
user = r.hgetall("user:42")  # {'name': 'Keyoor', 'plan': 'pro'}

hmset was removed; hset with mapping= is the current form. Hash values are always strings on the server, so numbers come back as strings.

Set with TTL and set-if-absentkey-expiry

r.set("session:abc", "payload", ex=3600)      # expires in 1h
created = r.set("lock:init", "1", nx=True, ex=30)
print(r.ttl("session:abc"))

ex/px/nx/xx flags on set() replace the old setex and setnx commands. nx=True returns None (not False) when the key already exists.

Iterate keys without blocking the serverscan-keys

for key in r.scan_iter(match="session:*", count=500):
    r.delete(key)

Never use r.keys() in production; it blocks the single-threaded server while it walks the whole keyspace. scan_iter pages through cursor-style.

Subscribe to a channelpubsub-listen

p = r.pubsub(ignore_subscribe_messages=True)
p.subscribe("alerts")
for message in p.listen():
    print(message["channel"], message["data"])

Without ignore_subscribe_messages you get a subscribe-confirmation message first. Pub/sub is fire-and-forget: subscribers that are offline miss messages; use streams if you need replay.

Distributed lockdistributed-lock

with r.lock("job:nightly-report", timeout=60, blocking_timeout=5):
    run_report()

timeout is how long the lock lives, not how long you wait; if the work runs longer than timeout the lock expires and a second worker can enter. blocking_timeout raises LockError if the lock cannot be acquired in time.

Share one connection poolshared-connection-pool

pool = redis.ConnectionPool(host="localhost", port=6379, max_connections=20, decode_responses=True)
r1 = redis.Redis(connection_pool=pool)
r2 = redis.Redis(connection_pool=pool)

Each Redis() instance otherwise creates its own pool; in web apps make one module-level pool (or one client) instead of a client per request.

Customize connection retriesretry-on-connection-error

from redis.retry import Retry
from redis.backoff import ExponentialBackoff

r = redis.Redis(retry=Retry(ExponentialBackoff(), retries=5))

Recent majors retry failed connections automatically, so a brief server blip may add latency instead of raising; tune or disable this deliberately rather than discovering it during an incident.

Opt out of legacy response shapes (new projects)unified-responses

r = redis.Redis(host="localhost", port=6379, legacy_responses=False, decode_responses=True)

Since 8.0 the wire protocol is RESP3 by default but replies keep old RESP2-style Python shapes for compatibility; legacy_responses=False gives the unified shapes the maintainers recommend for new code.

Alternatives

PackageRegistryPick it when
valkeyPyPIYou run the Valkey fork and want the client maintained by that project instead of Redis Ltd
coredisPyPIYou want an async-first client with typed responses rather than the sync-API-mirrored redis.asyncio
redis-omPyPIYou want declarative models and validation on top of Redis instead of raw command methods