Skip to main content

Detect memory-leak patterns in Python's functools.lru_cache usage.

Project description

lrucheck logo

lrucheck

PyPI Python versions Downloads License: MIT Ruff pre-commit

A small static checker that finds memory leaks from functools.lru_cache and functools.cache in Python code.

Why

Python's lru_cache is easy to use, but it has two common traps:

  1. @lru_cache on a method. The cache holds the self argument. Your instance never gets garbage-collected. The longer the program runs, the more memory it uses.
  2. @lru_cache(maxsize=None). The cache has no size limit. It grows forever as new arguments come in. (@cache does the same thing but is fine when you want it on purpose, for example on a method of an enum.Enum subclass.)

lrucheck reads your code (without running it) and prints a warning when it finds these patterns.

Install

uv add lrucheck

Use

Scan a file:

lrucheck path/to/file.py

Scan a folder (recursive):

lrucheck src/

Example

Given this file service.py:

from functools import lru_cache


class UserService:
    @lru_cache(maxsize=None)
    def find_user(self, user_id):
        return load_user(user_id)

    @lru_cache(maxsize=128)
    def get_settings(self, user_id):
        return load_settings(user_id)

Run lrucheck:

$ lrucheck service.py
service.py:5:6: LRU001 `@lru_cache` on a method keeps `self` in the cache and leaks the instance
service.py:5:6: LRU002 `@lru_cache(maxsize=None)` has no size limit and can grow forever. Use `@cache` directly if this is on purpose.
service.py:9:6: LRU001 `@lru_cache` on a method keeps `self` in the cache and leaks the instance
$ echo $?
1

The output format is the same as flake8 and ruff, so editors and CI tools can read it.

Rules

Code What it finds
LRU001 @lru_cache or @cache on a method. The cache keeps self, so the instance is never freed.
LRU002 @lru_cache(maxsize=None). The cache has no size limit and can grow forever. @cache is not flagged because it is sometimes the right choice.
LRU003 @lru_cache inside a function or closure. A new cache is made on every outer call, so cache hits are rare. (warning)
LRU004 @lru_cache placed above @staticmethod. Wrong decorator order. The reverse breaks on Python 3.9 and is non canonical on later versions. (warning)

Bad

from functools import lru_cache

class Service:
    @lru_cache(maxsize=None)   # LRU001 + LRU002
    def fetch(self, key):
        return load(key)

Good

from functools import lru_cache

@lru_cache(maxsize=128)
def fetch(key):
    return load(key)

If you must keep the cache near a class, use a @staticmethod or a top-level function:

class Service:
    @staticmethod
    @lru_cache(maxsize=128)
    def fetch(key):
        return load(key)

Severity

Each rule has a level. Errors (LRU001, LRU002) point to real memory leaks and fail the build. Warnings (LRU003, LRU004) point to less serious problems and do not change the exit code on their own. Warnings still print to the output and start with the warning: prefix.

service.py:5:6: LRU001 ...                  (error)
service.py:5:6: warning: LRU003 ...         (warning)

Exit codes

Code Meaning
0 No errors found. Warnings may still be printed.
1 One or more rule errors found.
2 A path was missing, a file could not be read, or a file had a syntax error.

This makes lrucheck easy to use in CI: a non-zero exit fails the build.

Pre-commit (planned)

A pre-commit hook is on the roadmap (see TODO.md). For now, you can run lrucheck from a script or a Makefile.

Roadmap

  • Read config from pyproject.toml [tool.lrucheck]
  • # noqa: LRU001 to skip a single line
  • --select and --ignore to turn rules on or off
  • JSON output for editors

See TODO.md for the full list.

Development

This project uses uv.

uv sync
uv run pytest
uv run lrucheck tests/examples/

Pre-commit

The project uses pre-commit to run checks before each commit:

  • ruff for linting and formatting
  • codespell for spelling
  • ty for type checking
  • standard hooks for trailing whitespace, end of file, large files

To set it up once:

uv run pre-commit install

After this, the hooks run on every git commit. You can also run them on all files at any time:

uv run pre-commit run --all-files

License

MIT.

Project details


Download files

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

Source Distribution

lrucheck-0.2.0.tar.gz (8.8 kB view details)

Uploaded Source

Built Distribution

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

lrucheck-0.2.0-py3-none-any.whl (8.1 kB view details)

Uploaded Python 3

File details

Details for the file lrucheck-0.2.0.tar.gz.

File metadata

  • Download URL: lrucheck-0.2.0.tar.gz
  • Upload date:
  • Size: 8.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for lrucheck-0.2.0.tar.gz
Algorithm Hash digest
SHA256 85d0c51824deb591937045592d35b89cffbae8fdb56c9961c236f2ff186736dd
MD5 e0ddd0bac247cd12878f0f0f03752a20
BLAKE2b-256 a93229edec939572e0e35be54ad4ab1915911971626065f4070d27f0c529bfbd

See more details on using hashes here.

File details

Details for the file lrucheck-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: lrucheck-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 8.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.11.8 {"installer":{"name":"uv","version":"0.11.8","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"macOS","version":null,"id":null,"libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":null}

File hashes

Hashes for lrucheck-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 12ed7dce2e2af97a61ddd8e6beb1ee0443cb83330bfbae4be86489a8b4feced9
MD5 ae756e96ae80ccbc0772c7a2c8c38a30
BLAKE2b-256 4905fb7addf8c66798d8d0754302625b52871e90764cae47d7e805e4cb14119e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page