Skip to main content

Library and CLI to compute the cognitive complexity of Python functions

Project description

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 every function, worst first
cococo src/ --max 20         # gate: exit non-zero if any function exceeds 20
cococo a.py b.py --min 10    # only show functions scoring >= 10
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 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 on a failing gate

When --max is exceeded, each offending function is reported on stderr together with a few concrete, mechanical refactors and an estimated complexity drop — so a human (or an agent) reading the failure knows what to do next:

cococo: 1 function(s) exceed cognitive complexity 5
  src/load.py:42 load = 14 (>5)
    - 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]

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.

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. A 2 means "fix the setup", not "code is too complex".

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.refactor 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.

Project details


Download files

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

Source Distribution

codecoco-3.5.1.tar.gz (49.6 kB view details)

Uploaded Source

Built Distribution

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

codecoco-3.5.1-py3-none-any.whl (30.7 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for codecoco-3.5.1.tar.gz
Algorithm Hash digest
SHA256 018f62513df490f3e309ae3ffbbcb9fdda84ab6ddaca7adc317eef96d501186a
MD5 b4e969c73b2a9a75b436f9ecfca92864
BLAKE2b-256 da3adeb7910b234ddc064a86689d95a9144747e779b5751f62796768faab9e05

See more details on using hashes here.

Provenance

The following attestation bundles were made for codecoco-3.5.1.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-3.5.1-py3-none-any.whl.

File metadata

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

File hashes

Hashes for codecoco-3.5.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2968f28e4a35734a9d19bcc2800ff701118ffbce8f5cdbd5baa82acd07cf2315
MD5 787fa0816c98beaaf957d23e67a35b63
BLAKE2b-256 ac33f5e9af8d90207090e312bfe0265d605fadd5953ad87cd1ef9f028e61461a

See more details on using hashes here.

Provenance

The following attestation bundles were made for codecoco-3.5.1-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 Pingdom Monitoring Sentry Error logging StatusPage Status page