The riskiest function in behave is setup_paths in behave/runner.py, and it hasn’t been changed in 462 days, so I read it as latent debt rather than an emergency: a cyclomatic complexity of 26 and 17 distinct callees waiting for the next developer who has to modify path discovery. Across 1,562 functions, 27 are critical, and all five of my top hotspots are in the debt quadrant, which makes this a structural-debt story and not a live-regression one. I would start with setup_paths and run_behave because they combine the highest complexity with the broadest coupling in the project.
The table below ranks functions by activity-weighted risk — a score that multiplies structural complexity by recent commit frequency. A function that is both hard to understand (high cyclomatic complexity) and actively changing is a higher priority than one that is complex but untouched. CC = cyclomatic complexity (independent execution paths); ND = max nesting depth; FO = fan-out (distinct callees).
Top 5 Hotspots
| Function | File | Risk | CC | ND | FO |
|---|---|---|---|---|---|
setup_paths | behave/runner.py | 13.9 | 26 | 3 | 17 |
run_hook | behave/runner.py | 13.3 | 18 | 3 | 16 |
run_behave | behave/main.py | 13.2 | 29 | 2 | 17 |
get_code_without_docstring | behave/formatter/steps_code.py | 12.9 | 16 | 5 | 9 |
collect_feature_locations | behave/runner_util.py | 12.0 | 10 | 5 | 16 |
1,562 functions analyzed
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Long Function×4Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.Exit Heavy×3Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.Stale Complex×3Stale Complex
High structural complexity but untouched for a long time — structural debt that will bite whoever opens it next.Complex Branching×2Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Deeply Nested×2Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.
The distribution tells the story. Of 1,562 functions, 182 sit in the debt quadrant (complex, quiet) against only 5 in the fire quadrant (complex, actively changing). The top five by activity-weighted risk are all debt, and all five report zero touches in the last year. That does not make them safe. It means the risk is latent: each one is a function I would want to refactor, or at least cover with characterization tests, before anyone has to change it under deadline pressure.
The five split into two groups. Three are orchestration functions in the core run path (setup_paths, run_hook, run_behave), flagged as god functions with many exit paths. Two are nesting-heavy helpers (get_code_without_docstring, collect_feature_locations), where the problem is depth rather than breadth.
setup_paths — runner.py
This is my top pick, with an activity-weighted risk score of 13.86 and 462 days since last change. From the source excerpt, it resolves where behave should look for features and steps: it handles a supplied path, an ’@’ file list, a path pointing at a feature file, and the default directory. It then walks up the directory tree until it finds a steps directory or environment file, raises a configuration error at the filesystem root, and finally scans for feature files. The excerpt carries a pylint suppression for too-many-branches and too-many-statements.
A complexity of 26 means at least 26 paths that each deserve a test, and a fan-out of 17 means broad coupling to path utilities, config, and the path manager. Python’s runtime dispatch makes that coupling looser than the number suggests. The file carries a 44% bug-fix rate across 9 commits, with no commits linked to bugs and no reverts, which I read as ordinary fix history, not evidence of defects in this function. My recommendation is extract-method along the function’s natural phases: resolve the starting directory, search upward for the base directory, validate that feature files exist, then register paths. Each phase becomes independently testable, and the upward search loop is the easiest piece to lift out first.
run_hook — runner.py
run_hook executes user-defined hooks such as before_all and after_all, and has gone 441 days without a change. The excerpt shows several responsibilities in one body: a shortcut when the hook is not needed, a deprecation warning for a legacy context argument, two nested helper functions that select the current statement, and separate handling for keyboard interrupts and general exceptions. Whether the exception branches abort the run depends on the hook name, which is easy to break when editing nearby code.
It shares setup_paths’s file-level history (44% bug-fix rate, 9 commits, no reverts), so I would treat it as maintainability debt that happens to sit in a sensitive spot, since hook failures decide whether a run continues. I would split error handling out of the execution path: one function that runs the hook, one that records a failure against the right statement. That would collapse the duplicated failure bookkeeping across the two exception branches.
run_behave — main.py
This has the highest cyclomatic complexity of the five, at 29, with shallow nesting of 2 and 441 days since last change. The excerpt shows why: a long run of early-return checks for version, tag help, language help, formatter help, and runner help, followed by configuration validation and a try block with a long chain of except clauses, one per error type. The complexity comes from breadth, not depth, and because the function is exit-heavy, every early return is another path to cover in tests.
The file has a 29% bug-fix rate across 7 commits, no reverts, and no distinct authors in the last 90 days. Of the five, this is the easiest to improve safely. I would move the informational early exits (‘—version’ and the various help options) into a dispatch table or a single helper that returns an exit code or None, and collapse the exception chain into a mapping from error types to message prefixes. That would plausibly cut the complexity by half without changing behaviour.
get_code_without_docstring — formatter/steps_code.py
This one has a different shape: lower complexity (16) and fan-out (9), but nesting of 5 and 462 days since last change. From the excerpt, it strips a docstring from a function’s source by scanning line by line with several state flags (inside the docstring, docstring done), handling both triple-quote styles and one-line docstrings, and optionally printing skipped lines. Those flags driving nested conditionals produce the nesting depth of 5, and each flag multiplies the cases a reader must hold in mind.
The file shows a 33% bug-fix rate across only 3 commits, so the history is thin. The blast radius is also small, since this lives in a formatter. I would replace the hand-rolled scanner with something smaller, such as Python’s own parsing tools to locate the docstring span, or at minimum extract the marker detection into a helper with guard-clause returns to flatten the nesting.
collect_feature_locations — runner_util.py
This function turns command-line paths into feature file locations, handling three cases: a directory to walk, an ’@’ list file, and a plain filename or ‘file:line’ location. Its complexity of 10 is moderate, but nesting reaches 5 and fan-out is 16, which together explain the placement. It has gone 462 days unchanged, and I note an overlap: setup_paths also handles the ’@’ prefix and feature-file paths, so the two share assumptions that are easy to let drift apart.
The file has a 29% bug-fix rate across 7 commits, with no reverts. I would split the three input kinds into three small functions, one per case, so the main loop becomes a simple dispatch. That removes most of the nesting and gives each input form its own tests.
What I would do with this list
The two runner.py functions and run_behave share the god-function and exit-heavy signals, and they form the spine of every test run. If I had a single week, I would write characterization tests for setup_paths first, because the filesystem-walking logic is the hardest to reason about by reading, then refactor run_behave, where the gain per hour is highest. The fire quadrant holds only 5 functions, and their complexity is lower than the debt functions above, so they belong on a watch list and not at the top of the refactoring queue.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
god_function | 4 |
long_function | 4 |
exit_heavy | 3 |
stale_complex | 3 |
complex_branching | 2 |
deeply_nested | 2 |
These labels belong to two tiers — Tier 1 (structural): complex_branching, deeply_nested, exit_heavy, long_function, god_function. Tier 2 (relational/temporal): hub_function, cyclic_hub, middle_man, neighbor_risk, stale_complex, churn_magnet, shotgun_target, volatile_god.
See more analyses with these patterns: complex_branching, deeply_nested, exit_heavy, god_function, long_function, stale_complex.
Reproduce This Analysis
git clone https://github.com/behave/behave
cd behave
git checkout 09cce0739c25b4e2358edf440089ec659e7cbe08
hotspots analyze . --mode snapshot --explain-patterns --force
To run the same analysis on your own codebase, run hotspots analyze . --mode snapshot in any local git repo — no configuration required.
I use Hotspots to highlight structural and activity risk — not “bad code.” I treat these findings as a prioritization aid, not a bug predictor. Editorial policy →