At commit 7f4879f, every one of the top five highest-risk functions in streamlit/streamlit sits in the ‘fire’ quadrant — high structural complexity and active commit churn at the same time. Across 5,269 total functions, 449 are rated critical, and the top-ranked function, _websocket_endpoint, carries an activity-weighted risk score of 19.82 with a cyclomatic complexity of 50 and a nesting depth of 8, touched 14 days ago. I would start there precisely because it is not a cleanup item for next quarter — it is a live surface where a misread branch today could break session establishment for every connected client.
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 |
|---|---|---|---|---|---|
_websocket_endpoint | lib/streamlit/web/server/starlette/starlette_websocket.py | 19.8 | 50 | 8 | 28 |
_run_script | lib/streamlit/runtime/scriptrunner/script_runner.py | 18.9 | 43 | 6 | 51 |
formatNumber | frontend/lib/src/util/formatNumber.ts | 18.5 | 18 | 9 | 12 |
parse_tree_from_messages | lib/streamlit/testing/v1/element_tree.py | 17.8 | 136 | 4 | 58 |
_bidi_component | lib/streamlit/components/v2/bidi_component/main.py | 17.5 | 56 | 4 | 40 |
Quadrant and pattern overview
5,269 functions analyzed
Every function in this codebase lands in either ‘fire’ or ‘watch’ — there is no dormant-debt quadrant and no truly quiet quadrant. That means structural complexity is not sitting in cold storage waiting to be discovered; it is co-located with active development across the board. The 1,379 ‘fire’ functions represent code that is both hard to reason about and actively changing right now.
High cyclomatic complexity — many independent execution paths, each a potential bug surface and required test case.Exit Heavy×5Exit Heavy
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.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 hotspots share the same four antipatterns: complex branching, exit-heavy control flow, god-function scope, and excessive length. Three of the five are also deeply nested. That combination means each function is simultaneously hard to test completely (many exit paths), hard to read (deep nesting), and a blast-radius risk when changed (broad coupling through high fan-out).
_websocket_endpoint — starlette_websocket.py
This async function is the front door for every browser connection to a running streamlit app. From the source excerpt, I can see it handles origin validation, XSRF token checking, signed-cookie parsing for auth, token filtering, session establishment, and the receive loop — all inside a single function body. That explains the cyclomatic complexity of 50: each security check, each cookie-parsing branch, each protocol-enforcement path is an independent execution branch.
The nesting depth of 8 is the number that concerns me most structurally. The excerpt shows nested try blocks inside if is_xsrf_enabled() inside the outer try, with conditional token extraction several levels deeper. At ND 8, a reader has to hold the entire stack of conditions in working memory to understand what state the code is in at any given statement. In Python, that burden is compounded by implicit duck typing — the websocket object’s behavior at each call site depends on the runtime type, not a declared interface, so the coupling implied by the fan-out of 28 is likely even broader than the number suggests.
The file has only one commit in its history with a single author in the last 90 days, and no bug-linked commits or reverts. That means there is no historical defect signal here — but the absence of defect history on a one-commit file also means the function has not yet been stress-tested by repeated change. It was touched 14 days ago, and its exit-heavy structure means any new security requirement (a new token type, a new XSRF mode) will add yet another branch to an already overloaded function.
Recommendation: Extract the XSRF and auth-cookie logic into a dedicated _resolve_user_info(websocket) -> dict coroutine, and extract the receive loop into _run_receive_loop(websocket, session_id). That alone would halve the CC and bring the nesting depth under 5, while making each security concern independently testable.
_run_script — script_runner.py
This is the engine that executes a user’s Streamlit script on every rerun. The source excerpt shows an explicit while True loop (chosen deliberately to avoid recursion stack overflows, per the inline comment), page-change detection, widget-state filtering, query-parameter management across multi-page apps, and media-file reference cleanup — all orchestrated from one function. A fan-out of 51 in Python is significant: 51 distinct callees means a change here can ripple into the pages manager, session state, query params, the media file manager, the sources watcher, and the script run context simultaneously.
It was touched twice in the last 30 days — including a change recorded at days_since_changed: 0, meaning it was modified on the analysis date itself. It is the only function in the top five with review comments on record, which tells me reviewers are already asking questions about this function. With CC 43 and ND 6, every new rerun scenario (fragment reruns, page transitions with stale widget state, query-param scoping) adds another branch to a function that is already testing the limits of what a human can reason about in a single sitting.
The god-function and long-function patterns are evident: the excerpt alone covers fragment detection, page-script-hash resolution, widget-state freezing, and query-string population — and that is only the setup phase before the actual script execution.
Recommendation: I would extract the page-transition logic (widget-state filtering + query-param scoping) into a _prepare_page_transition(ctx, rerun_data) method. That is a self-contained concern with clear inputs and outputs, and isolating it would bring the CC down meaningfully while making the page-transition behavior independently testable — which matters given that multi-page app edge cases appear to be where this function is actively evolving.
formatNumber — formatNumber.ts
Despite being a TypeScript utility rather than Python, this function earns its critical band through nesting depth alone: ND 9 is the deepest of any function in the top five, and it manifests as a cascading if/else if chain — one branch for each supported format string (plain, localized, percent, dollar, euro, yen, and others visible in the excerpt), with each branch containing its own precision-adjustment logic. The excerpt shows the percent branch alone requires two nested notNullOrUndefined checks and arithmetic on maxPrecision to back-calculate the correct decimal count after the formatter multiplies by 100.
With CC 18, there are 18 independent paths through this function, each corresponding to a distinct formatting mode and precision combination. Every path is a potential rendering regression in any dataframe, metric, or column that displays numbers. The exit-heavy pattern means those paths terminate at different points, making exhaustive test coverage non-trivial to verify. The function was touched 14 days ago with one author active in the last 90 days and no prior bug or revert signal — but number-formatting bugs are notoriously hard to catch without locale-specific test cases.
Recommendation: Replace the if/else if format-dispatch chain with a map of format keys to handler functions, each encapsulating its own precision logic. This converts 18 branching paths into a single lookup plus one handler call, flattening the nesting depth dramatically and making each format mode independently testable and extendable without touching the dispatcher.
parse_tree_from_messages — element_tree.py
This function has the highest raw cyclomatic complexity in the entire top five — CC 136 — by a wide margin. It lives in the testing framework (testing/v1/), and from the source excerpt it is clear why: it is a monolithic message dispatcher that walks a list of ForwardMsg protobuf messages and constructs the in-memory element tree used by Streamlit’s app-testing API. Each element type (alert, dataframe, table, button, checkbox, heading, image, and dozens more visible in the excerpt) is handled by its own elif branch, and some types fork further — alerts branch on format, checkboxes branch on style, headings branch on tag.
At CC 136, there are 136 independent execution paths — one for every element-type and sub-type combination the testing framework must recognize. Every new Streamlit widget added to the public API requires a new branch here, which explains the two touches in 30 days across the file’s three total commits. One in three commits to this file has been a bug fix, which is consistent with what you’d expect from a function that must stay synchronized with the full element surface area of the framework.
The fan-out of 58 reflects the breadth of that coupling: this function directly instantiates node classes for nearly every element type in the framework. In Python, each instantiation is a duck-typed call — the actual constructor behavior is resolved at runtime, so a signature change to any node class can silently break this function if the mismatch isn’t caught by tests.
Recommendation: The long-term fix is a registry pattern: each element type registers its own factory function keyed on the protobuf type string, so parse_tree_from_messages becomes a generic dispatcher that looks up and calls the appropriate factory. In the near term, even splitting the alert, heading, and checkbox sub-dispatch into dedicated helper functions would reduce the CC materially and give each sub-type its own test surface.
_bidi_component — main.py
The v2 in the file path signals that this is a second-generation component API. The function implements the full lifecycle of adding a bidirectional component instance to a running app: it validates the script run context, resolves the component from a runtime registry, handles serialization of data (including pandas DataFrames and NumPy arrays per the docstring), enforces style isolation via shadow DOM, wires up on_*_change callbacks from **kwargs, and manages session state for component results. That scope — registry lookup, data serialization, style sandboxing, event dispatch, state management — in a single method is what produces the CC of 56 and fan-out of 40.
The function was last modified at days_since_changed: 0, meaning it changed on the same day this analysis was run, and it has been touched twice in the last 30 days by two different authors. Two authors with concurrent recent activity on a CC 56 function in Python is a coordination risk: duck typing means the coupling to downstream serializers, the registry, and the session state is implicit, and two engineers making independent assumptions about that coupling can produce conflicts that only surface at runtime.
The components/v2/ path also deserves a note: the v2 designation implies a prior version exists. If v1 is still in use, any behavior change in _bidi_component that diverges from v1 semantics becomes a compatibility surface to manage — adding yet another dimension of branching risk to an already complex function.
Recommendation: The kwargs-to-callback wiring (parsing on_*_change keyword arguments) is a self-contained concern that could be extracted immediately into a _resolve_callbacks(kwargs) -> dict helper. Separately, the data-serialization path (handling DataFrames, arrays, and JSON-serializable types) is another natural extraction target. Either refactoring reduces the fan-out and CC while making the component protocol easier to test in isolation.
Patterns Found
Antipatterns detected across the top functions in this snapshot:
| Pattern | Occurrences |
|---|---|
complex_branching | 5 |
exit_heavy | 5 |
god_function | 5 |
long_function | 5 |
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.
Reproduce This Analysis
git clone https://github.com/streamlit/streamlit
cd streamlit
git checkout 7f4879fd890b41e459c2ca6efbe4b2cf57e2bb78
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 →