mrkeyoor.com_
Thu 06 Aug 15:42 UTC
PyPIUtilsupdated 06 Aug 2026

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.

Verdict

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.

API stability5/5The public surface is deliberately the redis-py surface, so it changes only when redis-py does; the 2.x line has been additive for years and new releases add commands rather than move existing ones.
Docs5/5The site lists supported and unsupported commands per family with links to the Redis reference, and carries copy-paste recipes for pytest, Django cache, django-redis, django-rq, FastAPI dependency overrides, and the TCP server mode.
Maintenance4/5Pushed within days of this review with 2.37.0 released in July 2026 and only 4 open issues, but it is effectively one maintainer funded through GitHub sponsors, and it has to chase every new Redis and Valkey command.
Ecosystem4/5Works with redis-py and valkey-py, sync and async, covers RedisJSON, RedisBloom, RedisTimeSeries, and Dragonfly-specific commands behind extras, and slots into Django, FastAPI, and RQ through documented hooks.

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
Skip it if

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 True

This 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]) == 5

Scripting 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

PackageRegistryPick it when
testcontainersPyPIYou want the real Redis image in CI and can afford Docker plus a few seconds of container startup per session.
redislitePyPIYou want an actual embedded Redis binary rather than a reimplementation, and can live with the platforms it builds on.
pytest-redisPyPIYou are on pytest and want fixtures that start and clean up a real redis-server process installed on the machine.