The riskiest functions in flake8 are a backlog rather than an emergency. make_decision in src/flake8/style_guide.py and _find_config_file in src/flake8/options/config.py have each gone 1,463 days without a change, and whoever touches them next will inherit cyclomatic complexity of 23 and 24 with no recent commit history to learn from. Across the 199 functions in flake8 at commit cd421bc, 15 are critical and 55 sit in the debt quadrant, while none are in the fire quadrant. I would treat this as a refactoring backlog to schedule before the next feature push in config handling, not as a live regression problem.
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 |
|---|---|---|---|---|---|
parse_files_to_codes_mapping | src/flake8/utils.py | 13.1 | 31 | 3 | 11 |
parse_config | src/flake8/options/config.py | 12.4 | 21 | 4 | 8 |
make_decision | src/flake8/style_guide.py | 12.3 | 23 | 2 | 8 |
build_logical_line_tokens | src/flake8/processor.py | 11.8 | 27 | 4 | 6 |
_find_config_file | src/flake8/options/config.py | 11.7 | 24 | 4 | 7 |
Start with the distribution, because it shapes everything else: nothing in flake8 is both complex and recently changing. The only functions with recent activity are three small ones, each touched once in the last year and last changed 12 days ago.
199 functions analyzed
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Exit Heavy×2Exit Heavy
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.God Function×1God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Stale Complex×1Stale Complex
High structural complexity but untouched for a long time — structural debt that will bite whoever opens it next.
That means the five hotspots below are ranked by structure alone. Each is untouched in the last year, so the case for reviewing them is blast radius when next changed, not live churn. Three of the five live in src/flake8/utils.py or src/flake8/options/config.py, the code that decides what flake8 reads and how it interprets it.
parse_files_to_codes_mapping — utils.py
I would start with parse_files_to_codes_mapping: it has the highest activity-weighted risk score in the repository (13.05), a cyclomatic complexity of 31, and hasn’t been changed in 442 days. It parses the per-file-ignores style setting, a string of the form ‘filenames: codes’, and the excerpt shows a small hand-rolled state machine doing the work: a nested class holding mutable state, two inner helper functions, and a token loop that branches on whether it is currently looking for filenames or codes. The function carries a noqa: C901 marker, so the complexity linter’s C901 warning is suppressed for it.
The analysis tags it as a god function, exit-heavy, and stale complex. The exit-heavy part matters for testing: with CC 31, covering every path means exercising each combination of separator, colon, filename, code, and end-of-input tokens in both states, plus the error branches that raise an execution error. The file-level history is quiet, with 4 total commits, no bug-linked commits, no reverts, and a bug-fix rate of 0%. I read that as maintainability debt, not a defect signal. I would replace the class-as-namespace state holder with a small dedicated parser object whose methods each handle one state. That turns the 31-path function into several smaller functions and makes each token transition testable on its own.
parse_config — options/config.py
parse_config has an activity-weighted risk score of 12.36, a cyclomatic complexity of 21, nesting depth of 4, and hasn’t been changed in 442 days. The excerpt shows it walking each key in the flake8 section of a config file, looking up the registered option, picking a typed reader (integer, boolean, or plain string) based on the option’s type or action, normalizing the value, and validating error codes for the ignore and extend-ignore options. That is several responsibilities crammed into one loop body.
The file shows 4 total commits and a bug-fix rate of 25%, which works out to one bug-fix commit. It’s a small sample and I would not read much into it, but it is one of three functions among the top five with a non-zero bug-fix rate (alongside build_logical_line_tokens and _find_config_file). The nesting depth of 4 comes from the loop, the type dispatch, and the code-validation loop stacked together. I would extract the typed-value reading into one helper and the ignore-code validation into another, which removes the deepest nesting and leaves the loop body as lookup, read, normalize, store.
make_decision — style_guide.py
make_decision has gone 1,463 days without a change, which makes it one of the two strongest archaeology cases in this repository. Its cyclomatic complexity is 23 but its nesting depth is only 2, so this is breadth, not depth: a flat chain of conditions comparing whether a code was selected and whether it was ignored, each returning a decision, ending in a fallback that raises an assertion error labelled ‘unreachable’. When both lists match, the excerpt shows a longest-prefix comparison to break the tie.
The analysis tags it exit-heavy, and that fits: nearly every branch returns. Each return is a test case, and the ‘unreachable’ branch is only unreachable if every combination of explicit and implicit selection and ignoring has been enumerated correctly, which is exactly the kind of claim that goes stale when the surrounding enum types change. The file has 6 total commits, no bug-linked commits, and a bug-fix rate of 0%. Because the logic is a truth table, I would express it as one: a lookup keyed on the pair of selection states, with the longest-prefix tie-break pulled out as its own function. Gaps in the table become visible instead of hiding in a chain of elifs.
build_logical_line_tokens — processor.py
build_logical_line_tokens has a cyclomatic complexity of 27, nesting depth of 4, and hasn’t been changed in 500 days. It assembles a logical line from the token stream, building a comment list, the joined text, and an offset mapping back to physical positions. From the excerpt it has special handling for string tokens and for the middle segments of f-strings and t-strings, including brace-count adjustments, plus whitespace reconstruction based on the previous token’s row and column. It also carries a noqa: C901 marker.
The f-string and t-string handling is where I would be most careful. The excerpt shows a branch guarded by a Python-version coverage pragma, so some paths only execute on newer interpreters, which complicates test coverage in a way the metrics alone don’t show. The file has 10 total commits and a bug-fix rate of 10%, which is one bug-fix commit, with no reverts. That is the largest commit history among the top five, so it is the most-worked of these functions even though it has been quiet for 500 days. I would split it by token concern: one helper that normalizes a token’s text (strings and f-string fragments), one that computes the inter-token spacing, and a thin loop that appends to the logical line and mapping.
_find_config_file — options/config.py
_find_config_file hasn’t been changed in 1,463 days and has a cyclomatic complexity of 24 with nesting depth of 4. It walks up the directory tree from a starting path, trying setup.cfg, tox.ini, and .flake8 at each level, and stops at the filesystem root or the home directory, with exception handling around reading and stat-ing files. The excerpt also shows platform-specific handling for when the home directory can’t be detected.
This is the other function I would flag for blast radius. It decides which configuration every flake8 run uses, so an edit to the termination logic changes results for every user without any change to their own settings. The file has 4 total commits and a bug-fix rate of 25%, again one commit and too small a sample to conclude anything. A reasonable first step is to extract the per-directory candidate check into its own function that returns a path or nothing. That leaves the directory walk as a short loop with a clear stop condition and lets the candidate check be tested without touching the filesystem hierarchy.
What the shape of the list tells me
All five are debt, and each of their files shows 10 or fewer total commits. This is a stable codebase where the complicated parts were written, settled, and left alone. That is a reasonable outcome, but it means the next developer to open one of these functions is working from code nobody has had reason to think hard about lately. I would refactor the config-handling trio (parse_files_to_codes_mapping, parse_config, _find_config_file) together, since they share a subsystem and any change to how options are discovered or parsed will likely touch more than one of them.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
complex_branching | 3 |
exit_heavy | 2 |
god_function | 1 |
stale_complex | 1 |
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, exit_heavy, god_function, stale_complex.
Reproduce This Analysis
git clone https://github.com/PyCQA/flake8
cd flake8
git checkout cd421bc8a0f348c111db3c2f94f8140ea8ae0c30
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 →