The Python Analyzer is the language-specific static analysis engine responsible for extracting structural components (classes, functions, methods) and call/dependency relationships from Python source files. It is one of several per-language analyzers plugged into the Dependency_Analyzer_Core pipeline (alongside the C-Family Tree-sitter Analyzers, JavaScript/TypeScript Analyzers, and PHP Analyzer), and its output feeds the repository-wide dependency graph that ultimately powers documentation generation in Backend_LLM_&_Documentation_Services.
Unlike the other language analyzers, which are built on tree-sitter grammars, the Python analyzer is implemented directly on top of Python's native ast module (ast.NodeVisitor), giving it precise, first-party access to the language's semantics (scopes, imports, class hierarchies, etc.) without an external parsing dependency.
Its single entry point, analyze_python_file, is called once per .py/.pyx file discovered during repository analysis and returns:
- A list of extracted
Nodecomponents (classes, functions, methods). - A list of
CallRelationshipedges (calls, inheritance, imports) between components. - The set of third-party (non-project) import roots observed in the file, used elsewhere to build a picture of external dependencies.
| Responsibility | Description |
|---|---|
| Component extraction | Walks the AST to identify top-level classes, top-level functions, and methods, producing a Node for each with metadata (docstring, parameters, source snippet, line ranges, qualified name). |
| Scope tracking | Maintains a scope stack (class/function) to distinguish top-level definitions from nested/local ones, so that functions nested inside functions are treated as implementation detail rather than independent components. |
| Import resolution | Tracks import and from ... import statements, mapping local aliases to canonical dotted module paths, and classifies each import root as project-internal or external/third-party. |
| Call resolution | Analyzes ast.Call nodes to determine, wherever possible, which component a call target refers to — resolving self/cls/super() calls, calls on typed variables, calls on known same-file classes, and calls through imported names. |
| Inheritance resolution | Extracts base classes for each class and attempts to resolve inherited methods (including across super() chains) so that overridden/inherited method calls still map to the correct defining class when possible. |
| Shallow type inference | Performs a light "receiver knowledge" pass on simple assignments (x = SomeClass(), x = module.factory()) so that later attribute calls on x can be attributed to the right class or import origin. |
flowchart TD
subgraph Dependency_Analysis_Service
RA[RepoAnalyzer] --> AS[AnalysisService]
AS --> CGA[CallGraphAnalyzer]
end
CGA -->|dispatches by file extension| PY[PythonASTAnalyzer<br/>.py / .pyx]
CGA --> TSJ[TreeSitterJSAnalyzer]
CGA --> TSTS[TreeSitterTSAnalyzer]
CGA --> TSPHP[TreeSitterPHPAnalyzer]
CGA --> TSC["C / C++ / C# / Java / Kotlin Analyzers"]
PY -->|Nodes + CallRelationships + external roots| CGA
CGA --> DPB[DependencyGraphBuilder]
DPB --> AR[AnalysisResult]
AR --> DGB[DependencyParser]
DGB -->|Dict of Node components| DOC[DocumentationGenerator]
click PY "Python_Analyzer.md"
See Dependency_Analysis_Service for the orchestration layer (AnalysisService, CallGraphAnalyzer, RepoAnalyzer) that discovers files, dispatches them to the correct language analyzer, and aggregates results. See Dependency_Analyzer_Core for the DependencyParser, DependencyGraphBuilder, and shared models (Node, CallRelationship, AnalysisResult) that this module produces and consumes.
classDiagram
class PythonASTAnalyzer {
-file_path: str
-repo_path: str
-content: str
-project_modules: Set~str~
-nodes: List~Node~
-call_relationships: List~CallRelationship~
-scope_stack: List~Tuple~
-component_stack: List~str~
-top_level_nodes: Dict~str, Node~
-class_methods: Dict~str, Set~str~~
-class_bases: Dict~str, List~str~~
-module_imports: Dict~str, str~
-from_imports: Dict~str, str~
-external_import_roots: Set~str~
-var_types: Dict~str, str~
-var_origins: Dict~str, str~
-local_function_names: List~Set~str~~
+analyze()
+visit_Import(node)
+visit_ImportFrom(node)
+visit_ClassDef(node)
+visit_FunctionDef(node)
+visit_AsyncFunctionDef(node)
+visit_Assign(node)
+visit_AnnAssign(node)
+visit_Call(node)
-_classify_call(func)
-_resolve_name_reference(name)
-_resolve_method_on_class(class_dotted, method)
-_resolve_method_via_bases(class_dotted, method)
-_attribute_chain(node)
}
class Node {
+id: str
+name: str
+component_type: str
+file_path: str
+relative_path: str
+depends_on: Set~str~
+source_code: str
+qualified_name: str
+get_display_name()
}
class CallRelationship {
+caller: str
+callee: str
+call_line: int
+is_resolved: bool
}
PythonASTAnalyzer --> Node : produces
PythonASTAnalyzer --> CallRelationship : produces
PythonASTAnalyzer ..> ExternalSymbols : uses
class ExternalSymbols {
PYTHON_OBJECT_METHODS
PYTHON_STDLIB_MODULES
}
Node and CallRelationship are shared Pydantic models defined in Dependency_Analyzer_Core (codewiki/src/be/dependency_analyzer/models/core.py) and used identically by every language analyzer, which keeps downstream consumers (graph builder, documentation generator) language-agnostic.
The analyzer distinguishes three kinds of definitions and only promotes two of them to first-class components:
flowchart TD
A[ast.parse source] --> B{Definition type}
B -->|Top-level class| C[Extract class Node<br/>component_type=class]
B -->|Top-level function| D[Extract function Node<br/>component_type=function]
B -->|Method inside a top-level class| E[Extract method Node<br/>component_type=method]
B -->|Class/function nested inside a function| F[Traverse for calls only<br/>NOT extracted as a component]
C --> G[Registered in top_level_nodes]
D --> G
E --> H[Registered in class_methods for the enclosing class]
Key rules:
- Component ID format:
"{relative_path}::{dotted_name}", e.g.services/user.py::UserService.create. - Qualified name format: dotted-module style, e.g.
services.user.UserService.create, used for cross-file/global resolution by the graph builder. - Functions whose bare name starts with
_test_are filtered out (_should_include_function), avoiding noise from ad-hoc test helpers. - Nested functions/classes (defined inside another function) are not extracted as separate nodes — calls made from within them are attributed to the enclosing extracted component via
component_stack, and their names are recorded inlocal_function_namesto avoid misclassifying self-recursive/local calls as external references.
sequenceDiagram
participant AST as ast.Import / ast.ImportFrom
participant PA as PythonASTAnalyzer
participant PM as project_modules (Set[str])
participant EXT as external_import_roots
AST->>PA: visit_Import / visit_ImportFrom
PA->>PA: bind alias -> canonical dotted target
PA->>PA: _note_import_root(target)
PA->>PM: is_project_import(target, project_modules)?
alt root is stdlib
PA-->>PA: ignore (in PYTHON_STDLIB_MODULES)
else matches a project module (dotted-boundary match)
PA-->>PA: treated as internal, not added to external roots
else
PA->>EXT: add root to external_import_roots
end
project_modulesis a set of dotted module paths for every Python file in the repository, supplied by the caller (analyze_python_file) so the analyzer can tell project-internal imports apart from third-party packages even in src-layout repos (e.g. importpkg.utilmatching project modulesrc.pkg.util) via_dotted_contains.PYTHON_STDLIB_MODULESandPYTHON_OBJECT_METHODScome fromcodewiki/src/be/dependency_analyzer/utils/external_symbols.pyand are used to filter out standard-library imports and built-in object methods (e.g.__init__,append) that should never resolve to project components.- The returned
external_import_rootsset is aggregated across files by the Dependency_Analysis_Service to characterize the repository's third-party footprint.
Call resolution is the most intricate part of the analyzer. visit_Call delegates to _classify_call, which decides how confidently a call target can be tied to a project component.
flowchart TD
Start[ast.Call node.func] --> IsName{Is ast.Name?}
IsName -->|Yes| ResolveName[_resolve_name_reference]
IsName -->|No, is ast.Attribute| Chain[_attribute_chain]
Chain --> Composite{Chain resolvable<br/>to simple name path?}
Composite -->|No - composite receiver| ObjMethod{attr in<br/>PYTHON_OBJECT_METHODS?}
ObjMethod -->|Yes| Drop[No relationship emitted]
ObjMethod -->|No| Unresolved1["(attr, False) unresolved"]
Composite -->|Yes| Root{Root of chain}
Root -->|self / cls + 1 attr| SelfMethod[_resolve_method_on_class<br/>on current class]
Root -->|super() + 1 attr| SuperMethod[_resolve_method_via_bases]
Root -->|typed variable var_types| VarMethod[_resolve_method_on_class<br/>on inferred class]
Root -->|known same-file class| ClassMethod[_resolve_method_on_class]
Root -->|from_imports / module_imports| ImportedDotted["(imported.dotted.path, False)"]
Root -->|var_origins tracked| OriginDotted["(origin.dotted.path, False)"]
Root -->|none of the above| RawDotted["(full dotted chain, False)"]
ResolveName --> NameLocal{Shadowed by<br/>local_function_names?}
NameLocal -->|Yes| Drop
NameLocal -->|No| NameLookup{In top_level_nodes?}
NameLookup -->|Yes| Resolved["(file::name, True) RESOLVED"]
NameLookup -->|No, in imports| ImportedName["(imported.dotted.path, False)"]
NameLookup -->|No| RawName["(name, False)"]
is_resolved=True— the callee is a definitively known component in the same file (top-level function/class,self/clsmethod match, or inherited method found via same-file base classes). The callee id is a full component id:"{relative_path}::{dotted_name}".is_resolved=False— the callee is expressed as a best-effort dotted/qualified name (e.g.module.Class.methodor a bare unqualified name). These are resolved later — at the repository level — by matching againstqualified_nameacross all files, which is why everyNodealso carries aqualified_name.
_resolve_method_via_bases performs a breadth-first search over class_bases (populated during visit_ClassDef) to find which ancestor class actually defines a method being called via self.method(), cls.method(), or super().method(). If a base class is itself defined in the same file, the search continues transitively; if a base is external/imported, the analyzer emits an unresolved dotted callee (imported.Base.method) so the global resolver has a chance to match it against another file's qualified name.
To resolve calls like service.do_work() where service is a local variable, the analyzer performs a lightweight, single-assignment type inference:
flowchart LR
A["x = SomeClass()"] -->|root is a same-file class,<br/>no further attrs| B[var_types[x] = SomeClass]
C["x = module.factory()"] -->|root is an import| D[var_origins[x] = module.factory]
B --> E["x.method() resolves via<br/>_resolve_method_on_class(SomeClass, method)"]
D --> F["x.method() resolves to<br/>module.factory.method (unresolved)"]
This tracking is intentionally shallow: it only considers single-target Name = Call(...) assignments (including annotated assignments), and only the most recent assignment to a name is remembered (no flow-sensitive reassignment tracking, no branch merging).
analyze_python_file(file_path, content, repo_path=None, project_modules=None) -> Tuple[List[Node], List[CallRelationship], Set[str]]
The single function other modules should call. It instantiates PythonASTAnalyzer, runs .analyze(), and returns the accumulated nodes, relationships, and external import roots. This is invoked by the CallGraphAnalyzer in Dependency_Analysis_Service as part of the multi-language repository scan orchestrated by AnalysisService and RepoAnalyzer.
sequenceDiagram
participant CGA as CallGraphAnalyzer
participant Fn as analyze_python_file
participant PA as PythonASTAnalyzer
CGA->>Fn: analyze_python_file(path, content, repo_path, project_modules)
Fn->>PA: new PythonASTAnalyzer(...)
Fn->>PA: analyzer.analyze()
PA->>PA: ast.parse(content) (SyntaxWarning suppressed)
PA->>PA: self.visit(tree) (NodeVisitor traversal)
Fn-->>CGA: (nodes, call_relationships, external_import_roots)
analyze() guards parsing with warnings.catch_warnings() to suppress SyntaxWarnings from regex-like escape sequences in analyzed source, and catches SyntaxError/generic exceptions so a single unparsable file does not abort the whole repository scan (logged and skipped instead).
The (nodes, call_relationships, external_import_roots) tuple produced per file is merged by the CallGraphAnalyzer with the results of all other language analyzers into a single repository-wide call graph. This flows into:
DependencyGraphBuilder/DependencyParser(see Dependency_Analyzer_Core) — builds the finaldepends_onedges on eachNodeby matching unresolvedCallRelationship.calleedotted names against every node'squalified_name, and resolved callees directly by component id.AnalysisResult— the top-level result object consumed by the Backend_LLM_&_Documentation_Services module (DocumentationGenerator,PydanticAIBackend, etc.) to generate per-module and per-component documentation, including dependency-aware context for LLM prompts.
flowchart LR
PY[PythonASTAnalyzer] --> CGA[CallGraphAnalyzer]
CGA --> DGB[DependencyGraphBuilder]
DGB --> AR[AnalysisResult]
AR --> DP[DependencyParser]
DP --> DG[DocumentationGenerator]
DG --> DOCS[Generated Markdown Docs]
| Module | Relationship |
|---|---|
| Dependency_Analysis_Service | Orchestrates repository scanning and dispatches files to PythonASTAnalyzer (and sibling analyzers). |
| Dependency_Analyzer_Core | Defines the shared Node/CallRelationship/AnalysisResult models this analyzer emits, and builds the final dependency graph. |
| C-Family_Tree-sitter_Analyzers, JavaScript_TypeScript_Analyzers, PHP_Analyzer | Sibling language analyzers producing the same Node/CallRelationship contract using tree-sitter grammars instead of Python's native ast. |
| Backend_LLM_&_Documentation_Services | Consumes the aggregated dependency graph to generate documentation for each analyzed component. |