Write Redis Lua scripts as real Python functions, not as strings.
Compiled at import, checked by mypy, sent with EVALSHA. Sync and async redis-py.
Documentation · Quickstart · API reference · Changelog
from redis_lua_py import Key, redis, script
@script
def rate_limit(key: Key, limit: int, ttl: int) -> int:
current = redis.incr(key)
if current == 1:
redis.expire(key, ttl)
if current > limit:
return -1
return limit - current
The body is never executed by Python. It is read as source when the module is
imported, compiled to Lua, and sent to Redis with EVALSHA. Your editor
highlights it, your linter sees it, and mypy checks the signature — none of
which is true of a string.
Define scripts at module level, where they compile once at import. A script
defined inside a function recompiles on every call, and one defined through
exec has no source to read and is refused.
from redis import Redis
client = Redis()
remaining = rate_limit(client, key="user:42", limit=10, ttl=60)
Importing the client as from redis import Redis leaves the name redis free
for the script namespace, so the two never collide.
Install
uv add redis-lua-py
Python 3.11+, and redis-py 5.0+ as the only dependency.
What it compiles to
Nothing is hidden. Every script exposes the Lua it produced:
>>> print(rate_limit.lua)
-- rate_limit
-- Generated by redis-lua-py from src/limits.py:6. Do not edit.
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local ttl = tonumber(ARGV[2])
local current = redis.call('INCR', key)
if current == 1 then
redis.call('EXPIRE', key, ttl)
end
if current > limit then
return -1
end
return limit - current
Read it in review, paste it into redis-cli, check it into a golden test. The
point of this library is to generate Lua you would have been willing to write.
The header is part of the body, and the body is what EVALSHA hashes, so the
path in it is relative to your project root rather than absolute — the same
script has the same SHA on a laptop, in CI and in a container, and the server's
script cache is cold once per script rather than once per environment.
What else it does
- Keys and arguments —
a parameter annotated
KeybecomesKEYS, which is what Redis Cluster routes on; anintorfloatis wrapped intonumberfor you. - Command names are checked
at compile time against Redis' own command table, so
redis.expires(...)is refused where you can see it rather than raised inside a script whose whole purpose was to be atomic. - Constants are folded —
a module-level
int,float,str,bytesorboolis read once, at import, and written into the script as a literal. - Binary values survive —
nothing here decodes, and
bytesis a passthrough in both directions. - The caller's side is typed —
a script is a
CompiledScript[R], and an async client gives youAwaitable[R]. - Sync and async from
the same script object, and
bindwhen passing the client every time gets repetitive. - The gaps between Lua and Python
are closed or refused — truthiness, 1-based indexing,
falseversusnil, block scope, and the nil that truncates a returned table. - Anything outside the supported subset raises at import, with a caret under the line at fault.
Full documentation: ignacemaes.com/redis-lua-py.
Testing your scripts
fakeredis embeds a real Lua interpreter, so your script executes for real against an in-process server:
import fakeredis
def test_rate_limit_refuses_past_the_limit():
client = fakeredis.FakeRedis()
assert rate_limit(client, key="u:42", limit=2, ttl=60) == 1
assert rate_limit(client, key="u:42", limit=2, ttl=60) == 0
assert rate_limit(client, key="u:42", limit=2, ttl=60) == -1
Install it with uv add --dev "fakeredis[lua]"; the lua extra is what brings
the interpreter. .lua is the whole script, so a golden snapshot is a string
comparison — see
Testing your scripts.
Development
uv sync
uv run pytest
uv run ruff check
uv run mypy
Tests run against fakeredis, which
executes real Lua, so uv run pytest needs no server. Set REDIS_URL to also
run them against a live Redis:
REDIS_URL=redis://localhost:6379/0 uv run pytest
src/redis_lua_py/_commands.py is generated from the Redis source. Refresh it
when a Redis release adds commands:
uv run python scripts/generate_commands.py 8.10.1
The docs site is built with Zensical; uv run zensical serve previews it with live reload.
Pull requests are squash-merged and their titles must follow Conventional Commits: the title becomes the changelog entry and decides the version bump. See CONTRIBUTING.md.
License
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file redis_lua_py-0.2.1.tar.gz.
File metadata
- Download URL: redis_lua_py-0.2.1.tar.gz
- Upload date:
- Size: 191.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1f64e7941b404057faa769915af931a7991e9b64dbd28736418119a3bbe556fa
|
|
| MD5 |
668aa15545fb3e043a3ff2a751ff0c76
|
|
| BLAKE2b-256 |
4f12f46d4807250f4c49251861deaf3765df9ce143ccfae6b51c4d4db81667ad
|
Provenance
The following attestation bundles were made for redis_lua_py-0.2.1.tar.gz:
Publisher:
release.yml on IgnaceMaes/redis-lua-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
redis_lua_py-0.2.1.tar.gz -
Subject digest:
1f64e7941b404057faa769915af931a7991e9b64dbd28736418119a3bbe556fa - Sigstore transparency entry: 2808907164
- Sigstore integration time:
-
Permalink:
IgnaceMaes/redis-lua-py@7a20075b25d14e23090e500d37c7aa833d30f11a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/IgnaceMaes
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7a20075b25d14e23090e500d37c7aa833d30f11a -
Trigger Event:
push
-
Statement type:
File details
Details for the file redis_lua_py-0.2.1-py3-none-any.whl.
File metadata
- Download URL: redis_lua_py-0.2.1-py3-none-any.whl
- Upload date:
- Size: 31.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
59177d73056217c91d2e01dd52400aa3a7abde2579772539b13ebb0e5d100cb9
|
|
| MD5 |
47b5bf46e20deade8d6f5f90f032e942
|
|
| BLAKE2b-256 |
17b92078e63219dddece3cfc38e6a04a63f0d4d04e96202c44d667f26e4f44a1
|
Provenance
The following attestation bundles were made for redis_lua_py-0.2.1-py3-none-any.whl:
Publisher:
release.yml on IgnaceMaes/redis-lua-py
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
redis_lua_py-0.2.1-py3-none-any.whl -
Subject digest:
59177d73056217c91d2e01dd52400aa3a7abde2579772539b13ebb0e5d100cb9 - Sigstore transparency entry: 2808907188
- Sigstore integration time:
-
Permalink:
IgnaceMaes/redis-lua-py@7a20075b25d14e23090e500d37c7aa833d30f11a -
Branch / Tag:
refs/heads/main - Owner: https://github.com/IgnaceMaes
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@7a20075b25d14e23090e500d37c7aa833d30f11a -
Trigger Event:
push
-
Statement type: