Skip to main content

cuss: code usage statistics

[!WARNING] This is a vibe-coded project. Every line of it was written by an LLM, including this warning. It is linted, typed and tested, none of which is evidence that it is correct. Read the code before trusting a number it gives you — especially before deleting anything on its advice.

Which public symbols of a Python package does the world actually use, and how?

Answers two questions:

  • Can I deprecate X?
  • Which symbols should I improve first?

Install

uvx cuss --help          # run it without installing
uv tool install cuss     # or keep it on PATH

Needs GITHUB_TOKEN, GH_TOKEN, or an authenticated gh, since it searches code.

Usage

cuss scipy.special --since 2y --min-stars 50
scipy.special — 400 files, 289 repos, pushed >= 2y

repos  symbol     files  call  value
   39  softmax       41    72      3
   27  gamma         29   130      3
   26  erf           26    63      1
   20  expit         19    33      2
   19  logsumexp     21    92
   19  binom         19    54

14 of 117 used symbols shown (--top)
557 public symbols unused (--unused)

Rows are ranked by distinct repositories, so that column leads. One column per usage kind follows, and only for the kinds that occur. A package of functions needs two; httpx, whose API is largely classes and exceptions, needs five:

repos  symbol           files  call  subcls  annot  catch  value
   50  AsyncClient         54    66       2     28             5
   20  Response            21                   37             5
   19  Client              19    15       4     28             4
   16  HTTPError           16                    1     26      6

value comes last and is the residue — the symbol named as a value, so passed to a function, aliased, put in a tuple, or used in arithmetic. Names are relative to the scope named in the header.

Importing a name is not using it. A file that imports gamma and never mentions it again contributes nothing, so a symbol nobody does more than import counts as unused.

There is no total column, because it would be the row sum and nothing more.

The target is a module, a symbol, a distribution, a directory, or a file:

cuss scipy.special             # rank one module
cuss scipy.special.gamma       # one symbol
cuss scipy-stubs --unused      # deprecation candidates across the package
cuss ./scipy-stubs/special     # a working tree, scoped to a submodule
cuss ./src/mypkg/_core.py      # whichever file is open

Dotted targets are resolved from sys.path. Paths climb to the top-level package — a parent holding __init__ means there is more package above — so pointing anywhere inside a checkout works, and the position within it becomes the scope.

How it works

Code search finds the corpus. AST analysis measures it.

flowchart TD
    target["cuss scipy.special"]

    target --> index["public API index<br>AST over installed .pyi"]
    target --> search["code search<br>10 req/min · 1000 results per query"]

    search --> hits["hits<br>repo · path · blob sha"]
    hits --> meta["repo metadata<br>GraphQL · ~100 repos per request"]
    meta --> filter{"pushedAt · stars<br>archived · fork"}
    filter -->|drop| discard["discarded<br>never downloaded"]
    filter -->|keep| blobs["file text<br>raw.githubusercontent.com · unmetered"]

    blobs --> usage["AST usage extraction"]
    index --> usage
    usage --> report["ranked report"]
    index --> report

Results are cached: blobs by their immutable blob sha, search pages and repo metadata with a TTL. --refresh ignores the cache.

Why not count search hits?

One query per symbol is the obvious design. It does not work.

Measured Consequence
/search/code allows 10 requests per minute 3136 public symbols in scipy-stubs ≈ 5 hours
No regex, no OR, no parentheses, no path: glob Cannot express "this symbol, qualified"
Only the first 1000 results of a query are reachable Counts are truncated anyway
Date qualifiers are silently ignored Cannot ask for code that is still alive

So search runs a handful of queries to find files, and the AST counts symbols. Budget is spent per page, not per symbol. Age and popularity filtering happens afterwards, over repository metadata, which is where those qualifiers actually exist.

Two queries find the corpus — from numpy import and import numpy, the second covering every alias — and each fills it in proportion to how common its idiom is, 3.3M results against 85k. Rank the two halves of a numpy corpus separately and only 5 of the top 12 symbols are shared, so weighting them equally would describe a population that does not exist. No source falls below one page, though, or the symbols peculiar to a rarer idiom would read as unused.

What gets counted

Every import form is resolved to a qualified name:

Source Resolves to
import scipy.special scipy.special.gamma via the dotted chain
import scipy.special as sp sp.gamma
from scipy import special special.gamma
from scipy.special import gamma as g g
from scipy.special import * bare gamma, against the public API index

Any name the file binds itself — assignment, parameter, except ... as, match capture — shadows the import and is dropped, so a local def gamma is never mistaken for the real one. Resolved names are trimmed to the longest prefix that exists in the API index, so gamma(x).shape counts as scipy.special.gamma and unknown attributes count as nothing.

Each reference is classified by where it appears:

flowchart TD
    node["resolved name"] --> parent{"parent node"}
    parent -->|"Call.func"| call["call<br>+ keyword names"]
    parent -->|"ClassDef.bases"| subclass["subclass"]
    parent -->|"decorator_list"| decorator["decorator"]
    parent -->|"annotation · returns"| annotation["annotation"]
    parent -->|"except clause"| catch["catch"]
    parent -->|"otherwise"| value["value"]

Ranking is by distinct repositories first, then total references — one prolific codebase should not outvote the ecosystem. Keyword names are collected per call site as well, the most direct signal for which overloads and parameters need to be right; they are reported by --json only, being too ragged for a column.

Prior art: python-api-inspect

Quansight-Labs/python-api-inspect asks the same question, down to the wording ("Can certain functions be depreciated?"), and its AST analysis is more thorough than this one. It could not be reused or built upon:

Last commit February 2021
Published to PyPI No
Hosted datasette instance python-api-inspect.aves.io no longer resolves
Shape Nix shell + scripts producing a ~6 GB sqlite database, not a CLI
Corpus Repository lists checked into the repo, pinned to master refs from 2021
Refreshing the corpus Needs a local sqlite copy of the libraries.io Open Data dump

The deeper mismatch is structural. It counts the namespaces it observes and has no index of a package's declared public API, so it cannot report the symbols nobody used — which is the entire deprecation question. And it reads .py and .ipynb source, so a stub-only distribution like scipy-stubs has nowhere to fit.

Limits

  • The 1000-result cap and best-match ordering make the corpus a sample, not a census. Counts rank; they do not total.
  • Best-match ordering is not quality ordering. Without --min-stars, expect hallucinated imports from vibe-coded repositories to show up as real usage.
  • Shadowing is per file, not per scope. A name bound anywhere in a file suppresses that import everywhere in it, so counts err low rather than high.
  • Public GitHub only.

Download files

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

Source Distribution

cuss-0.1.0.tar.gz (18.0 kB view details)

Uploaded Source

Built Distribution

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

cuss-0.1.0-py3-none-any.whl (19.6 kB view details)

Uploaded Python 3

File details

Details for the file cuss-0.1.0.tar.gz.

File metadata

  • Download URL: cuss-0.1.0.tar.gz
  • Upload date:
  • Size: 18.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for cuss-0.1.0.tar.gz
Algorithm Hash digest
SHA256 78c6e40bd94f233f2d4b2701f856b539010d620048aa9365cfd69af3de67f91a
MD5 0154d986dda3f0c3f3c4e11fc15e367d
BLAKE2b-256 35984d520c6070ef33bf36624bc738b5895dde17385174e3e88b6b74a3239b1e

See more details on using hashes here.

Provenance

The following attestation bundles were made for cuss-0.1.0.tar.gz:

Publisher: publish-pypi.yml on jorenham/cuss

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

File details

Details for the file cuss-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: cuss-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 19.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for cuss-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 54a0c7357d147ce6b50351b3784a42e23fda0a508b1b0b1bda4eca76274134ae
MD5 9b0b3f0df6343f4a8d156f8fd846c4c9
BLAKE2b-256 ca993afd2ecba76e06f352d112dadc60c57fc5f5530bd946c11bc132db0fe637

See more details on using hashes here.

Provenance

The following attestation bundles were made for cuss-0.1.0-py3-none-any.whl:

Publisher: publish-pypi.yml on jorenham/cuss

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

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