lrucheck
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:
@lru_cacheon a method. The cache holds theselfargument. Your instance never gets garbage-collected. The longer the program runs, the more memory it uses.@lru_cache(maxsize=None). The cache has no size limit. It grows forever as new arguments come in. (@cachedoes the same thing but is fine when you want it on purpose, for example on a method of anenum.Enumsubclass.)
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: LRU001to skip a single line--selectand--ignoreto 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:
rufffor linting and formattingcodespellfor spellingtyfor 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.
Metadata
Release files for lrucheck 0.2.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| lrucheck-0.2.0.tar.gz | 8.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| lrucheck-0.2.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 16.9 kB
Release files / lrucheck-0.2.0.tar.gz
| Download URL | lrucheck-0.2.0.tar.gz |
|---|---|
| Size | 8.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
85d0c51824deb591937045592d35b89cffbae8fdb56c9961c236f2ff186736dd
|
|
BLAKE2b-256 checksum How to use checksums |
a93229edec939572e0e35be54ad4ab1915911971626065f4070d27f0c529bfbd
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|
Release files / lrucheck-0.2.0-py3-none-any.whl
| Download URL | lrucheck-0.2.0-py3-none-any.whl |
|---|---|
| Size | 8.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
12ed7dce2e2af97a61ddd8e6beb1ee0443cb83330bfbae4be86489a8b4feced9
|
|
BLAKE2b-256 checksum How to use checksums |
4905fb7addf8c66798d8d0754302625b52871e90764cae47d7e805e4cb14119e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is 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}
|