fakeredis
fakeredis reimplements the Redis command set in pure Python and exposes it behind the same classes redis-py gives you, so fakeredis.FakeStrictRedis() behaves like redis.Redis() with the data held in a process-local dictionary instead of on a server. Each client gets its own FakeServer object unless you hand it one to share. It covers the core data types plus Redis Stack families such as JSON, Bloom and Cuckoo filters, TimeSeries, and Geo, can pretend to be Redis 6, 7, or 8 as well as Valkey, DragonflyDB, or KeyDB, and can even listen on a real TCP socket when your code insists on dialing a host and port.
The fastest way to get Redis-dependent tests running with no server, and the maintainer keeps command coverage close to current Redis. Pair it with at least one job that runs against real Redis, because it is an emulation and the gaps show up in production rather than in your suite.
Use it if
- Your test suite touches Redis and you do not want a server or a Docker container in CI just to run unit tests
- You want each test to start from an empty, isolated data store without writing flushdb teardown code
- You need to exercise error handling: flipping server.connected to False makes every command raise ConnectionError on demand
- You use Redis Stack commands such as JSON.SET or BF.ADD and do not want to pull a redis-stack image into the test environment
- You are on Django, FastAPI, or RQ, where the documented pattern is swapping a connection class or dependency rather than changing your application code
- You use Redis Cluster: the cluster command family is documented as not implemented, so RedisCluster clients and MOVED or ASK redirect handling cannot be tested against it
- You treat a green suite as proof the code works: this is an independent reimplementation, so unimplemented commands, error strings, and edge case semantics can differ from the server you deploy against, and you still want a smoke run against real Redis
- You are measuring performance, memory, or eviction: it is single-process pure Python with no maxmemory policy behavior worth trusting
- You need the extras but did not install them: EVAL and EVALSHA require the lua extra, JSON commands require json, and the probabilistic filters require bf, and without them the command fails rather than degrading
- Your tests assert on expiry timing: keys expire against the real wall clock, so anything time sensitive needs a clock-freezing library on top
Setup reality
pip install fakeredis brings in redis-py and sortedcontainers and little else, and the first FakeStrictRedis() works immediately. Two things bite afterwards. First, state is per FakeServer, and every client builds its own unless you pass server=; two components that each construct their own client will silently see different data, which looks like a caching bug rather than a test setup bug. Second, code that creates its own connection deep inside a framework cannot be swapped by assigning a variable, so you either inject FakeConnection as the connection class (Django and django-redis both document the option), monkeypatch the factory (the django-rq recipe in the docs), or run TcpFakeServer on a background thread and point a real redis client at 127.0.0.1. Extras need quoting on zsh, as in pip install "fakeredis[json]". The default emulation target is Redis 7, so pass version or server_type if you deploy something else.
Patterns
Swap in a fake clientquick-start
import fakeredis
r = fakeredis.FakeStrictRedis()
r.set("foo", "bar")
assert r.get("foo") == b"bar"
r.lpush("queue", "a", "b", "c")
assert r.lrange("queue", 0, -1) == [b"c", b"b", b"a"]Values come back as bytes exactly like redis-py. Pass decode_responses=True if your production client uses it, or your assertions will pass locally and fail against the real thing.
Share state between two clientsshared-server
server = fakeredis.FakeServer()
writer = fakeredis.FakeStrictRedis(server=server)
reader = fakeredis.FakeStrictRedis(server=server)
writer.set("greeting", "hello")
assert reader.get("greeting") == b"hello"This is the single most common surprise: without server=, each client gets a private FakeServer, so a producer and a consumer built separately never see each other data.
Use the async clientasync-client
from fakeredis import FakeAsyncRedis
async def test_cache():
r = FakeAsyncRedis()
await r.set("foo", "bar")
assert await r.get("foo") == b"bar"
await r.aclose()FakeAsyncRedis mirrors redis.asyncio.Redis. A shared FakeServer can be passed to both sync and async clients if part of your code is still blocking.
One clean store per testpytest-fixture
import pytest
import fakeredis
@pytest.fixture
def redis_client():
return fakeredis.FakeStrictRedis()
def test_cache_set(redis_client):
redis_client.set("user:1", "alice")
assert redis_client.get("user:1") == b"alice"A function-scoped fixture gives every test an empty store, so no flushdb teardown is needed. Move it to session scope only if you deliberately want state to carry over.
Test your error handling deterministicallysimulate-connection-error
server = fakeredis.FakeServer()
r = fakeredis.FakeStrictRedis(server=server)
server.connected = False
with pytest.raises(redis.exceptions.ConnectionError):
r.set("foo", "bar")
server.connected = True
assert r.set("foo", "bar") is TrueThis is the reason to keep a reference to the FakeServer. Reproducing a Redis outage against a real server in CI means stopping a container mid-test.
Emulate a specific server and versionpin-server-version
r6 = fakeredis.FakeStrictRedis(version=6)
valkey = fakeredis.FakeStrictRedis(server_type="valkey")
dragonfly = fakeredis.FakeStrictRedis(server_type="dragonfly")Redis 7 is the default. Pin to what you actually deploy, otherwise a command that only exists in a newer version passes in tests and fails in production.
Build the client the same way production doesfrom-url
r = fakeredis.FakeStrictRedis.from_url(
"redis://localhost:6379/0",
decode_responses=True,
)
r.set("foo", "bar")
assert r.get("foo") == "bar"from_url ignores the host and port and never opens a socket, which lets you keep one factory function and only change the class in tests.
Point the Django cache at the fakedjango-cache
from fakeredis import FakeConnection
CACHES = {
"default": {
"BACKEND": "django.core.cache.backends.redis.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
"OPTIONS": {"connection_class": FakeConnection},
}
}
# django-redis instead wants:
# "OPTIONS": {"CONNECTION_POOL_KWARGS": {"connection_class": FakeConnection}}Wrap this in @override_settings for a single test module rather than putting it in your base settings, or you will ship a build where the cache silently does nothing.
Serve the fake over a real sockettcp-server
from threading import Thread
from fakeredis import TcpFakeServer
address = ("127.0.0.1", 6390)
server = TcpFakeServer(address, server_type="redis")
thread = Thread(target=server.serve_forever, daemon=True)
thread.start()
import redis
r = redis.Redis(host=address[0], port=address[1])
r.set("foo", "bar")
server.shutdown()
server.server_close()
thread.join()The escape hatch for code that builds its own connection and cannot be injected. It is slower than direct mode because everything goes through a real socket, so keep it for the few tests that need it.
Run EVAL scriptslua-scripting
# pip install "fakeredis[lua]"
r = fakeredis.FakeStrictRedis()
incr_by = r.register_script(
"return redis.call('INCRBY', KEYS[1], ARGV[1])"
)
assert incr_by(keys=["counter"], args=[5]) == 5Scripting is only available with the lua extra, which pulls in lupa. Without it the EVAL family raises rather than falling back to anything.
Use Redis Stack command familiesredis-stack-json
# pip install "fakeredis[json,bf]"
r = fakeredis.FakeStrictRedis()
r.json().set("user:1", "$", {"name": "ada", "tags": ["admin"]})
assert r.json().get("user:1", "$.name") == ["ada"]
r.bf().create("seen", 0.01, 1000)
r.bf().add("seen", "user:1")Each family is a separate extra: json pulls jsonpath-ng, bf pulls pyprobables. Check the supported-commands pages before assuming a specific JSON or search command is covered.
Pipelines and WATCH-based transactionspipelines-and-transactions
r = fakeredis.FakeStrictRedis()
r.set("balance", 100)
with r.pipeline() as pipe:
pipe.watch("balance")
current = int(pipe.get("balance"))
pipe.multi()
pipe.set("balance", current - 10)
pipe.execute()
assert r.get("balance") == b"90"The pipeline and transaction API matches redis-py, but there is no second server process racing you, so a WatchError path that fires in production may never trigger here.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| testcontainers | PyPI | You want the real Redis image in CI and can afford Docker plus a few seconds of container startup per session. |
| redislite | PyPI | You want an actual embedded Redis binary rather than a reimplementation, and can live with the platforms it builds on. |
| pytest-redis | PyPI | You are on pytest and want fixtures that start and clean up a real redis-server process installed on the machine. |