Detect memory-leak patterns in Python's functools.lru_cache usage.
Project description
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.
Project details
Release history Release notifications | RSS feed
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 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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
85d0c51824deb591937045592d35b89cffbae8fdb56c9961c236f2ff186736dd
|
|
| MD5 |
e0ddd0bac247cd12878f0f0f03752a20
|
|
| BLAKE2b-256 |
a93229edec939572e0e35be54ad4ab1915911971626065f4070d27f0c529bfbd
|
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
12ed7dce2e2af97a61ddd8e6beb1ee0443cb83330bfbae4be86489a8b4feced9
|
|
| MD5 |
ae756e96ae80ccbc0772c7a2c8c38a30
|
|
| BLAKE2b-256 |
4905fb7addf8c66798d8d0754302625b52871e90764cae47d7e805e4cb14119e
|