redis review
redis-py 8.1.0 is Redis's official Python client, with synchronous and asyncio command APIs plus pools, pipelines, transactions, pub/sub, streams, locks, Sentinel, Cluster, Search, Time Series, and Active-Active support. The client stays close to server command names instead of supplying an object model. Version 8.1.0 adds async maintenance notifications, list and set cardinality commands, stream limits, more Search and Time Series methods, and experimental HIMPORT fieldsets. It fixes hiredis push-data handling under RESP3 and Sentinel pool capacity after failover. Our base install was pure Python and typed.
redis 8.1.0 installed in 0.2 seconds and occupied 3 MB as one package in our sandbox, with typed pure-Python code and no audit findings. Use redis-py when the service truly depends on Redis; keep it out when a process-local cache or a relational store answers the real requirement.
We installed it
| Install | ✓ · 0.2s | 1 package on disk · 3 MB |
| Import | ✓ | import redis in 1.04s · pure Python · py.typed · requires Python >=3.10 |
| Known vulns | 0 | (pip-audit) |
Answers from our run
Does redis install cleanly?
Yes. In a fresh container with an empty cache, pip install redis finished in 0.2s, leaving 1 package and 3 MB on disk. pip-audit reported no known vulnerabilities.
What does redis need to run?
Python >=3.10, and nothing compiled: it is pure Python. In our run import redis succeeded in 1.04s, and the package ships py.typed for type checkers.
redis or valkey: which should you use?
valkey: Use it when the deployed servers and dependency policy follow the Valkey project. redis 8.1.0 installed in 0.2 seconds and occupied 3 MB as one package in our sandbox, with typed pure-Python code and no audit findings.
When should you not use redis?
The cache never leaves one process. functools.cache or a local cache removes a server, network hop, credentials, monitoring, and failure mode.
Use it if
- A Python service already uses Redis for caching, counters, streams, queues, pub/sub, sessions, or coordination.
- The project needs current Redis command coverage with similar method names in synchronous and asyncio code.
- Connection pools, retry rules, Sentinel or Cluster routing, pipelines, and module commands should live in one official client.
- The team is prepared to choose response decoding and RESP compatibility explicitly during the 8.x migration.
- The cache never leaves one process. `functools.cache` or a local cache removes a server, network hop, credentials, monitoring, and failure mode.
- The data model needs joins, foreign keys, or ad hoc relational queries. Redis structures and this command client do not supply those constraints.
- Infrastructure has standardized on Valkey and project ownership matters. The `valkey` client follows that fork's governance and release direction.
- Application code wants validated models and indexed objects rather than server commands. `redis-om` adds that higher-level contract.
- Response-shape changes cannot be tested. redis-py 8 uses RESP3 on the wire by default while retaining legacy-compatible Python values unless you opt out.
Setup reality
We installed redis 8.1.0 in a fresh Python 3.12 Bookworm sandbox. pip completed in 0.2 seconds and left one package using 3 MB. The distribution declares 11 direct dependencies, requires Python 3.10 or later, is pure Python, and ships py.typed. import redis succeeded in 1.04 seconds. pip-audit found zero known vulnerabilities, and the project uses the MIT License. Installing redis[hiredis] would add a compiled parser and change that deployment profile.
The package does not start Redis. Supply a reachable server URL, authentication, TLS trust, and database choice. redis:// is plaintext; rediss:// uses TLS. Replies are bytes unless decode_responses=True. In version 8, the default wire protocol is RESP3, while legacy_responses=True preserves older Python shapes. Set protocol=2 for RESP2 or legacy_responses=False when new code should adopt unified response values deliberately.
Each Redis client owns a pool unless one is supplied. Build a long-lived client or shared pool per process, bound max_connections, and close async clients with aclose(). Pipelines wrap commands in MULTI/EXEC by default; transaction=False only batches network round trips. Pub/sub consumes a dedicated connection and drops messages for disconnected subscribers. scan_iter avoids the blocking KEYS command, but it may repeat keys while the dataset changes.
Retries can duplicate non-idempotent writes or extend request latency. A Redis lock's timeout is its lease duration, not how long acquisition waits; work that outlives the lease can overlap another holder. Version 8.1.0's HIMPORT fieldset support is explicitly experimental and stores preparation state per physical connection. Direct calls can reprepare after reconnects, but a pipeline must include PREPARE and its SET operations on the same connection.
Patterns
Return strings instead of bytes connect-and-decode
import redis
client = redis.Redis(
host='localhost',
port=6379,
decode_responses=True,
)
client.set('greeting', 'hello')
assert client.get('greeting') == 'hello'Without `decode_responses=True`, string commands normally return bytes. Keep binary payload clients undecoded.
Connect with TLS from a URL tls-url
import os
import redis
client = redis.Redis.from_url(
os.environ['REDIS_URL'],
decode_responses=True,
ssl_ca_certs='/etc/ssl/redis-ca.pem',
)Use a `rediss://` URL for TLS. The URL path selects a logical database on deployments that support multiple databases.
Close an asyncio client async-lifecycle
import redis.asyncio as redis
async def round_trip() -> str | None:
client = redis.Redis(decode_responses=True)
try:
await client.set('key', 'value')
return await client.get('key')
finally:
await client.aclose()The supported async namespace is `redis.asyncio`. Old code importing the separate aioredis package is obsolete.
Batch without a transaction pipeline-batch
with client.pipeline(transaction=False) as pipe:
pipe.set('a', 1)
pipe.incr('a')
results = pipe.execute()Pipelines default to MULTI/EXEC. Set `transaction=False` only when reducing round trips is enough and atomic execution is unnecessary.
Store and retrieve a hash hash-mapping
client.hset(
'user:42',
mapping={'name': 'Ada', 'plan': 'pro'},
)
user = client.hgetall('user:42')Current code uses `hset(..., mapping=...)`; copied `hmset` examples target a deprecated command wrapper.
Create an expiring key once set-with-expiry
created = client.set(
'job:claim:42',
'worker-1',
nx=True,
ex=30,
)
if created is None:
skip_claimed_job()The 30-second expiry is a lease. If work can exceed it, another worker may acquire the same key before the first finishes.
Walk matching keys incrementally scan-keys
seen = set()
for key in client.scan_iter(match='session:*', count=500):
if key in seen:
continue
seen.add(key)
process(key)SCAN's count is a hint and results may repeat while keys change. Avoid the blocking KEYS command on production datasets.
Listen on a pub/sub channel pubsub-listener
with client.pubsub(ignore_subscribe_messages=True) as pubsub:
pubsub.subscribe('alerts')
for message in pubsub.listen():
handle(message['data'])Pub/sub does not store messages for disconnected consumers. Use Redis streams when recovery and acknowledgement matter.
Bound a process-wide pool shared-pool
import redis
pool = redis.ConnectionPool(
host='localhost',
max_connections=20,
decode_responses=True,
)
client = redis.Redis(connection_pool=pool)Every Redis instance otherwise receives its own pool. Reuse the client or explicit pool instead of constructing one per web request.
Opt into unified RESP responses response-migration
client = redis.Redis(
host='localhost',
protocol=3,
legacy_responses=False,
decode_responses=True,
)redis-py 8 defaults to RESP3 on the wire but keeps legacy Python shapes. This makes both migration choices explicit.
Update a value with WATCH optimistic-transaction
from redis.exceptions import WatchError
while True:
try:
with client.pipeline() as pipe:
pipe.watch('balance:42')
current = int(pipe.get('balance:42') or 0)
pipe.multi()
pipe.set('balance:42', current + 10)
pipe.execute()
break
except WatchError:
continueWATCH detects a concurrent modification before EXEC. Keep the retry body free of external side effects because it may run more than once.
Read and acknowledge stream entries stream-consumer-group
messages = client.xreadgroup(
groupname='workers',
consumername='worker-1',
streams={'jobs': '>'},
count=10,
block=5000,
)
for stream, entries in messages:
for message_id, fields in entries:
process(fields)
client.xack(stream, 'workers', message_id)A crash before XACK leaves the entry pending. Add pending-entry recovery instead of reading only new `>` messages forever.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| valkey | PyPI | Use it when the deployed servers and dependency policy follow the Valkey project. |
| coredis | PyPI | Choose it for an async-first, typing-focused Redis client after checking command and deployment coverage. |
| redis-om | PyPI | Use it when Pydantic-style models and indexed object access are the actual application contract. |
| aiocache | PyPI | Use it when code needs a cache abstraction that can swap Redis for memory or another backend. |
More data guides
numpy · fsspec · pandas · pyarrow · sqlalchemy · s3fs · the whole shelf →
How this guide is made: grounded in the library's documentation, release notes, changelog, and issue history, on a fixed rubric — not a hands-on install of every release. The 50 most-downloaded entries are additionally install-verified in clean containers. Corrections: contact the desk.

