Blazingly fast cognitive complexity analysis for Python, written in Rust.
Installation β’ Quick Start β’ Integrations β’ Documentation β’ Complexipy Teams
Cognitive complexity measures how hard code is to understand by humans, not machines.
Unlike traditional metrics like cyclomatic complexity, cognitive complexity accounts for nesting depth and control flow patterns that affect human comprehension. Inspired by G. Ann Campbell's research at SonarSource, complexipy provides a fast, accurate implementation for Python.
Key benefits:
- Human-focused - Penalizes nesting, flow breaks, and human-unfriendly logic
- Actionable insights - Identifies genuinely hard-to-maintain code
- Different from cyclomatic - Measures readability while cyclomatic measures structural, testing, and branch density
How is complexity calculated? Learn about the scoring algorithm, what each control structure contributes, and how nesting affects the final score.
How does this compare to Ruff's PLR0912? Understand the key differences between cyclomatic complexity (Ruff) and cognitive complexity (complexipy), and why you might want to use both.
Is this a SonarSource/Sonar product? No. complexipy is an independent project inspired by G. Ann Campbell's research, but it's not affiliated with or endorsed by SonarSource.
pip install complexipy
# or
uv add complexipy# Analyze current directory
complexipy .
# Analyze specific file/directory
complexipy path/to/code.py
# Analyze with custom threshold
complexipy . --max-complexity-allowed 10
# Save results to JSON/CSV
complexipy . --output-format json --output-format csv
# Show the top 5 most complex functions
complexipy . --top 5
# Show deterministic refactor suggestions for failing functions
complexipy . --failed --suggest-refactors
# Emit plain text for scripting/AI agents
complexipy . --plain
# Include module-level script complexity as <module>
complexipy . --check-script
# Write a GitLab report to a deterministic path
complexipy . --output-format gitlab --output complexipy-code-quality.json
# Compare complexity against a git reference
complexipy . --diff HEAD~1
# Fail only on threshold-breaking regressions in a diff
complexipy . --diff main
# Analyze current directory while excluding files or directories with glob patterns
complexipy . --exclude "tests/**" --exclude "path/to/exclude.py"
# Disregard all inline ignore comments
complexipy . --no-ignore
# Report all ignored functions
complexipy . --report-ignoredfrom complexipy import file_complexity, code_complexity
# Analyze a file
result = file_complexity("app.py", check_script=True)
print(f"File complexity: {result.complexity}")
for func in result.functions:
print(f"{func.name}: {func.complexity}")
# Analyze code string
snippet = """
def complex_function(data):
if data:
for item in data:
if item.is_valid():
process(item)
"""
result = code_complexity(snippet, check_script=True)
print(f"Complexity: {result.complexity}")π§ GitHub Actions
- uses: rohaquinlop/complexipy-action@v2
with:
paths: .
max_complexity_allowed: 10
output_format: jsonπ GitHub Code Scanning (SARIF)
Upload complexity violations as inline PR annotations using SARIF:
- name: Run complexipy
run: complexipy . --output-format sarif --output complexipy-results.sarif --ignore-complexity
- name: Upload SARIF results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: complexipy-results.sarifπ¦ GitLab Code Quality
Publish complexity violations as a GitLab Code Quality artifact:
complexity:
image: python:3.11
script:
- pip install complexipy
- complexipy . --output-format gitlab --output complexipy-code-quality.json --ignore-complexity
artifacts:
when: always
reports:
codequality: complexipy-code-quality.jsonπͺ Pre-commit Hook
repos:
- repo: https://github.com/rohaquinlop/complexipy-pre-commit
rev: v5.1.0
hooks:
- id: complexipyπ VS Code Extension
Install from the marketplace for real-time complexity analysis with visual indicators.
complexipy supports configuration via TOML files. Configuration files are loaded in this order of precedence:
complexipy.toml(project-specific config).complexipy.toml(hidden config file)pyproject.toml(under[tool.complexipy]section)
# complexipy.toml or .complexipy.toml
paths = ["src", "tests"]
max-complexity-allowed = 10
snapshot-create = false
snapshot-ignore = false
quiet = false
ignore-complexity = false
failed = false
color = "auto"
sort = "asc"
exclude = []
check-script = false
no-ignore = false
report-ignored = false
output-format = ["json", "sarif"]
output = "reports/"# pyproject.toml
[tool.complexipy]
paths = ["src", "tests"]
max-complexity-allowed = 10
snapshot-create = false
snapshot-ignore = false
quiet = false
ignore-complexity = false
failed = false
color = "auto"
sort = "asc"
exclude = []
check-script = false
no-ignore = false
report-ignored = false
output-format = ["json"]
output = "complexipy-results.json"Legacy TOML keys such as output-json = true and CLI flags such as
--output-json still work for now, but they are deprecated in favor of
output-format and --output-format.
check-script is supported in TOML. --top and --plain are CLI-only flags.
| Flag | Description | Default |
|---|---|---|
--exclude |
Exclude glob patterns relative to each provided path. Use patterns like tests/** for directories or src/legacy/file.py for specific files. |
|
--max-complexity-allowed |
Complexity threshold | 15 |
--snapshot-create |
Save the current violations above the threshold into complexipy-snapshot.json |
false |
--snapshot-ignore |
Skip comparing against the snapshot even if it exists | false |
--failed |
Show only functions above the complexity threshold | false |
--suggest-refactors |
Show deterministic Rust AST-based refactor plans in rich CLI output. Ignored by --plain |
false |
--color <auto|yes|no> |
Use color | auto |
--sort <asc|desc|file_name> |
Sort results | asc |
--quiet |
Suppress output | false |
--ignore-complexity |
Don't exit with error on threshold breach | false |
--version |
Show installed complexipy version and exit | - |
--top <n> |
Show only the n most complex functions, globally sorted by complexity descending |
β |
--plain |
Emit plain text lines as <path> <function> <complexity>. Cannot be combined with --quiet |
false |
--output-format <format> |
Select a machine-readable output format. Repeat the flag to request multiple formats (json, csv, gitlab, sarif) |
β |
--output <path> |
Write machine-readable output to a file or directory. Use a directory when emitting multiple formats | β |
--diff <ref> |
Show a complexity diff against a git reference and enforce the threshold. Fails on regressions above --max-complexity-allowed (see Complexity Diff) |
β |
--diff-only <ref> |
Show a complexity diff visually without affecting the exit code (see Complexity Diff) | β |
--ratchet, -R |
Deprecated. Use --diff instead, which now enforces by default |
false |
--check-script |
Report module-level (script) complexity as a synthetic <module> entry |
false |
--no-ignore |
Analyze every function, disregarding inline ignore comments (# complexipy: ignore, # noqa: complexipy) |
false |
--report-ignored |
List every file:line where an ignore comment suppresses a function. Prints even under --quiet |
false |
--output-json |
Deprecated alias for --output-format json |
false |
--output-csv |
Deprecated alias for --output-format csv |
false |
--output-gitlab |
Deprecated alias for --output-format gitlab |
false |
--output-sarif |
Deprecated alias for --output-format sarif |
false |
Example:
# Exclude a directory recursively under the provided root
complexipy . --exclude "tests/**"
# Exclude a specific file
complexipy . --exclude "src/legacy/old_code.py"
Use --suggest-refactors to print a small, ranked set of deterministic refactor plans next to rich CLI results:
complexipy . --failed --suggest-refactorsSample output:
Refactor plans:
β’ Flatten nested condition block with guard clauses (lines 3-8, estimated: 7 -> 5 (-2))
- invert the outer condition and return early
Plans are based on the Rust AST analysis only; no AI is used and no code is rewritten automatically. Estimated reductions are approximate, ranked, and limited, so treat them as guidance rather than exact future scores. --plain --suggest-refactors keeps plain output unchanged.
Use snapshots to adopt complexipy in large, existing codebases without touching every legacy function at once.
# Record the current state (creates complexipy-snapshot.json in the working directory)
complexipy . --snapshot-create --max-complexity-allowed 15
# Block regressions while allowing previously-recorded functions
complexipy . --max-complexity-allowed 15
# Temporarily skip the snapshot gate
complexipy . --snapshot-ignoreThe snapshot file only stores functions whose complexity exceeds the configured threshold. When a snapshot file exists, complexipy will automatically:
- fail if a new function crosses the threshold,
- fail if a tracked function becomes more complex, and
- pass (and update the snapshot) when everything is stable or improved, automatically removing entries that now meet the standard.
Use --snapshot-ignore if you need to temporarily bypass the snapshot gate (for example during a refactor or while regenerating the baseline).
Compare complexity against any git reference to see whether a branch or commit made things better or worse:
# Compare the working tree against the previous commit
complexipy . --diff HEAD~1
# Compare against a named branch
complexipy . --diff mainBy default --diff enforces the complexity threshold: the run exits with code 1 only when a change breaches the contract relative to --max-complexity-allowed:
- A new function is introduced above the threshold, or
- an existing function's complexity increases and ends above the threshold (already-over functions getting worse also fail).
Functions that regress but stay at or below the threshold (e.g. 3 β 4 with -mx 15) are not flagged β the threshold is still the main contract, and --diff only catches regressions that actually break it.
To see the diff visually without affecting the exit code, use --diff-only instead:
complexipy . --diff-only HEAD~1Requires git to be available and the analysed paths to be inside a git repository.
Use --check-script when you also want to score module-level control flow, not just functions:
# Report top-level script logic as <module>
complexipy scripts/bootstrap.py --check-scriptThe same capability is available in the Python API via check_script=True on both file_complexity() and code_complexity().
You can explicitly ignore a known complex function inline, similar to Ruff's C901 ignores:
def legacy_adapter(x, y): # complexipy: ignore
if x and y:
return x + y
return 0Place # complexipy: ignore on the function definition line (or the line immediately above). An optional reason can be provided in parentheses or plain text, it's ignored by the parser.
Use --no-ignore to disregard all inline ignore comments and analyze every function:
# Treat every function equally, regardless of suppression
complexipy . --no-ignoreFunctions previously suppressed by # complexipy: ignore or # noqa: complexipy will be analyzed normally and may fail the threshold.
Use --report-ignored to list every location where an ignore comment suppresses a function:
# List ignored functions
complexipy . --report-ignored
# Combine with --no-ignore to report while analyzing everything
complexipy . --report-ignored --no-ignoreWhen --output-format json is also active, ignored locations are exported to complexipy-ignored.json. The report prints even under --quiet.
Both flags are also available in the Python API via no_ignore=True on file_complexity() and code_complexity(). To programmatically collect ignored locations, use collect_all_ignored_locations() from the complexipy package.
# Core functions
file_complexity(path: str, check_script: bool = False, no_ignore: bool = False) -> FileComplexity
code_complexity(source: str, check_script: bool = False, no_ignore: bool = False) -> CodeComplexity
collect_all_ignored_locations(paths: List[str], exclude: List[str] = [], invocation_path: str = "") -> Tuple[List[IgnoredLocation], List[str]]
# Return types
FileComplexity:
ββ path: str
ββ file_name: str
ββ complexity: int
ββ functions: List[FunctionComplexity]
FunctionComplexity:
ββ name: str
ββ complexity: int
ββ line_start: int
ββ line_end: int
ββ line_complexities: List[LineComplexity]
ββ refactor_plans: List[RefactorPlan]
RefactorPlan:
ββ kind: str
ββ title: str
ββ line_start: int
ββ line_end: int
ββ current_complexity: int
ββ estimated_reduction: int
ββ estimated_complexity_after: int
ββ steps: List[str]
LineComplexity:
ββ line: int
ββ complexity: int
IgnoredLocation:
ββ path: str
ββ line: int
ββ comment: str
CodeComplexity:
ββ complexity: int
ββ functions: List[FunctionComplexity]Inspired by the Cognitive Complexity research by G. Ann Campbell
complexipy is an independent project and is not affiliated with or endorsed by SonarSource
Documentation β’ PyPI β’ GitHub
Built with β€οΈ by @rohaquinlop and contributors