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.

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 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, and bind when passing the client every time gets repetitive.
  • 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

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

redis_lua_py-0.2.1.tar.gz (191.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

redis_lua_py-0.2.1-py3-none-any.whl (31.0 kB view details)

Uploaded Python 3

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

Hashes for redis_lua_py-0.2.1.tar.gz
Algorithm Hash digest
SHA256 1f64e7941b404057faa769915af931a7991e9b64dbd28736418119a3bbe556fa
MD5 668aa15545fb3e043a3ff2a751ff0c76
BLAKE2b-256 4f12f46d4807250f4c49251861deaf3765df9ce143ccfae6b51c4d4db81667ad

See more details on using hashes here.

Provenance

The following attestation bundles were made for redis_lua_py-0.2.1.tar.gz:

Publisher: release.yml on IgnaceMaes/redis-lua-py

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for redis_lua_py-0.2.1-py3-none-any.whl
Algorithm Hash digest
SHA256 59177d73056217c91d2e01dd52400aa3a7abde2579772539b13ebb0e5d100cb9
MD5 47b5bf46e20deade8d6f5f90f032e942
BLAKE2b-256 17b92078e63219dddece3cfc38e6a04a63f0d4d04e96202c44d667f26e4f44a1

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 files

0.2.0

2 files

0.1.0

2 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