Skip to main content

cococo

code cognitive complexity — a library and CLI to compute the cognitive complexity of Python functions.

This is a fork of Melevir/cognitive_complexity (MIT) that adds a command-line tool, modern-Python construct support, and Python 3.10+ packaging. Three names differ on purpose: install codecoco, import cognitive_complexity, run cococo (cognitive_complexity and cococo were both already taken on PyPI, so the distribution is published as codecoco).

Installation

pip install codecoco
# or, with uv:
uv pip install codecoco

This installs the cococo command and the importable cognitive_complexity package. To install the unreleased tip from the repository instead:

pip install git+https://github.com/qwhex/cococo

Usage

Command line

cococo src/                  # score worst-first, with refactor suggestions inline
cococo src/ --max 20         # gate: exit non-zero if any function exceeds 20
cococo a.py b.py --min 10    # only list functions scoring >= 10
cococo src/ --suggest-min 10 # only attach suggestions to functions scoring >= 10
cococo src/ --max 20 --no-suggest  # gate only: skip suggestions (faster for CI)
cococo src/ --max 20 --json  # machine-readable report for a pipeline
cococo src/ --fix            # apply safe guard-clause rewrites in place
cococo src/ --nested fold    # pre-2.0.0 scoring: fold nested defs into the parent
cococo src/ --max 20 --baseline .cococo.json  # ratchet: fail only on regressions
cococo . --exclude 'generated/*'   # skip extra paths on top of the defaults

Scanning a directory walks it recursively, but never into hidden directories (.git, .venv, .tox, tool caches) or the usual vendor/build trees (venv, site-packages, node_modules, build, dist, __pycache__, *.egg-info), so cococo . scores your code rather than your dependencies — and --fix can never rewrite an installed package. --exclude PATTERN (repeatable) prunes more; a file named directly on the command line is always scanned, exclusions or not.

cococo scores every function, method, and named nested function as its own unit — a nested def is reported on its own row with a qualified name (outer.<locals>.inner, Klass.method.<locals>.helper), scored from nesting level 0, not folded into the function that encloses it. This keeps factory and registry shapes (FastAPI/Flask app factories, decorator factories, dispatch tables of closures) honest: the trivial outer function scores low and each inner handler is judged on its own merits. Lambdas, being anonymous, still fold into their enclosing function. See docs/nested-function-scoring.md for the rationale.

For gates pinned to pre-2.0.0 numbers, --nested=fold restores the old model (nested defs fold into the enclosing function; a decorator/closure factory is scored by its inner function) as a migration aid; the same is available in the library as get_cognitive_complexity(funcdef, fold_nested=True). See CHANGELOG.md.

Refactor suggestions

Beyond reporting a score, cococo points at what to do about a complex function: the default listing prints concrete, mechanical refactors inline — each with the lines it touches and an estimated complexity drop:

  14  src/load.py:42  load
    - Extract this block into a helper function (lines 50-61, ~-7 -> 7)
    - Flatten nested block with a guard clause (lines 45-61, ~-3 -> 11) [--fix]

--suggest-min N attaches suggestions only to functions scoring at least N (it defaults to --min), to focus the output on the worst offenders. Under a --max gate the offending functions are reported on stderr with the same suggestions instead, so a failing CI step says exactly what to fix. --no-suggest skips suggestion computation entirely — a faster path for a CI gate that only needs the pass/fail.

Suggestions tagged [--fix] can be applied automatically. --fix rewrites only transforms it can prove keep behavior identical (an if with no else that is the last statement of a function or loop body becomes an early return/continue guard, de-indenting its body); anything else is left untouched, and comments/formatting in the moved body are preserved. The file is written back as itself — same encoding (a coding: declaration or a BOM is honored, not flattened to UTF-8) and same line endings, so a CRLF file does not come back as a whole-file diff. When in doubt --fix skips rather than writes: a symlinked .py is reported and left as a link (name its target to rewrite it), and a file that changed on disk while it was being transformed is skipped instead of overwritten with the stale rewrite.

Adopting the gate incrementally

No real codebase passes a strict ceiling on day one, so the --max gate has two ways to grandfather existing offenders rather than be all-or-nothing:

  • Per-function: put # cococo: ignore on a function's def line to exclude that one function from the gate (the listing still shows it). cococo warns when an ignore is no longer needed (the function is back under the ceiling), so the directives don't rot silently.

    def legacy_handler(req):  # cococo: ignore
        ...
    
  • Whole codebase: --baseline FILE (requires --max) records every current score the first time it runs, then on later runs fails only on regressions — a function rising above its recorded score, or new code over --max. This lets a team adopt the gate against a dirty tree in one commit and ratchet down from there. Commit the baseline file; delete it to re-baseline.

Exit codes

In gate mode (--max), the exit code distinguishes the outcomes a CI step cares about:

  • 0 — all functions within the ceiling (or, without --max, a successful listing).
  • 1 — one or more functions exceed the ceiling (offenders printed with suggestions).
  • 2 — the gate could not be trusted: no functions were scanned (a typo'd or empty path), a file was skipped (unreadable, unparseable, or too deeply nested to score), or a --fix write failed or was skipped as unsafe. A 2 means "fix the setup", not "code is too complex".

The same split holds outside the gate: --explain exits 2 when it cannot resolve its target (missing file, unknown qualname or line, unparseable source), never 1 — so 1 only ever means "over the ceiling". A run that exits 2 also never creates a --baseline file: the recorded ceilings would come from a scan the tool itself did not trust.

JSON output

--json emits the same scores, per-construct breakdowns, and suggestions as a single JSON document on stdout (exit code still gates on --max), so cococo drops into a pipeline:

cococo src/ --max 20 --json | jq '.functions[] | select(.over)'

Library

Score every function under a path (no CLI required):

from cognitive_complexity.discovery import scored_functions

for f in scored_functions(["src/"]):
    print(f.qualname, f.score)

scored_functions returns a list of ScoredFunction named tuples with .score, .qualname, .path, .lineno, .funcdef, .breakdown, and .ignored fields.

The suggestion engine is importable too:

from cognitive_complexity.api import get_cognitive_complexity_breakdown
from cognitive_complexity.detectors import suggest_refactors

breakdown = get_cognitive_complexity_breakdown(funcdef)
for s in suggest_refactors(funcdef, breakdown):
    print(s.title, s.estimated_reduction)

The low-level AST API:

>>> import ast
>>> funcdef = ast.parse("""
... def f(a):
...     return a * f(a - 1)  # +1 for recursion
... """).body[0]

>>> from cognitive_complexity.api import get_cognitive_complexity
>>> get_cognitive_complexity(funcdef)
1

What's different from upstream

This fork diverges from Melevir/cognitive_complexity 1.3.0:

  • async for is counted as a loop (upstream scored it 0).
  • match/case is counted as a single branching structure plus a nesting level (upstream did not handle it).
  • comprehension if filters each count as a decision point.
  • method recursion (self.method(...) / cls.method(...)) is detected, not only bare-name recursion.
  • named nested functions are scored as their own units (reported as outer.<locals>.inner), not folded into the enclosing function; lambdas still fold. This removes the per-containment nesting surcharge on factory/registry code and the old is_decorator special case — both still available via --nested=fold for pre-2.0.0 compatibility. See docs/nested-function-scoring.md.
  • a cococo command-line interface, with heuristic refactor suggestions on a failing gate, a --json report for pipelines, and a --fix flag that applies provably safe guard-clause rewrites.
  • Python 3.10+ only; type hints and packaging modernized.

The core control-flow scoring (Campbell's rules) is unchanged — it is the empirically validated part of the metric.

What is cognitive complexity

For a synthesis of the research and industry thinking on what makes code hard to understand — and how cognitive complexity fits in — see docs/cognitive-complexity-of-code.md.

Here are some readings about cognitive complexity:

Realization details

This is not a precise realization of the original algorithm proposed by G. Ann Campbell, but it gives rather similar results. The algorithm gives complexity points for breaking control flow, nesting, recursion, and stacked logical operations.

Known limitation: only direct recursion is detected (a function calling itself by name, or via self/cls). Indirect/mutual recursion — a() calls b() calls a() — is not counted, since detecting it needs a whole-program call graph rather than the single-function AST this tool works from.

Development

To develop cococo, first set up and activate a virtual environment so the toolchain (python, pytest, etc.) is available on your PATH:

# With standard pip/venv:
python -m venv .venv
source .venv/bin/activate
pip install -r requirements_dev.txt

# Or with uv:
uv venv
uv pip install -r requirements_dev.txt

Once the environment is active (or by prefixing commands with uv run, e.g., uv run just test), you can use the just recipes:

just install-hooks  # pre-push runs `just check` (the same gate as CI)
just check          # format-check + lint + type-check + complexity + tests + readme lint
just test           # tests with coverage
just bench          # performance benchmark

just check is the single gate — CI runs the exact same recipe.

License

MIT. See LICENSE. Original work © Ilya Lebedev and contributors.

Download files

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

Source Distribution

codecoco-4.0.0.tar.gz (76.5 kB view details)

Uploaded Source

Built Distribution

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

codecoco-4.0.0-py3-none-any.whl (52.7 kB view details)

Uploaded Python 3

File details

Details for the file codecoco-4.0.0.tar.gz.

File metadata

  • Download URL: codecoco-4.0.0.tar.gz
  • Upload date:
  • Size: 76.5 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for codecoco-4.0.0.tar.gz
Algorithm Hash digest
SHA256 2079ee8d9e7ac8b763719b6f27582cfb0fa195cd25a50ab320af973c80a9ae2c
MD5 4381a90d4f1c12d4cd2f001f3360d941
BLAKE2b-256 e2bfc6248879ef0bd83883ac95a1d569ac8229bc0b4932ce6deee15b74cd32f7

See more details on using hashes here.

Provenance

The following attestation bundles were made for codecoco-4.0.0.tar.gz:

Publisher: release.yml on qwhex/cococo

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

File details

Details for the file codecoco-4.0.0-py3-none-any.whl.

File metadata

  • Download URL: codecoco-4.0.0-py3-none-any.whl
  • Upload date:
  • Size: 52.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for codecoco-4.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 aabeec622b6f2e1530024a252e62f5b41cd1e2aca72a3c5421c817e7ce0db967
MD5 8a0649645dc914fcad4b5208e1344e7e
BLAKE2b-256 300f0b132a48a64155db39b61cd37d78309a4c5c3d081bdc0543fad0c8f56fc5

See more details on using hashes here.

Provenance

The following attestation bundles were made for codecoco-4.0.0-py3-none-any.whl:

Publisher: release.yml on qwhex/cococo

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 Sentry Error logging StatusPage Status page