apimirage
LLMs use deprecated APIs up to 38% of the time — and ruff can't catch it.
apimirage is a tiny static checker that finds hallucinated and
deprecated API usage in Python code. It parses your file with ast,
collects every call made through an imported name, then verifies each one
for real with importlib + inspect.signature. Nothing is ever executed —
imports are only used for existence checks.
$ apimirage app.py
app.py:12: [ghost-api] requests.post_json -- attribute does not exist on the real module -- possibly hallucinated
app.py:27: [ghost-param] json.dumps(pretty=...) -- keyword argument 'pretty' is not accepted by the real signature
app.py:41: [deprecated] asyncio.coroutine -- API is deprecated: marked with __deprecated__ (PEP 702)
apimirage: 3 findings in 1 file
Zero dependencies. Python standard library only (ast + importlib + inspect).
Everything stays local.
Install
pip install apimirage
Or from source:
git clone https://github.com/hahahahahahahahah6/apimirage
cd apimirage
pip install .
Usage
apimirage file.py [more_files.py ...]
apimirage -f json src/ --quiet
apimirage --version
Exit codes: 0 = clean, 1 = findings, 2 = usage error (missing file,
syntax error).
Finding kinds:
| kind | meaning |
|---|---|
ghost-module |
the imported module cannot be imported at all — likely hallucinated |
ghost-api |
the module is real, but the attribute doesn't exist on it — likely hallucinated |
ghost-param |
the function is real, but the keyword argument isn't in its real signature |
deprecated |
the API advertises deprecation (__deprecated__ per PEP 702, or a "deprecated" docstring) |
unverifiable |
the import itself raised an unexpected error, so existence couldn't be confirmed |
It also works as a library:
from apimirage import check_file
result = check_file("app.py")
for finding in result.findings:
print(finding.one_line())
Why not ruff / pylint?
Ruff and pylint are static rule engines: they match code against a fixed list of known-bad patterns. That works for style and for APIs they already know about, but it fundamentally can't answer the two questions that matter for AI-generated code:
- "Does this API actually exist?" — An LLM can invent a perfectly
plausible name (
requests.post_json,pd.read_excel_sheets) that no rule list contains. Ruff sees a valid-looking call and stays silent.apimiragereally imports the module and checks the attribute, so a hallucination is caught no matter how convincing it looks. - "Is this real API already deprecated?" — Deprecation is a property of the
installed library version, not of your source text. A rule engine would need
a constantly updated database of every library's deprecations.
apimiragereads the deprecation signals off the live objects themselves (__deprecated__, deprecation docstrings), so it tracks whatever you have installed.
The trade-off is deliberate: apimirage only verifies names that are statically
resolvable through imports, and importing a module does run its top-level code
(it never calls anything). Dynamic getattr, monkeypatching, and
version-specific APIs can produce false negatives or positives — see
Limitations below.
Limitations
- Only calls through imported names are checked; dynamic access
(
getattr(mod, name)) and monkeypatched attributes are invisible to it. - An API that exists in your installed version but not in your deployment target (or vice versa) will be judged against what's installed here.
- Importing a module executes its top-level code.
apimiragenever calls your functions, but a module with heavy import-time side effects will run them. from x import *and unresolvable local names are skipped, not flagged.
Development
python3 -m unittest discover -s tests -v # 15 tests, stdlib only
python3 -m build # build sdist + wheel
License
MIT — see LICENSE.
Metadata
Release files for apimirage 0.1.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 | Provenance | |
|---|---|---|---|---|
| apimirage-0.1.0.tar.gz | 10.8 kB | Without provenance | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| apimirage-0.1.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 19.6 kB
Release files / apimirage-0.1.0.tar.gz
| Download URL | apimirage-0.1.0.tar.gz |
|---|---|
| Size | 10.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
b51c15a7b6219407e5d011e7ef6e97d40d4d8f654e498c212a8e8e82f9abd824
|
|
BLAKE2b-256 checksum How to use checksums |
62061191ef61d7ef330ad45a11668188c35a4ae1e3503f689a2e7e0285d02ae1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Release files / apimirage-0.1.0-py3-none-any.whl
| Download URL | apimirage-0.1.0-py3-none-any.whl |
|---|---|
| Size | 8.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
8c7d375076a9fa11fc3652e9fe47f6d3175585aba94a1952c92aed076ee6d7cc
|
|
BLAKE2b-256 checksum How to use checksums |
1eae607bdd78eaec98a6ddaf61d10cd2297e7363986e6879ccf4b05b32372def
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
twine/7.0.0 CPython/3.12.3
|
Security for apimirage 0.1.0
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.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.