Skip to main content

redis-lua-py: Redis Lua scripts as real Python functions.

PyPI Python CI license

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, and coredis.

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.10+, and no dependencies: bring your own client, redis-py 4.2+ or coredis.

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 Key becomes KEYS, which is what Redis Cluster routes on; an int or float is wrapped in tonumber for 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, bytes or bool is read once, at import, and written into the script as a literal.
  • Binary values survive — nothing here decodes, and bytes is a passthrough in both directions.
  • The caller's side is typed — a script is a CompiledScript[R], and an async client gives you Awaitable[R].
  • Sync and async from the same script object, with redis-py or coredis, and bind when passing the client every time gets repetitive.
  • Build-time generation for libraries — python -m redis_lua_py generate writes your scripts to a module of typed functions that needs only the standard library, called just like the @script, so your users never depend on this package. --check keeps it current in CI.
  • Redis Functions — the same Python compiles into a function library, loaded with FUNCTION LOAD on first use and called with FCALL, and scripts take Redis 7 flags such as no-writes.
  • The gaps between Lua and Python are closed or refused — truthiness, 1-based indexing, false versus nil, 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

Release files for redis-lua-py 0.11.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for redis-lua-py 0.11.0
File Size Uploaded
redis_lua_py-0.11.0.tar.gz 315.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for redis-lua-py 0.11.0
File Interpreter ABI Platform
redis_lua_py-0.11.0-py3-none-any.whl Python 3 none any Details

Total release size: 423.6 kB

Release files / redis_lua_py-0.11.0.tar.gz

Download URL redis_lua_py-0.11.0.tar.gz
Size 315.6 kB
Tags Source
SHA-256 checksum
How to use checksums
8648db0231b997ac01fea38543da317afc43856b562dc121b371936fd7e9d9f8
BLAKE2b-256 checksum
How to use checksums
5f1479566ad1ef6faed4761b4fd2c2fc76de5e5975559184f37672d8940717c4
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release files / redis_lua_py-0.11.0-py3-none-any.whl

Download URL redis_lua_py-0.11.0-py3-none-any.whl
Size 108.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
2794b9ae6f86369a0b6580f1960cc165cd14e60bf34f91e40b474db45da82d78
BLAKE2b-256 checksum
How to use checksums
a8a6745a4f56586946f8dff8b96fab3f98484f2157d7af33624b0eb3a81d68e9
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.

Transparency log

Release history Release notifications | RSS feed

0.12.0

2 release files

This release

0.11.0 This release

2 release files

0.10.0

2 release files

0.9.0

2 release files

0.8.0

2 release files

0.7.0

2 release files

0.6.0

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page