62.5 redis-py: Strings, Hashes, Lists, Sets, and Sorted Sets
Alright, let’s get our hands dirty with redis-py, the Python client for Redis. Forget the dry, academic approach. We’re going to talk about this like two engineers at a whiteboard, one of whom has been burned a few times and is trying to save the other from the same fate.
First, the golden rule: Redis is a data structures server. It’s not just a dumb key-value store where you chuck strings. You use it wrong, and you’re leaving 90% of its power on the table. The redis-py library maps these powerful data structures directly to intuitive Python types. Your job is to pick the right structure for the task, or you’ll end up with a convoluted, slow mess that’s a nightmare to maintain.
The Humble String: More Than Meets the Eye
Yes, the simplest structure is the string. You set and get. It seems trivial, and for simple cache keys, it is. But the real power is in the commands you’re probably not using.
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
# The basics. You know these.
r.set('user:1000:name', 'Alice')
print(r.get('user:1000:name')) # Output: b'Alice' (remember, it returns bytes!)
# But wait, there's more! Need a cache that expires? Don't muck about with background tasks.
# Set with a 300-second (5-minute) timeout. It just vanishes. Poof.
r.setex('api:response:latest', 300, '{"data": "some_json_payload"}')
# Atomic increment? This is where Redis shines for counters.
r.set('page:views:home', 0)
r.incr('page:views:home') # -> 1
r.incrby('page:views:home', 5) # -> 6
The INCR command is atomic. If you try to build a counter in your SQL database with UPDATE counters SET value = value + 1 WHERE name = 'home', you’re in for a world of pain under concurrent load. Redis handles this perfectly because it’s single-threaded. It’s one of its greatest strengths, not a weakness, for this use case.
Hashes: For When You Need a Dictionary
If you find yourself storing serialized JSON objects under a single key and then constantly fetching and updating the entire blob just to change one field, stop. You’re doing it wrong. You’re creating needless network traffic and race conditions. This is what Hashes are for.
# TERRIBLE: Storing as a JSON string
import json
user_data = {'name': 'Alice', 'email': 'alice@example.com', 'visits': 10}
r.set('user:1000', json.dumps(user_data))
# To update visits, you have to GET, parse, update, re-serialize, SET. Yuck.
# THE RIGHT WAY: Use a Hash
r.hset('user:1000', mapping={'name': 'Alice', 'email': 'alice@example.com', 'visits': 10})
# Update a single field without touching the others
r.hincrby('user:1000', 'visits', 1) # Atomic increment on the 'visits' field
print(r.hgetall('user:1000'))
# Output: {b'name': b'Alice', b'email': b'alice@example.com', b'visits': b'11'}
Hashes are perfect for representing objects with multiple attributes. Use them. Your future self will thank you. The hset with a mapping is the most Pythonic way to set multiple fields at once.
Lists: Your Simple, Powerful Queue
Need a queue? Before you reach for RabbitMQ or Kafka for every little thing, see if a Redis List will do the job. It’s fantastic for job queues, message passing, or just keeping a fixed-length history of events.
# Push items onto the "end" (right) of the list
r.rpush('queue:tasks', 'task1')
r.rpush('queue:tasks', 'task2', 'task3') # You can push multiple in one go
# A worker pops from the "beginning" (left) of the list.
# This is atomic. If multiple workers are blocked on blpop, only one gets the task.
task = r.blpop('queue:tasks', timeout=10) # Blocks for up to 10 seconds if list is empty
print(task) # Output: (b'queue:tasks', b'task1')
# Want a fixed-length history?
r.lpush('site:recent_visits', 'visitor_id_456')
r.ltrim('site:recent_visits', 0, 49) # Trim the list to only the first 50 items
The BLPOP command is the killer feature here. It’s a blocking pop. Your worker doesn’t need to poll endlessly; it just sits there, asleep, until a task appears. It’s efficient and simple. The LTRIM command is how you cap a list to a specific size, perfect for “last N events” scenarios.
Sets: Unordered, but Uniquely Powerful
Sets are about uniqueness and membership. Did this user like this post? What are all the tags for this article? Who are the friends of this user? If you need to check if something exists in a group, a Set is your answer.
# Track article tags
r.sadd('article:1000:tags', 'tech', 'python', 'databases')
r.sadd('article:1000:tags', 'python') # Adding 'python' again does nothing. Uniqueness!
# Check if an article is tagged with 'python'
print(r.sismember('article:1000:tags', 'python')) # Output: True
# Get all tags for the article (order is arbitrary!)
print(r.smembers('article:1000:tags')) # Output: {b'python', b'databases', b'tech'}
# Find the intersection of tags between two articles? Easy.
r.sadd('article:1001:tags', 'python', 'redis')
common_tags = r.sinter('article:1000:tags', 'article:1001:tags')
print(common_tags) # Output: {b'python'}
Sorted Sets: The Best Thing Since Sliced Bread
This is Redis’s secret weapon. It’s a Set (so all members are unique) but each member has a score, and the set is ordered by that score. This is how you build leaderboards, priority queues, or time-series data where you need to range query.
# A simple leaderboard
r.zadd('game:leaderboard', {'Alice': 100, 'Bob': 200, 'Charlie': 150})
# ZADD is smart now. By default, it treats the mapping as member->score.
# Who's in the top 2?
print(r.zrevrange('game:leaderboard', 0, 1, withscores=True))
# Output: [(b'Bob', 200.0), (b'Charlie', 150.0)]
# What's Charlie's rank? (0-indexed from the top)
print(r.zrevrank('game:leaderboard', 'Charlie')) # Output: 1
# Increment a score atomically
r.zincrby('game:leaderboard', 50, 'Alice') # Alice gets 50 more points
print(r.zscore('game:leaderboard', 'Alice')) # Output: 150.0
The most common pitfall with Sorted Sets is forgetting that order is by score, and if scores are tied, lexicographically. For a time-series, you’d use the timestamp as the score. Want a “most recently viewed” list? Use the epoch timestamp as the score and ZREVRANGE to get the top N. It’s incredibly versatile. The designers nailed this one.