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 the EX option 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-py parses that reply into the Python value True, 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 Redis client (backed by a ConnectionPool) at startup and share it across your whole application instead of opening new connections per call.
  • Set decode_responses=True for typical text-based applications so you work with str instead of juggling bytes.
  • Always set socket_connect_timeout and socket_timeout so a slow or unreachable Redis doesn’t hang your application indefinitely.
  • Catch redis.exceptions.ConnectionError and redis.exceptions.TimeoutError around 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 of r.keys() for any pattern search outside of one-off debugging.
  • For high-concurrency async frameworks (e.g. FastAPI, asyncio-based services), use redis.asyncio.Redis instead 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:home every time the script runs, printing the current count after incrementing. (Hint: use r.incr, which creates the key at 1 if it doesn’t exist.)
  • Modify that script to build its client from a ConnectionPool with socket_connect_timeout=2, and wrap the connection attempt in a try/except redis.exceptions.ConnectionError block that prints a friendly message if Redis is unreachable.
  • Write a script that sets a key session:demo with a 30-second TTL using r.set(..., ex=30), then immediately reads the remaining time with r.ttl("session:demo") and prints it. Expected end state: the printed TTL should be a positive integer no greater than 30.

Summary

  • redis-py translates Python method calls into the exact same RESP commands you’d type at redis-cli, so command atomicity and semantics (TTL behavior, type errors, single-threaded execution) are unchanged.
  • Create one client or ConnectionPool at startup and reuse it — don’t open a new connection per request.
  • Use decode_responses=True if you want str back instead of bytes.
  • Always set connection/socket timeouts and catch redis.exceptions.ConnectionError so a Redis outage degrades gracefully instead of crashing your app.
  • Prefer r.scan_iter() over r.keys(), and catch WRONGTYPE/ResponseError around any key whose type isn’t guaranteed.