Connecting to Redis from Python
Redis speaks a simple, text-based wire protocol called RESP over a plain TCP socket, and every official client library — including Python’s redis-py package — is really just a translator: it turns method calls like r.set(...) into RESP commands and turns the replies back into Python values. This lesson covers installing and configuring redis-py, connecting to a server, running commands from Python code, and avoiding the connection-handling mistakes that cause outages and confusing bugs in production applications. By the end you’ll understand connection pooling, string-vs-bytes handling, and how to fail gracefully when Redis is unreachable.
Overview / How it works
redis-py is the official, most widely used Python client for Redis. When you write redis.Redis(host="localhost", port=6379), you are not opening a socket yet — you’re creating a client object that lazily manages a pool of TCP connections to the server. The first time you actually call a command like r.get("user:1001:name"), the client borrows a connection from its internal pool (opening a new TCP connection if none is free), encodes the command as a RESP array of bulk strings, writes it to the socket, and blocks waiting for a reply. Redis parses the incoming bytes, executes the command, and writes back a RESP reply, which redis-py decodes into a native Python type: a simple status like OK becomes True, an integer reply becomes an int, a bulk string becomes bytes (or str if you set decode_responses=True), and a null reply becomes None.
It’s important to remember that Redis’s command execution engine is single-threaded: no matter which language or client library sends a command, the server processes commands one at a time to completion, so an individual SET or INCR issued from Python is just as atomic as one typed at redis-cli. What Python-specific code adds on top is connection management, serialization of Python objects into strings/bytes, and error handling around network failures — things the Redis server itself has no concept of.
Syntax
The general pattern for connecting is to create one client object and reuse it for the lifetime of your application:
redis.Redis(
host="localhost",
port=6379,
db=0,
password=None,
decode_responses=False,
socket_timeout=None,
socket_connect_timeout=None,
)
| Parameter | Meaning |
|---|---|
host / port |
Address of the Redis server. Defaults to localhost:6379. |
db |
Logical database index (0–15 by default). Keys in different DBs are isolated. |
password |
Password for AUTH, if the server requires one. |
decode_responses |
If True, string replies come back as str instead of bytes. |
socket_timeout |
Seconds to wait for a reply before raising a TimeoutError. |
socket_connect_timeout |
Seconds to wait when establishing the TCP connection itself. |
max_connections |
Set on a ConnectionPool; caps how many sockets the client will open. |
Examples
Example 1: A basic connection
After installing the library with pip install redis, the simplest possible program connects, writes a key, and reads it back:
import redis
r = redis.Redis(host="localhost", port=6379, db=0, decode_responses=True)
r.set("user:1001:name", "Ada Lovelace")
name = r.get("user:1001:name")
print(name)
Output:
Ada Lovelace
Because decode_responses=True was passed, r.get returns a native Python str instead of bytes, so print(name) shows the text directly rather than b'Ada Lovelace'. Under the hood, this Python code sends exactly the commands you’d type at redis-cli:
SET user:1001:name "Ada Lovelace"
GET user:1001:name
Output:
OK
"Ada Lovelace"
Example 2: Connection pooling for a busy application
Opening a new TCP connection for every request is wasteful. Instead, create one ConnectionPool at startup and share it across every request handler in your application:
import redis
pool = redis.ConnectionPool(
host="localhost",
port=6379,
db=0,
decode_responses=True,
max_connections=10,
)
r = redis.Redis(connection_pool=pool)
r.incr("pageviews:home")
r.incr("pageviews:home")
print(r.get("pageviews:home"))
Output:
2
Each call to r.incr checks out a connection from the pool, sends INCR, and returns the connection to the pool once the reply arrives — no new TCP handshake per call. The equivalent raw commands, run in sequence, look like this:
INCR pageviews:home
INCR pageviews:home
GET pageviews:home
Output:
(integer) 1
(integer) 2
"2"
Example 3: Handling a Redis outage gracefully
Network calls fail. A well-behaved application catches connection errors instead of crashing:
import redis
r = redis.Redis(host="localhost", port=6379, socket_connect_timeout=2, decode_responses=True)
try:
r.ping()
print("Connected")
except redis.exceptions.ConnectionError as exc:
print(f"Could not connect to Redis: {exc}")
Output:
Connected
PING is a cheap O(1) way to verify connectivity (and is what health checks typically use); wrapping it in try/except redis.exceptions.ConnectionError means your application can log a clear message or fall back to a degraded mode instead of throwing an unhandled traceback when Redis is temporarily down.
How it works step by step
When you call r.set("session:abc123", "active", ex=30) from Python, roughly this happens:
- The client encodes the call as a RESP command array, e.g.
*5\r\n$3\r\nSET\r\n..., including theEXoption and its value. - A connection is borrowed from the pool (or a new TCP socket is opened and, if configured, authenticated with
AUTH) and the bytes are written to it. - The Redis server’s single event loop reads the full command, executes it atomically — no other client’s command can interleave mid-execution — and sets both the value and a TTL of 30 seconds on the key.
- Redis writes a RESP simple-string reply (
+OK\r\n) back on the same socket. redis-pyparses that reply into the Python valueTrue, and the connection is returned to the pool for reuse.
The same TTL behavior applies as it would from redis-cli: EXPIRE or SET ... EX attaches the TTL, and a later plain SET on that key from either Python or redis-cli clears it unless you pass KEEPTTL. This is confirmed with TTL:
SET session:abc123 "active" EX 30
TTL session:abc123
Output:
OK
(integer) 30
Common Mistakes
Creating a new client per request. Instantiating redis.Redis(...) inside a request handler or function creates a brand-new connection pool every single call, which means a fresh TCP handshake (and possibly `AUTH`) on every request — slow, and it can exhaust the server’s maxclients limit under load. Create one client (or pool) at application startup and import/reuse it everywhere.
Ignoring bytes vs. str. By default decode_responses is False, so r.get("user:1001:name") returns b'Ada Lovelace', not 'Ada Lovelace'. Code that compares this against a plain string literal will silently fail every comparison. Set decode_responses=True when your application works with text, or decode manually with .decode("utf-8").
Scanning the keyspace with r.keys(). Just like typing KEYS * at redis-cli, calling r.keys("session:*") from Python is O(N) and blocks the single-threaded server for the entire scan — dangerous on a nontrivial dataset. Use the cursor-based, non-blocking iterator instead:
# Avoid in production:
# matches = r.keys("session:*")
# Prefer:
for key in r.scan_iter(match="session:*", count=100):
print(key)
Not handling type mismatches. Every Redis key has exactly one type, and calling a command against the wrong type raises an exception in Python just as it returns an error at redis-cli:
r.set("user:1001:name", "Ada Lovelace")
r.lpush("user:1001:name", "oops") # raises redis.exceptions.ResponseError
Output:
redis.exceptions.ResponseError: WRONGTYPE Operation against a key holding the wrong kind of value
Catch redis.exceptions.ResponseError (or the more general redis.exceptions.RedisError) around calls where the key’s type isn’t guaranteed by your own code.
Best Practices
- Create a single
Redisclient (backed by aConnectionPool) at startup and share it across your whole application instead of opening new connections per call. - Set
decode_responses=Truefor typical text-based applications so you work withstrinstead of jugglingbytes. - Always set
socket_connect_timeoutandsocket_timeoutso a slow or unreachable Redis doesn’t hang your application indefinitely. - Catch
redis.exceptions.ConnectionErrorandredis.exceptions.TimeoutErroraround calls on the critical path, and decide on a fallback (cache miss, degraded response) rather than letting the exception propagate to the user. - Read host, port, and password from environment variables or a config system — never hardcode credentials in source code.
- Use
r.scan_iter()instead ofr.keys()for any pattern search outside of one-off debugging. - For high-concurrency async frameworks (e.g. FastAPI, asyncio-based services), use
redis.asyncio.Redisinstead of the synchronous client so calls don’t block the event loop.
Practice Exercises
- Write a Python script that connects to a local Redis instance and increments a key called
visits:homeevery time the script runs, printing the current count after incrementing. (Hint: user.incr, which creates the key at 1 if it doesn’t exist.) - Modify that script to build its client from a
ConnectionPoolwithsocket_connect_timeout=2, and wrap the connection attempt in atry/except redis.exceptions.ConnectionErrorblock that prints a friendly message if Redis is unreachable. - Write a script that sets a key
session:demowith a 30-second TTL usingr.set(..., ex=30), then immediately reads the remaining time withr.ttl("session:demo")and prints it. Expected end state: the printed TTL should be a positive integer no greater than 30.
Summary
redis-pytranslates Python method calls into the exact same RESP commands you’d type atredis-cli, so command atomicity and semantics (TTL behavior, type errors, single-threaded execution) are unchanged.- Create one client or
ConnectionPoolat startup and reuse it — don’t open a new connection per request. - Use
decode_responses=Trueif you wantstrback instead ofbytes. - Always set connection/socket timeouts and catch
redis.exceptions.ConnectionErrorso a Redis outage degrades gracefully instead of crashing your app. - Prefer
r.scan_iter()overr.keys(), and catchWRONGTYPE/ResponseErroraround any key whose type isn’t guaranteed.
