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.
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.
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
- You only need per-process caching in one Python app: functools.lru_cache or diskcache does the job without running and monitoring a server
- Your data is relational or needs ad hoc queries: Redis is a data-structure store, and forcing query workloads into it means rebuilding indexes by hand; use Postgres
- You moved to Valkey over the Redis licensing changes: the valkey client is the fork's own package and tracks its server, not Redis Ltd's roadmap
- You want an object-mapping layer: redis-py is deliberately low level, one method per command; redis-om adds models and validation on top if that is what you actually wanted
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
| Package | Registry | Pick it when |
|---|---|---|
| valkey | PyPI | You run the Valkey fork and want the client maintained by that project instead of Redis Ltd |
| coredis | PyPI | You want an async-first client with typed responses rather than the sync-API-mirrored redis.asyncio |
| redis-om | PyPI | You want declarative models and validation on top of Redis instead of raw command methods |