Usage Guide¶
This guide covers everything you need to know to effectively use complexipy in your Python projects.
Installation¶
Command Line Usage¶
Basic Analysis¶
Analyze your entire project:
Analyze specific files or directories:
Setting Complexity Threshold¶
The default threshold is 15. Functions exceeding this value will be highlighted:
Filtering Results¶
Show only functions that exceed the threshold:
Show only the top N most complex functions across the analyzed files:
Show deterministic refactor suggestions for failing functions:
When --top is set, results are globally re-sorted by complexity descending
before truncation.
Suppress analysis output (useful for CI pipelines):
Sorting Results¶
Sort by complexity score:
complexipy . --sort asc # Ascending (default)
complexipy . --sort desc # Descending
complexipy . --sort file_name # Alphabetically by file name
Excluding Files and Directories¶
Exclude specific paths from analysis:
# Exclude a directory recursively
complexipy . --exclude "tests/**"
# Exclude multiple directories recursively
complexipy . --exclude "tests/**" --exclude "migrations/**" --exclude "build/**"
# Exclude specific files
complexipy . --exclude "src/legacy/old_code.py"
How exclusion works
- Exclusions are glob patterns evaluated relative to each provided root path
- Use
directory/**to exclude a directory recursively - Use an exact relative file path, such as
src/legacy/old_code.py, to exclude one file - Gitignore rules are still respected during file discovery
Output Formats¶
Save results to JSON, CSV, GitLab Code Quality, or SARIF:
# JSON output (saved to complexipy-results.json)
complexipy . --output-format json
# CSV output (saved to complexipy-results.csv)
complexipy . --output-format csv
# Both
complexipy . --output-format json --output-format csv
# Explicit destination for a single format
complexipy . --output-format gitlab --output complexipy-code-quality.json
# Multiple formats written into a directory
complexipy . --output-format json --output-format sarif --output reports/
# GitLab Code Quality report
complexipy . --output-format gitlab
# SARIF output
complexipy . --output-format sarif
# Add --suggest-refactors to any of the above to also carry refactor rule
# findings (C001, C007, ...) as their own SARIF/GitLab rule IDs, alongside
# the always-present cognitive-complexity findings.
complexipy . --output-format sarif --suggest-refactors
JSON, SARIF, and GitLab Code Quality all follow the same rule: refactor plan
data (refactor_plans in JSON, per-rule findings in SARIF/GitLab) is only
emitted when --suggest-refactors is also passed. Without it, only the
cognitive-complexity threshold findings appear -- matching the CLI's rich
output, which also hides refactor suggestions unless the flag is set.
Complexity Diff¶
Compare current results against any git reference:
complexipy . --diff HEAD~1
complexipy . --diff main
complexipy src/ --max-complexity-allowed 10 --diff HEAD~1
By 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 introduced above the threshold.
- A modified function whose complexity increased and ends above the threshold (including already-over functions that get worse).
Functions that regress but stay at or below the threshold (e.g. 3 → 4 with --max-complexity-allowed 15) are not failures.
To see the diff visually without affecting the exit code, use --diff-only instead:
To compare the staged (git index) content instead of the working tree — "what complexity am I about to commit?" — add --staged:
# Staged changes vs HEAD (the default baseline for --staged)
complexipy . --staged
# Staged changes vs a specific ref
complexipy . --diff main --staged
Like --diff, --staged enforces the threshold against the staged content and fails when a staged change pushes a function above --max-complexity-allowed. Staged deletions produce REMOVED entries and staged additions NEW entries. Use --diff-only with --staged for a visual-only view.
This requires git and a repository-backed path.
Instead of repeating the reference on every call, declare the comparison policy once in a configuration file — see Diff Configuration.
Ratchet Mode¶
Plain Output¶
Use plain output when you need one machine-friendly line per function:
Each line is emitted as:
--plain is CLI-only and cannot be combined with --quiet.
Module-Level Script Complexity¶
Use --check-script to include module-level code in the results as
<module>:
This is useful for scripts with complex top-level control flow outside functions.
Refactor Suggestions¶
Use --suggest-refactors to print a small, ranked set of deterministic refactor plans next to rich CLI results:
Sample output (abbreviated -- the real output also shows a caret-underlined span, the surrounding source, and a documentation link):
[1] C007 Merge nested if statements
--> sample.py:4:9
Category: ◆ Readability | Applicability: * Safe to apply
Lines 4-6 -> Estimated reduction: -2 complexity (6 -> 4)
Suggestion: * Safe to apply
Merge nested conditions into `if item.active and item.ready:`
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.
JSON Output Structure:
[
{
"path": "src",
"file_name": "main.py",
"function_name": "process_data",
"complexity": 6,
"refactor_plans": [
{
"rule_id": "C007",
"kind": "collapsible_if",
"title": "Merge nested if statements",
"line_start": 4,
"line_end": 6,
"column_start": 9,
"current_complexity": 6,
"estimated_reduction": 2,
"estimated_complexity_after": 4,
"category": "Readability",
"applicability": "MachineApplicable",
"description": "Merge nested if statements into a single if with combined conditions",
"explanation": "Nested if statements with a single body can be merged into a single if with combined conditions using 'and'. This reduces nesting and improves readability.",
"references": [],
"suggestion": {
"replacement": " if item.active and item.ready:\n total += item.value",
"applicability": "MachineApplicable",
"description": "Merge nested conditions into `if item.active and item.ready:`"
},
"help": null,
"doc_url": "https://rohaquinlop.github.io/complexipy/refactoring-rules/#c007-collapsible-if"
}
]
}
]
JSON output contains one entry per emitted function. The refactor_plans list is only populated when --suggest-refactors is also passed -- otherwise it's [], matching the CLI's rich-output behavior. CSV output is unchanged and does not include plans. Function line ranges are available through the Python API (line_start, line_end), but are not included in the machine-readable CLI JSON/CSV function entries.
Color Output¶
Control color output:
complexipy . --color auto # Default: auto-detect terminal support
complexipy . --color yes # Force colors
complexipy . --color no # Disable colors
Configuration Files¶
Configuration Priority¶
complexipy loads configuration in this order (highest to lowest priority):
- Command-line arguments
complexipy.toml.complexipy.tomlpyproject.toml(under[tool.complexipy])
Example Configurations¶
paths = ["src", "tests"]
max-complexity-allowed = 10
exclude = ["migrations/**", "build/**"]
snapshot-create = false
snapshot-ignore = false
quiet = false
ignore-complexity = false
failed = false
color = "auto"
sort = "asc"
output-format = ["json", "gitlab"]
output = "reports/"
check-script = false
no-ignore = false
report-ignored = false
check-script is supported in TOML. --top and --plain are CLI-only flags.
Diff Configuration¶
The comparison policy can be declared once in the repository instead of
passing the same flags on every call. Add a [tool.complexipy.diff]
section to the same configuration file:
branchsets the default reference for--diffand--diff-only. A plaincomplexipy .then behaves likecomplexipy . --diff main, enforcement included. Pass--diff <ref>or--diff-only <ref>to override the reference for a single run.stagedenables staged comparison by default, like passing--stagedon every call.- CLI flags always take precedence over the section values.
branch = ""disables the diff for the current repository (opt-out).- Resolution order: CLI flag, then the
diffsection. - If the configured
branchdoes not exist in the local clone (for example a fresh or shallow clone), every function reports asNEWand the enforcement still applies. Fetch the branch or pass--diff <ref>with an existing reference instead.
Python API¶
Analyzing Files¶
from complexipy import file_complexity
# Analyze a file
result = file_complexity("src/main.py", check_script=True)
print(f"Total complexity: {result.complexity}")
# Analyze without honoring inline ignore comments
result = file_complexity("src/main.py", no_ignore=True)
print(f"File path: {result.path}")
# Iterate over functions
for func in result.functions:
print(f"{func.name}:")
print(f" Complexity: {func.complexity}")
print(f" Lines: {func.line_start}-{func.line_end}")
Analyzing Code Strings¶
from complexipy import code_complexity
# Analyze code snippet
code = """
def calculate_discount(price, customer):
if customer.is_premium:
if price > 100:
return price * 0.8
else:
return price * 0.9
return price
"""
result = code_complexity(code, check_script=True)
print(f"Complexity: {result.complexity}")
# Analyze code string without honoring ignore comments
result = code_complexity(code, no_ignore=True)
for func in result.functions:
print(f"{func.name}: {func.complexity}")
Comparing Against a Git Reference¶
compute_diff compares current complexity results against a git reference
(commit, tag, or branch) and returns DiffEntry objects — one per function
that changed, appeared, or disappeared. has_regressions reports whether any
entry breaches a complexity threshold (a REGRESSED or NEW function above
max_complexity).
from complexipy import (
compute_diff,
has_regressions,
file_complexity,
DiffEntry,
DiffStatus,
)
# Analyze the current state of the files you care about
current = [file_complexity(p) for p in changed_files]
# Compare against a git reference (the working directory is the default cwd)
entries = compute_diff(current, "origin/main")
# Filter for regressions above your threshold
regressions = [
e
for e in entries
if e.status == DiffStatus.REGRESSED and e.new_complexity > 15
]
# Or use the ratchet gate directly (fails on REGRESSED/NEW above threshold)
if has_regressions(entries, 15):
raise SystemExit("Complexity regressions detected")
DiffEntry exposes file_path, func_name, old_complexity, and
new_complexity (either may be None for NEW / REMOVED functions), plus the
status and delta properties. status is a DiffStatus member — a
str-based enum, so it compares equal to its string value (e.g.
DiffStatus.REGRESSED == "REGRESSED").
for e in entries:
if e.status != DiffStatus.UNCHANGED:
print(f"{e.file_path}::{e.func_name}: {e.status} {e.delta}")
Practical API Usage¶
Example: Pre-commit Hook
#!/usr/bin/env python3
"""Check complexity of staged Python files."""
import sys
from pathlib import Path
from complexipy import file_complexity
MAX_COMPLEXITY = 15
def main():
# Get staged Python files (integrate with git)
staged_files = get_staged_python_files()
violations = []
for filepath in staged_files:
result = file_complexity(str(filepath))
for func in result.functions:
if func.complexity > MAX_COMPLEXITY:
violations.append({
'file': filepath,
'function': func.name,
'complexity': func.complexity,
'line': func.line_start
})
if violations:
print("Complexity violations found:")
for v in violations:
print(f" {v['file']}:{v['line']} - "
f"{v['function']} (complexity: {v['complexity']})")
sys.exit(1)
print("All functions pass complexity check!")
sys.exit(0)
if __name__ == "__main__":
main()
Example: Code Quality Dashboard
from pathlib import Path
from complexipy import file_complexity
import json
def analyze_project(root_path: str):
"""Generate complexity report for entire project."""
project = Path(root_path)
results = []
for py_file in project.rglob("*.py"):
if "venv" in str(py_file) or ".venv" in str(py_file):
continue
try:
result = file_complexity(str(py_file))
results.append({
'file': str(py_file),
'complexity': result.complexity,
'functions': [
{
'name': f.name,
'complexity': f.complexity,
'line_start': f.line_start,
'line_end': f.line_end
}
for f in result.functions
]
})
except Exception as e:
print(f"Error analyzing {py_file}: {e}")
# Sort by complexity
results.sort(key=lambda x: x['complexity'], reverse=True)
# Save report
with open("complexity-report.json", "w") as f:
json.dump(results, f, indent=2)
# Print summary
total_files = len(results)
total_complexity = sum(r['complexity'] for r in results)
avg_complexity = total_complexity / total_files if total_files else 0
print(f"Analyzed {total_files} files")
print(f"Total complexity: {total_complexity}")
print(f"Average complexity: {avg_complexity:.2f}")
# Top 10 most complex files
print("\nTop 10 most complex files:")
for r in results[:10]:
print(f" {r['file']}: {r['complexity']}")
if __name__ == "__main__":
analyze_project("./src")
Snapshot Baselines¶
Snapshots allow you to adopt complexipy gradually in large, existing codebases.
Creating a Snapshot¶
This creates complexipy-snapshot.json in your working directory, recording all functions that currently exceed the threshold.
How Snapshots Work¶
Once a snapshot exists, complexipy will:
- ✅ Pass: Functions that were already in the snapshot and haven't gotten worse
- ✅ Pass: Functions that improved (automatically removed from snapshot)
- ❌ Fail: New functions that exceed the threshold
- ❌ Fail: Tracked functions that got more complex
Using Snapshots in CI¶
# .github/workflows/complexity.yml
name: Complexity Check
on: [push, pull_request]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install complexipy
run: pip install complexipy
- name: Check complexity
run: complexipy . --max-complexity-allowed 15
The snapshot file (complexipy-snapshot.json) should be committed to version control.
Ignoring Snapshots¶
Temporarily disable snapshot checking:
Use this when:
- Refactoring multiple files at once
- Regenerating the baseline
- Testing different thresholds
Snapshot File Format¶
[
{
"path": "src",
"file_name": "legacy.py",
"functions": [
{
"name": "old_function",
"complexity": 23
}
]
}
]
Snapshots are stored as a JSON array of analyzed files. Each entry contains only functions above the threshold at the time the snapshot was written. The file is rewritten after successful snapshot checks, so improved functions are removed automatically. Updates only touch the files analyzed in the run — entries for files outside the analysis are preserved, so running on a subset of files (for example through a pre-commit hook) never shrinks the baseline. Snapshots created by older complexipy versions may need to be regenerated with --snapshot-create.
Inline Ignores¶
Suppress complexity warnings for specific functions using the # complexipy: ignore comment:
def complex_legacy_function(): # complexipy: ignore
# Complex logic that can't be refactored yet
pass
# Or with a reason
def another_complex_function(): # complexipy: ignore (technical debt: issue #123)
pass
The ignore comment can also be placed on the line above the function definition:
Deprecated Syntax
The # noqa: complexipy syntax is deprecated and will be removed in a future version.
Please migrate to # complexipy: ignore instead.
Why? Tools like yesqa automatically strip # noqa
comments that aren't recognized by flake8, which would silently remove your complexipy
suppressions. The new syntax avoids this conflict entirely.
Use Sparingly
Inline ignores should be temporary. Document why the complexity is necessary and track technical debt.
Disabling Inline Ignores¶
Use --no-ignore to disregard all inline ignore comments and analyze every function:
Functions previously suppressed by # complexipy: ignore or # noqa: complexipy will be analyzed normally and may fail the threshold.
Reporting Ignored Functions¶
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-ignore
The output format is path:line # comment-text. When --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:
from complexipy import file_complexity, code_complexity
# Analyze without honoring ignore comments
result = file_complexity("app.py", no_ignore=True)
To programmatically collect ignored locations, use collect_all_ignored_locations():
from complexipy import collect_all_ignored_locations
locations, failed = collect_all_ignored_locations(
paths=["src"],
exclude=["tests/"],
)
for loc in locations:
print(f"{loc.path}:{loc.line} {loc.comment}")
Removing Stale Ignore Comments¶
An ignore comment is only necessary while the suppressed function's complexity
exceeds --max-complexity-allowed. Once the function's complexity drops to or
below the limit, the comment is stale and can be removed. complexipy detects
this automatically on every run and reports the locations to clean up:
Example output:
The following ignore comment(s) are no longer necessary (complexity is within the allowed limit) and can be removed:
src/legacy.py:42 function=parse_legacy_config complexity=8 # complexipy: ignore
The report is purely informational: it never affects the exit code, and it is
suppressed under --quiet. The same detection is available programmatically
via collect_removable_ignored_locations():
from complexipy import collect_removable_ignored_locations
removable, failed = collect_removable_ignored_locations(
paths=["src"],
exclude=["tests/"],
max_complexity_allowed=15,
)
for rem in removable:
print(f"{rem.path}:{rem.line} function={rem.function} complexity={rem.complexity}")
CI/CD Integration¶
GitHub Actions¶
Use the official action:
- uses: rohaquinlop/complexipy-action@v2
with:
paths: src tests
max_complexity_allowed: 15
output_format: json
Or run directly:
Pre-commit Hook¶
Add to .pre-commit-config.yaml:
repos:
- repo: https://github.com/rohaquinlop/complexipy-pre-commit
rev: v5.1.0
hooks:
- id: complexipy
args: [--max-complexity-allowed=15]
GitLab CI¶
.complexipy_code_quality:
image: python:3.11
script:
- pip install complexipy
- complexipy . --output-format gitlab --output complexipy-code-quality.json --ignore-complexity --max-complexity-allowed 15
artifacts:
when: always
reports:
codequality: complexipy-code-quality.json
complexity:
extends: .complexipy_code_quality
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
Use --ignore-complexity when your main goal is to upload the report even if violations exist. GitLab will still display the findings in the merge request widget and pipeline UI.
If you also want the job to fail when the threshold is exceeded, split the workflow into two jobs:
complexity_check:
image: python:3.11
script:
- pip install complexipy
- complexipy . --max-complexity-allowed 15
complexity_report:
image: python:3.11
script:
- pip install complexipy
- complexipy . --output-format gitlab --output complexipy-code-quality.json --ignore-complexity --max-complexity-allowed 15
artifacts:
when: always
reports:
codequality: complexipy-code-quality.json
VS Code Integration¶
Install the complexipy extension for real-time complexity analysis:
- Inline complexity scores
- Hover tooltips with details
- Color-coded indicators
- Quick-fix suggestions
Tips and Best Practices¶
1. Start with High Thresholds¶
When introducing complexipy to an existing codebase:
# Create baseline
complexipy . --snapshot-create --max-complexity-allowed 25
# Gradually lower threshold over time
complexipy . --max-complexity-allowed 20
complexipy . --max-complexity-allowed 15
2. Focus on High-Traffic Code¶
Not all complex code needs immediate refactoring:
# Focus on frequently changed files
complexipy src/core/ --max-complexity-allowed 10
complexipy src/legacy/ --max-complexity-allowed 25
3. Use with Code Reviews¶
4. Combine with Test Coverage¶
High complexity + low coverage = high risk
# Check coverage for complex functions
pytest --cov=src --cov-report=term-missing
complexipy src/ --failed
5. Track Trends¶
# Generate historical data
complexipy . --output-format json
# Commit complexipy-results.json to track changes over time
Troubleshooting¶
No Python files found¶
Ensure you're in the correct directory and your files have .py extensions.
Syntax errors in analyzed files¶
complexipy requires valid Python syntax. Fix syntax errors first:
Performance issues on large codebases¶
Exclude unnecessary directories:
Different results than expected¶
Check configuration file precedence. Use --help to see active configuration:
Next Steps¶
- Read Understanding Complexity Scores to interpret results
- See Comparison with Ruff for complementary tooling
- Check out About complexipy to learn more about the project