The riskiest function in paramiko is run in paramiko/transport.py, and it hasn’t been changed in 432 days. That makes it a maintenance concern rather than an emergency: cyclomatic complexity of 45, nesting depth 6, fan-out of 56, and an activity-weighted risk score of 18.02. Whoever modifies it next inherits all of that with no recent change history to learn from. Across 882 functions, 55 are critical, and none of them are in the ‘fire’ quadrant. 174 functions are structural debt, and all five I analyze below are in that group.
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 |
|---|---|---|---|---|---|
run | paramiko/transport.py | 18.0 | 45 | 6 | 56 |
connect | paramiko/client.py | 16.0 | 28 | 5 | 28 |
_auth | paramiko/client.py | 15.7 | 31 | 5 | 18 |
_process | paramiko/sftp_server.py | 14.9 | 35 | 3 | 36 |
_parse_userauth_request | paramiko/auth_handler.py | 13.9 | 20 | 3 | 28 |
882 functions analyzed
The distribution is lopsided in an informative way. Zero functions are both complex and actively changing, and 174 are complex and quiet. That is what I would expect from a mature SSH library: the protocol machinery was written, hardened, and left alone. The risk is not that something is churning today. It is that the people who understood these functions best may not be the ones who touch them next.
Multiple return or throw paths dispersed through the body — each exit needs separate test coverage.God Function×5God Function
Calls an unusually large number of distinct functions (high fan-out), making it the structural centre of gravity for a subsystem.Long Function×5Long Function
Function body is too long to review in a single pass; likely contains multiple distinct responsibilities.Stale Complex×5Stale Complex
High structural complexity but untouched for a long time — structural debt that will bite whoever opens it next.Complex Branching×3Complex Branching
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Deeply Nested×3Deeply Nested
Control structures nested 4+ levels deep, making it hard to reason about the full execution state at inner branches.
All five top functions are flagged as exit-heavy, god functions, long functions, and stale-complex. Three also carry flags for complex branching and deep nesting. To me that points to one consistent shape: large dispatch-style functions that handle many protocol cases in a single body.
run — transport.py
I would start with run. It has the highest activity-weighted risk score in the repository (18.02) and the widest coupling of the five, a fan-out of 56. It hasn’t been changed in 432 days, with 0 touches in the last year, so this is structural debt with a high blast radius when next changed, not live churn.
The excerpt shows the Transport thread’s main loop. It writes the local version string, checks the remote banner, starts the handshake, sends key-exchange init, and then loops reading messages. Inside the loop it branches on message type. Ignore, disconnect, and debug messages are handled inline. An expected-packet check can raise an exception (a different exception class when strict key exchange was agreed). Key-exchange message IDs are routed to the kex engine, and everything else goes through a handler table or a channel handler table. That is a lot of responsibility for one function, and the metrics (CC 45, ND 6) match what I see.
The file-level history adds context. Of 27 commits to this file, 3 were linked to bug fixes, a bug-fix rate of 22%, with no reverts. Only 1 distinct author touched it in the last 90 days. I would not read that as ‘this function has bugs’. I read it as a file that has taken some corrective work and now sits with a single recent contributor.
My recommendation: extract the handshake preamble (version exchange, banner check, handshake start, kex init) into its own method, and extract the message-type dispatch inside the loop. The loop body then reads as ‘read message, route message’, and each route can be tested without standing up a full thread. Given the fan-out of 56, I would write characterization tests around the dispatch ordering before moving anything, because the exit paths here are numerous.
connect — client.py
connect is the main entry point for SSHClient users, and its signature alone shows why it scores so high: eighteen parameters covering timeouts, key sources, agent use, compression, a pre-opened socket, a transport factory, and an auth strategy. Cyclomatic complexity is 28, nesting depth 5, fan-out 28. It hasn’t been changed in 223 days and has 0 touches in the last year.
The file-level history for client.py shows 8 commits and a bug-fix rate of 25%. No reverts, and no distinct authors in the last 90 days. With a pull-request review comment density of 3.7, that is a modest signal. I think maintainability debt, not defect risk, is the accurate framing.
My recommendation: introduce a parameter object (or reuse the auth strategy concept already in the signature) so that timeout settings and key-source settings travel as groups. Then split the body into ‘establish transport’ and ‘authenticate’ steps. That alone should bring complexity well under 20 and make the public method readable.
_auth — client.py
_auth is more complex than connect (31 versus 28) but less coupled (fan-out 18), and it shares the same 223 days since last change. The excerpt shows credentials tried in a fixed order: an explicit key, then key files (looping over RSA, ECDSA, and Ed25519 key classes), then agent keys, and presumably password authentication after that. At each step it checks whether the server’s allowed methods indicate a second factor is needed.
The repetition is what stands out. The same sequence of ‘try publickey auth, compute allowed types, intersect with two-factor types, return if none’ appears for the explicit key, for each key file, and for each agent key. A small helper that attempts one key and reports whether auth completed or a second factor is required would absorb all of it. Replacing three copies of that logic with one call is the most effective way I can see to cut both branch count and nesting.
Because _auth and connect sit in the same file and were last changed on the same day, I would refactor them together and review them in one pull request.
_process — sftp_server.py
_process is the oldest of the five: 1,237 days since last change, 0 touches in the last year. Its nesting depth is only 3, so this one is wide rather than deep. The excerpt shows a long if/elif chain keyed on the SFTP command type (open, close, read, write, remove, rename, mkdir, rmdir, opendir, readdir, and more below the excerpt). Each branch reads its arguments from the message and sends a status or data response, and many repeat a ‘handle not found, send bad-message status, return’ check.
The file has only 3 commits in its history, a bug-fix rate of 33%, with no reverts and no recent authors. A function this old and this lightly reviewed (a pull-request review comment density of 0) stands out in the list for how little change and review activity it shows.
My recommendation: replace the if/elif chain with a dispatch table mapping command constants to small handler methods, and pull the repeated invalid-handle check into a shared lookup helper. Each command becomes independently testable, and the dispatcher shrinks to a lookup. Since it is a protocol dispatcher with a stable command set, this is a low-risk refactor if tests are written first.
_parse_userauth_request — auth_handler.py
This is the server-side handler for incoming authentication requests, and I treat it differently from the rest because it sits on a security boundary. Cyclomatic complexity is 20, nesting depth 3, fan-out 28, unchanged for 223 days. From the excerpt, it rejects requests when not in server mode, ignores them if already authenticated, parses username, service, and method, rejects mid-flight username changes, and then branches by method (none, password, publickey, and others). Within the password branch, a change-request case is always treated as failure.
The history is the strongest of the five. In 8 commits to auth_handler.py, 1 was linked to a bug fix (a bug-fix rate of 12.5%), and the pull-request review comment density is 14.0, the highest of any file in this set. Reviewers have clearly spent time here. The excerpt also contains several TODO comments about public-key algorithm handling, which tells me maintainers already see unfinished questions in this code. I would not read any of that as a defect claim, but it does justify putting review ahead of purely cosmetic cleanup.
My recommendation: split the method-specific branches into separate handlers (one per auth method) and keep the shared preamble (mode check, service check, username consistency) in the parent. Each authentication path becomes auditable on its own, which matters most for the public-key path where the TODOs sit.
What I would do first
For a team inheriting this code, here is the order I would follow. Start with _process: it has the oldest last change, a flat structure that splits cleanly into a dispatch table, and the least risk per hour of effort. Then do connect and _auth together in client.py. Treat _parse_userauth_request as a review-first item because of its role and its review-comment history. Leave run for last, not because it matters least but because it needs the most test scaffolding before it can be safely split.
One contrast worth noting: the only ‘watch’ function in the data is active but structurally simple, the opposite profile of everything above. I mention it as a reminder that recent change and structural risk are separate axes in this repository.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
exit_heavy | 5 |
god_function | 5 |
long_function | 5 |
stale_complex | 5 |
complex_branching | 3 |
deeply_nested | 3 |
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/paramiko/paramiko
cd paramiko
git checkout d87f9c01c158588fe3ecb635ca7c0d7c0c1a26cc
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 →