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 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 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.
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: ignoreon a function'sdefline 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
--fixwrite failed. A2means "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.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 foris counted as a loop (upstream scored it 0).match/caseis counted as a single branching structure plus a nesting level (upstream did not handle it).- comprehension
iffilters 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 oldis_decoratorspecial case — both still available via--nested=foldfor pre-2.0.0 compatibility. See docs/nested-function-scoring.md. - a
cocococommand-line interface, with heuristic refactor suggestions on a failing gate, a--jsonreport for pipelines, and a--fixflag 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:
- Cognitive Complexity, Because Testability != Understandability;
- Cognitive Complexity: A new way of measuring understandability, white paper by G. Ann Campbell;
- Cognitive Complexity: the New Guide to Refactoring for Maintainable Code;
- Cognitive Complexity from CodeClimate docs;
- Is Your Code Readable By Humans? Cognitive Complexity Tells You.
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
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 codecoco-3.7.0.tar.gz.
File metadata
- Download URL: codecoco-3.7.0.tar.gz
- Upload date:
- Size: 62.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
f75a496fa9abce84fae7c31ae406b5051decd9cbc2669ccc027bd06d9f7dc949
|
|
| MD5 |
aa38835878848aa6fa2cb44d72980a85
|
|
| BLAKE2b-256 |
ba5277bf6710a564b375faf2ec1753cc40496fc4c2c7ea82a7ed274d062ebdff
|
Provenance
The following attestation bundles were made for codecoco-3.7.0.tar.gz:
Publisher:
release.yml on qwhex/cococo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
codecoco-3.7.0.tar.gz -
Subject digest:
f75a496fa9abce84fae7c31ae406b5051decd9cbc2669ccc027bd06d9f7dc949 - Sigstore transparency entry: 1933316810
- Sigstore integration time:
-
Permalink:
qwhex/cococo@d39fe679a37817aa394b2cd9823560a69ce110e1 -
Branch / Tag:
refs/tags/v3.7.0 - Owner: https://github.com/qwhex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d39fe679a37817aa394b2cd9823560a69ce110e1 -
Trigger Event:
push
-
Statement type:
File details
Details for the file codecoco-3.7.0-py3-none-any.whl.
File metadata
- Download URL: codecoco-3.7.0-py3-none-any.whl
- Upload date:
- Size: 46.0 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
386e2b877fbfd1092b125248ac270e8713478b3a84b9e6588eb4b1b65ddd5378
|
|
| MD5 |
320a9fb78bf1f2c124e27b7a4ba6c249
|
|
| BLAKE2b-256 |
7efff633b4d5421044805b1d3dcfcf15ddb6ea3c9feff71fa9b20826aab80d37
|
Provenance
The following attestation bundles were made for codecoco-3.7.0-py3-none-any.whl:
Publisher:
release.yml on qwhex/cococo
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
codecoco-3.7.0-py3-none-any.whl -
Subject digest:
386e2b877fbfd1092b125248ac270e8713478b3a84b9e6588eb4b1b65ddd5378 - Sigstore transparency entry: 1933316873
- Sigstore integration time:
-
Permalink:
qwhex/cococo@d39fe679a37817aa394b2cd9823560a69ce110e1 -
Branch / Tag:
refs/tags/v3.7.0 - Owner: https://github.com/qwhex
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d39fe679a37817aa394b2cd9823560a69ce110e1 -
Trigger Event:
push
-
Statement type: