Skip to content

rohaquinlop/complexipy

Repository files navigation

complexipy

complexipy

Blazingly fast cognitive complexity analysis for Python, written in Rust.

PyPI Downloads License

Installation β€’ Quick Start β€’ Integrations β€’ Documentation β€’ Complexipy Teams

What is Cognitive Complexity?

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

Common Questions

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.

Installation

pip install complexipy
# or
uv add complexipy

Quick Start

Command Line

# 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

# 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 --ratchet
# Analyze current directory while excluding specific files
complexipy . --exclude path/to/exclude.py --exclude path/to/other/exclude.py

Python API

from 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}")

Integrations

πŸ”§ 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: v4.2.0
      hooks:
          - id: complexipy
πŸ”Œ VS Code Extension

Install from the marketplace for real-time complexity analysis with visual indicators.

Configuration

TOML Configuration Files

complexipy supports configuration via TOML files. Configuration files are loaded in this order of precedence:

  1. complexipy.toml (project-specific config)
  2. .complexipy.toml (hidden config file)
  3. pyproject.toml (under [tool.complexipy] section)

Example Configuration

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

CLI Options

Flag Description Default
--exclude Exclude entries relative to each provided path. Entries resolve to existing directories (prefix match) or files (exact match). Non-existent entries are ignored.
--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
--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 (e.g. HEAD~1, main) β€”
--ratchet With --diff, fail only when a change pushes a function above --max-complexity-allowed or makes an already-over function worse false
--check-script Report module-level (script) complexity as a synthetic <module> entry 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 only top-level 'tests' directory under the provided root
complexipy . --exclude tests
# This will not exclude './complexipy/utils.py' if you pass '--exclude utils' at repo root,
# because there is no './utils' directory or file at that level.

Snapshot Baselines

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-ignore

The 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).

Complexity Diff

Compare complexity against any git reference to see at a glance 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 main

# Fail only on threshold-breaking regressions
complexipy . --diff main --ratchet

# Combine with other flags
complexipy src/ --max-complexity-allowed 10 --diff HEAD~1

--ratchet requires --diff. It exits with code 1 only when a new or modified function breaches --max-complexity-allowed; regressions that stay within the threshold are allowed.

Sample output:

Complexity diff  (vs HEAD~1)
────────────────────────────────────────────────────────────────────────
  REGRESSED   src/api.py::handle_request                        12 β†’ 19  (+7)
  IMPROVED    src/utils.py::flatten_tree                        22 β†’ 14  (-8)
  NEW         src/auth.py::validate_token                       17        (new)
────────────────────────────────────────────────────────────────────────
Net: 1 regressed, 1 improved, 1 new

The diff is appended after the normal analysis output and does not affect the exit code. Requires git to be available and the analysed paths to be inside a git repository.

Script Complexity

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-script

The same capability is available in the Python API via check_script=True on both file_complexity() and code_complexity().

Inline Ignores

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 0

Place # 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.

API Reference

# Core functions
file_complexity(path: str, check_script: bool = False) -> FileComplexity
code_complexity(source: str, check_script: bool = False) -> CodeComplexity

# Return types
FileComplexity:
  β”œβ”€ path: str
  β”œβ”€ complexity: int
  └─ functions: List[FunctionComplexity]

FunctionComplexity:
  β”œβ”€ name: str
  β”œβ”€ complexity: int
  β”œβ”€ line_start: int
  └─ line_end: int

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

Sponsor this project

 

Packages

 
 
 

Contributors