Skip to content

Python: defs nested inside a function are never extracted — app-factory (create_app) codebases lose their entire HTTP layer, and inbound traces silently return zero callers #1548

Closed

Description

@Alexiiiine

Version: 0.10.0 (npm, Windows 11)
Impact: on an app-factory codebase this silently removes the entire HTTP layer from the graph, and — worse — makes trace_path --direction inbound return zero callers for functions that clearly have callers.

Summary

The Python extractor never descends into a function body. Any def or class whose scope chain passes through a function is absent from the graph.

This is not a general "nested declarations" gap: defs inside module-level if / try / for / with blocks are indexed, so block statements are transparent. The boundary is specifically a function body. JS/TS is unaffected — a plain function declared inside another function indexes fine.

That makes the Flask/FastAPI application-factory pattern (def create_app(): ... @app.get(...) def handler(): ...) a total blind spot. It is the idiomatic way to structure both frameworks.

Minimal reproduction

app_py.py

def target():
    return 0


def module_level():
    return target()


if True:
    def inside_if():
        return target()


def factory():
    def nested():
        return target()
    return nested

app_js.js

function targetJs() { return 0; }

function moduleLevelJs() { return targetJs(); }

function factoryJs() {
  function nestedJs() { return targetJs(); }
  return nestedJs;
}
$ index_repository  -> nodes=26 edges=30 skipped=0 parse_partial=0

$ search_graph label=Function
total: 10
results: 10  (rows: name label lines in out; qn = group prefix + "." + name)
repro.app_js (app_js.js):
  factoryJs Function 5-8 0 0
  moduleLevelJs Function 3-3 0 1
  nestedJs Function 6-6 0 1
  targetJs Function 1-1 2 0
repro.app_py (app_py.py):
  factory Function 14-17 0 0
  inside_if Function 10-11 0 1
  module_level Function 5-6 0 1
  target Function 1-2 3 0

nested (Python) is the only declaration missing. nestedJs — the exact same shape in JavaScript — is present.

The more serious half: call edges are lost, not re-attributed

$ trace_path function_name=target direction=inbound
callers_total: 3
repro.app_py:
  inside_if 1
  module_level 1
repro.app_py.py:
  __file__ 1

$ trace_path function_name=targetJs direction=inbound
callers_total: 2
repro.app_js:
  moduleLevelJs 1
  nestedJs 1

nested calls target(), but that edge does not appear anywhere — it is not re-attributed to the enclosing factory, which shows out = 0. The call is dropped entirely.

Consequence: for any function reached only from inside an app factory, trace_path --direction inbound returns an empty or short list with no indication that anything was skipped. For the "what can reach this dangerous function?" use case this is a silent false negative, which is worse than an error.

(Minor, visible above: a synthetic <file>.py.__file__ node is reported as a caller at hop 1. It also shows up at hop 2 on real traces. It looks like file-level fallback residue leaking into caller lists.)

Full rule, measured

A 24-case matrix against 0.10.0:

case in graph
module-level def / async def / decorated def yes
method in a class; method in a class-in-a-class yes
def inside module-level if / try / for / with yes
def inside a def no
decorated def inside a def no
class declared inside a def, and all of its methods no
def inside a method no
JS: function in function, arrow in function, object-literal method, class method yes

Real-world impact

On a production FastAPI codebase (~4,600 Python/TS symbols) where the app is built in a single create_app():

  • that one module: 99 of 376 defs indexed (26%) — 278 handlers invisible
  • 90 of 190 route paths resolved (47%). By verb: POST 60/60, PATCH 16/16, PUT 2/2, DELETE 6/6 — but GET only 21/111, because the read endpoints are the ones defined inside the factory
  • core endpoints such as /api/summary are absent, and their handler functions return 0 hits from search_graph
  • this single pattern accounts for ~38% of the repo-wide 750-symbol extraction gap

Every other module in the same repo is 95–100% covered, so the defect is narrow and concentrated — which is also what makes it easy to miss: aggregate coverage still looks healthy.

Suggested fix

Recurse into function_definition / async bodies when collecting definitions, the same way class_body is already traversed, and parent nested symbols to the enclosing function rather than dropping them. Even behind an opt-in flag this would close the gap.

If full recursion is considered too noisy for ordinary closures, a targeted alternative: descend when the inner def carries a decorator (which is exactly the route-handler case) — though the call-edge loss above would remain for undecorated helpers.

Related

Activity

  1. DeusData commented on Aug 12, 2026

    @DeusData
    Owner

    Thanks @Alexiiiine — clean reproducer, and the app-factory pattern makes this matter more than a niche parse gap: create_app-style factories are the standard Flask idiom, and closures returned from functions are ordinary Python and JavaScript.

    Queued for the next batch. We want to fix it at the extraction level (nested definitions should be first-class nodes with their enclosing scope recorded) rather than by special-casing one shape, so it gets a proper reproduce-first pass rather than a quick patch at the end of today's release.

  2. Alexiiiine commented on Aug 12, 2026

    @Alexiiiine
    Author

    My agent wrote this. I'm actually appalled it submitted this issue without my knowledge. Hope it helps though. This project is very promising. I have been testing it out on one of my bigger and messier code bases. Thank you for creating and maintaining this project. @DeusData

  3. added
    bugSomething isn't working
    priority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.
    on Aug 12, 2026
  4. DeusData commented on Aug 12, 2026

    @DeusData
    Owner

    Thank you for clarifying, @Alexiiiine. The public reproduction is technically useful, and no further action is expected from you. We will keep this as a product extraction bug based on the code example and will not infer anything beyond the technical report you have chosen to leave here.

  5. added a commit that references this issue on Oct 18, 2026
  6. DeusData commented on Oct 18, 2026

    @DeusData
    Owner

    Fixed on main by #2686 (merge 1346ac4): Python definitions nested inside functions and classes are now extracted as their own nodes, with their own calls, decorators, inheritance and handlers. Across 26 open-source Python repositories that adds 53,415 nested definitions, and calls made inside them are now attributed to the nested definition instead of the file. Test coverage edges from helpers nested in test functions are kept. Thank you very much for reporting this, it was one of the larger gaps in the Python graph.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingparsing/qualityGraph extraction bugs, false positives, missing edgespriority/highNeeds near-term maintainer attention; high-impact bug, regression, safety issue, or release blocker.windowsWindows-specific issues

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions