Across pylint’s 6,026 analyzed functions, 52 sit in the ‘fire’ quadrant — structurally complex code that is also being actively changed this month. The top hotspot, visit_assignname in pylint/checkers/base/name_checker/checker.py, carries an activity-weighted risk of 17.83 with 83 cyclomatic complexity and a commit in the last 13 days, making it a live regression risk rather than a backlog item. I’d start any review pass this week with the four fire/debt functions in the critical band before touching anything in the 2,995-function low-risk pile.
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 |
|---|---|---|---|---|---|
visit_assignname | pylint/checkers/base/name_checker/checker.py | 17.8 | 83 | 5 | 30 |
safe_infer | pylint/checkers/utils.py | 17.6 | 31 | 3 | 9 |
_check_consumer | pylint/checkers/variables.py | 17.0 | 63 | 5 | 25 |
_check_in_slots | pylint/checkers/classes/class_checker.py | 16.8 | 34 | 5 | 19 |
visit_with | pylint/checkers/typecheck.py | 16.7 | 46 | 6 | 11 |
6,026 functions analyzed
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.God Function×8God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Complex Branching×7Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Deeply Nested×7Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.Long Function×6Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.Hub Function×1Hub Function
Many other functions call this one — a change here ripples widely through callers.
The pattern mix across the top hotspots is consistent: nine instances of exit-heavy control flow, eight god-functions, seven with complex branching, seven deeply nested, six long functions. That’s not five isolated problems — it’s one recurring shape. Checker visit methods in pylint accumulate a long chain of isinstance checks and early returns as new AST node types and Python language features get supported. That’s domain-inherent for a static analysis tool, but it also means test coverage has to keep pace with every added branch, and right now the branch count in several of these functions has run well ahead of what a reviewer can hold in their head.
visit_assignname — pylint/checkers/base/name_checker/checker.py
This is the top-ranked hotspot in the repo, and it sits in the ‘fire’ quadrant: one commit in the last 13 days against a function that already carries 83 cyclomatic complexity, 5 levels of nesting, and fan-out to 30 distinct functions. The source excerpt shows why — a long isinstance dispatch chain checking assign_type against Comprehension, TypeVar, ParamSpec, TypeVarTuple, TypeAlias, and then a nested branch for module-scope Assign/AnnAssign handling with tuple-unpacking logic inside. The pylint pragma comment disabling too-many-branches and too-many-statements on the function signature itself is a tell — the maintainers already know this function is over the line, they’ve just suppressed the linter’s own warning rather than split it. The file-level history shows half of the last 4 commits here were bug-fix motivated, and reviewers are already spending real time on this code, judging by comment volume on recent pull requests. My recommendation: extract the module-scope AnnAssign/Assign branch (the TypeVar/TypeAlias tuple-unpacking block) into its own helper before the next feature lands here — that alone would cut several nesting levels and isolate the tuple-index logic that is currently hardest to test.
safe_infer — pylint/checkers/utils.py
This one is ‘debt’, not ‘fire’ — zero touches in the last 30 days, last changed 36 days ago. But it’s tagged as a hub_function, the only one in this dataset, and that label matters more than the complexity number alone. Looking at the excerpt, safe_infer sits at the center of pylint’s inference-ambiguity handling: it wraps node.infer(), walks the resulting generator, and returns None on any of several ambiguity conditions (multiple inferred types, mismatched constants, mismatched constructors). Other hotspots in this same file and adjacent checkers — visit_with and _check_in_slots below — call into it directly. A change to its ambiguity rules has a wide blast radius precisely because so much of the checker suite depends on its return contract. Two-thirds of this file’s last 6 commits were bug fixes, spread across 5 distinct authors in the last 90 days, which suggests this function’s edge cases have already drawn repeated fixes from different contributors. Since nothing is touching it right now, this is the moment to add characterization tests for its exception paths (InferenceError, StopIteration, the bare Exception re-raise) before the next author has to reason about it under time pressure.
_check_consumer — pylint/checkers/variables.py
Another ‘debt’ entry: 63 cyclomatic complexity, 5 levels of nesting, fan-out of 25, and 36 days since it was last touched. The excerpt shows a long sequence of conditional returns tied to a VariableVisitConsumerAction enum — consumed-name checks, late-binding closure checks, a special case for self-referential class references inside lambdas with an inline comment walking through both a valid and an invalid example. That comment block is doing the work a smaller function would do implicitly; the fact that it takes six lines of prose to explain one conditional is itself a signal the branch is doing too much. Only one commit total, from a single author in the last 90 days, tells me this hasn’t drawn much recent collaborative attention, which is exactly the setup for high blast radius when someone eventually needs to touch it under a deadline. If you own the variables checker, this is worth a dedicated pass to split the recursive-class-in-lambda check out from the main consumption logic before the next name-resolution bug report forces a rushed edit.
_check_in_slots — pylint/checkers/classes/class_checker.py
Back in the ‘fire’ quadrant — 2 touches in the last 30 days, most recently 7 days ago, against 34 cyclomatic complexity and fan-out of 19. The excerpt walks a chain of early-return guard clauses (not an Instance, unknown bases, no __slots__ defined, __setattr__ present on any base, Generic in the MRO) before reaching the actual slot-membership check, which itself branches again for __dict__ presence, property descriptors, and __class__ reassignment. That’s a lot of exit points feeding into a single add_message call, and each guard is a distinct test case an engineer needs to hit to be confident a change here doesn’t silently suppress or over-fire the assigning-non-slot message. The recent pull request comment volume on this file matches the pattern seen in visit_assignname — reviewers are already spending effort here. Given it’s actively changing, I’d prioritize a decompose-conditional pass on the guard-clause block specifically, since that’s the part most likely to gain another exception the next time a language feature interacts with __slots__.
visit_with — pylint/checkers/typecheck.py
This is the deepest nesting in the top five — 6 levels — paired with 46 cyclomatic complexity, and it’s ‘debt’: zero touches in 30 days, 36 days dormant. The excerpt shows a match-statement dispatch over the inferred context-manager type (Generator, AsyncGenerator, then a fallback case), with a nested for-loop walking the inference context’s path history to determine whether a decorator makes the not-context-manager message a false positive. That inner loop-with-break-and-else is the kind of control flow that’s easy to get subtly wrong when someone adds a new decorator-matching case later, and at nesting depth 6 it’s hard to verify by inspection which of the outer match arms a given change actually falls under. only a fifth of this file’s recent commits were bug fixes, lower than the other hotspots here, so I’d frame this as maintainability debt rather than an active defect signal — but the fan-out of 11 combined with the nesting depth means any refactor needs care about which callers rely on early continues versus the fallthrough case. First step: extract the inference-context path walk into its own named helper so the match-statement body reads as a flat dispatch rather than a nested search.
Worth noting in passing: visit_attribute and visit_binop in the context data sit right below this top five at activity-weighted risk 16.51 and 16.48, both ‘debt’ with 36 days dormant and the same god_function/deeply_nested signature as visit_with — the typecheck and strings checkers together are where a lot of the repo’s structural debt has accumulated, even though none of it is under active edit right now.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
exit_heavy | 9 |
god_function | 8 |
complex_branching | 7 |
deeply_nested | 7 |
long_function | 6 |
hub_function | 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, deeply_nested, exit_heavy, god_function, hub_function, long_function.
Reproduce This Analysis
git clone https://github.com/PyCQA/pylint
cd pylint
git checkout b99a594da39806f0af75981958d2521274e699f8
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 →