Python’s pyclbr module scans Python source files for class and function definitions without importing them, returning structural metadata via static analysis. import pyclbr. Scan module: pyclbr.readmodule(module, path=None) → {name: Class} — top-level classes only; pyclbr.readmodule_ex(module, path=None) → {name: Class|Function} — classes and top-level functions. Class attributes: .name → class name string; .module → module name; .file → source file path; .lineno → definition line number; .super → list of base class names or Class objects (resolved if in same module); .methods → {method_name: lineno} dict of all methods defined in the class body. Function attributes: .name, .module, .file, .lineno. Path argument: pass a list of extra directories to prepend to the module search path — pyclbr.readmodule("mymodule", path=["/project/src"]). The module uses tokenize internally and never executes the source. Claude Code generates class hierarchy explorers, method inventories, documentation extractors, and API surface scanners.
CLAUDE.md for pyclbr
## pyclbr Stack
- Stdlib: import pyclbr
- Scan: classes = pyclbr.readmodule("mymodule")
- all_ = pyclbr.readmodule_ex("mymodule", path=["/src"])
- Class: cls.name cls.file cls.lineno cls.super cls.methods
- Func: fn.name fn.file fn.lineno
- Note: Static analysis only — never imports or executes the module
pyclbr Class and Function Browser Pipeline
# app/pyclbrutil.py — scan, hierarchy, method inventory, diff, API surface
from __future__ import annotations
import importlib.util
import os
import sys
from dataclasses import dataclass, field
from pathlib import Path
import pyclbr
# ─────────────────────────────────────────────────────────────────────────────
# 1. Scan helpers
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class ClassInfo:
name: str
file: str
lineno: int
bases: list[str]
methods: dict[str, int] # method_name → lineno
@property
def method_names(self) -> list[str]:
return sorted(self.methods)
def __str__(self) -> str:
bases_str = f"({', '.join(self.bases)})" if self.bases else ""
return (f"class {self.name}{bases_str} "
f"[{Path(self.file).name}:{self.lineno}] "
f"{len(self.methods)} methods")
@dataclass
class FunctionInfo:
name: str
file: str
lineno: int
def __str__(self) -> str:
return f"def {self.name} [{Path(self.file).name}:{self.lineno}]"
def scan_module(
module_name: str,
extra_path: list[str] | None = None,
) -> tuple[list[ClassInfo], list[FunctionInfo]]:
"""
Scan a module by name, returning (classes, functions).
Uses static analysis — no import/execution.
Example:
classes, funcs = scan_module("collections")
for cls in classes:
print(cls)
"""
path = extra_path or []
raw = pyclbr.readmodule_ex(module_name, path=path)
classes: list[ClassInfo] = []
functions: list[FunctionInfo] = []
for name, obj in raw.items():
if isinstance(obj, pyclbr.Class):
bases = []
for b in obj.super:
if isinstance(b, pyclbr.Class):
bases.append(b.name)
else:
bases.append(str(b))
classes.append(ClassInfo(
name=name,
file=obj.file,
lineno=obj.lineno,
bases=bases,
methods=dict(obj.methods),
))
elif isinstance(obj, pyclbr.Function):
functions.append(FunctionInfo(
name=name,
file=obj.file,
lineno=obj.lineno,
))
classes.sort(key=lambda c: c.lineno)
functions.sort(key=lambda f: f.lineno)
return classes, functions
def scan_file(
file_path: str | Path,
module_name: str | None = None,
) -> tuple[list[ClassInfo], list[FunctionInfo]]:
"""
Scan a .py file by path, returning (classes, functions).
Derives a module name from the file stem if not provided.
Example:
classes, funcs = scan_file("/project/src/mymodule.py")
"""
path = Path(file_path).resolve()
name = module_name or path.stem
directory = str(path.parent)
return scan_module(name, extra_path=[directory])
# ─────────────────────────────────────────────────────────────────────────────
# 2. Class hierarchy
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class HierarchyNode:
name: str
bases: list[str]
children: list["HierarchyNode"] = field(default_factory=list)
def render(self, indent: int = 0) -> str:
prefix = " " * indent
bases_str = f"({', '.join(self.bases)})" if self.bases else ""
lines = [f"{prefix}{self.name}{bases_str}"]
for child in self.children:
lines.append(child.render(indent + 1))
return "\n".join(lines)
def build_hierarchy(
classes: list[ClassInfo],
root_filter: list[str] | None = None,
) -> list[HierarchyNode]:
"""
Build a class inheritance tree from a list of ClassInfo objects.
Returns root nodes (classes with no in-module base).
Example:
classes, _ = scan_module("http.server")
roots = build_hierarchy(classes)
for r in roots:
print(r.render())
"""
by_name: dict[str, HierarchyNode] = {
c.name: HierarchyNode(name=c.name, bases=c.bases)
for c in classes
}
known = set(by_name)
# Wire up parent → child relationships for in-module parents
for node in by_name.values():
for base in node.bases:
if base in by_name:
by_name[base].children.append(node)
# Roots: classes whose bases are all outside this module
roots = [
node for node in by_name.values()
if not any(b in known for b in node.bases)
]
if root_filter:
roots = [r for r in roots if r.name in root_filter]
return sorted(roots, key=lambda n: n.name)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Method inventory
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class MethodEntry:
class_name: str
method: str
lineno: int
def __str__(self) -> str:
return f"{self.class_name}.{self.method} (line {self.lineno})"
def method_inventory(
classes: list[ClassInfo],
pattern: str | None = None,
) -> list[MethodEntry]:
"""
Flatten all class methods into a list, optionally filtered by name substring.
Example:
classes, _ = scan_module("email.message")
for entry in method_inventory(classes, pattern="get"):
print(entry)
"""
entries = []
for cls in classes:
for method, lineno in sorted(cls.methods.items(), key=lambda kv: kv[1]):
if pattern is None or pattern in method:
entries.append(MethodEntry(cls.name, method, lineno))
return entries
def find_class(classes: list[ClassInfo], name: str) -> ClassInfo | None:
"""
Find a ClassInfo by exact name.
Example:
cls = find_class(classes, "HTTPServer")
"""
for c in classes:
if c.name == name:
return c
return None
# ─────────────────────────────────────────────────────────────────────────────
# 4. API surface diff
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class ApiDiff:
added_classes: list[str]
removed_classes: list[str]
added_functions: list[str]
removed_functions: list[str]
changed_methods: dict[str, dict] # class_name → {added, removed}
def summary(self) -> str:
lines = []
if self.added_classes:
lines.append(f" + classes: {', '.join(self.added_classes)}")
if self.removed_classes:
lines.append(f" - classes: {', '.join(self.removed_classes)}")
if self.added_functions:
lines.append(f" + functions: {', '.join(self.added_functions)}")
if self.removed_functions:
lines.append(f" - functions: {', '.join(self.removed_functions)}")
for cls_name, delta in self.changed_methods.items():
if delta.get("added"):
lines.append(f" + {cls_name}: {', '.join(delta['added'])}")
if delta.get("removed"):
lines.append(f" - {cls_name}: {', '.join(delta['removed'])}")
return "\n".join(lines) if lines else " (no changes)"
def diff_api(
old_file: str | Path,
new_file: str | Path,
module_name: str | None = None,
) -> ApiDiff:
"""
Compare the public API surface of two versions of the same module file.
Returns an ApiDiff describing what changed.
Example:
delta = diff_api("mymodule_v1.py", "mymodule_v2.py")
print(delta.summary())
"""
old_classes, old_funcs = scan_file(old_file, module_name)
new_classes, new_funcs = scan_file(new_file, module_name)
old_cls_map = {c.name: c for c in old_classes}
new_cls_map = {c.name: c for c in new_classes}
old_fn_names = {f.name for f in old_funcs}
new_fn_names = {f.name for f in new_funcs}
changed: dict[str, dict] = {}
for name in old_cls_map.keys() & new_cls_map.keys():
old_m = set(old_cls_map[name].methods)
new_m = set(new_cls_map[name].methods)
added = sorted(new_m - old_m)
removed = sorted(old_m - new_m)
if added or removed:
changed[name] = {"added": added, "removed": removed}
return ApiDiff(
added_classes=sorted(set(new_cls_map) - set(old_cls_map)),
removed_classes=sorted(set(old_cls_map) - set(new_cls_map)),
added_functions=sorted(new_fn_names - old_fn_names),
removed_functions=sorted(old_fn_names - new_fn_names),
changed_methods=changed,
)
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import tempfile
print("=== pyclbr demo ===")
# ── scan stdlib module ────────────────────────────────────────────────────
print("\n--- scan_module('email.message') ---")
classes, funcs = scan_module("email.message")
for c in classes:
print(f" {c}")
for f in funcs:
print(f" {f}")
# ── hierarchy ─────────────────────────────────────────────────────────────
print("\n--- build_hierarchy ---")
roots = build_hierarchy(classes)
for r in roots:
print(r.render())
# ── method_inventory (filter 'get') ───────────────────────────────────────
print("\n--- method_inventory(pattern='get') ---")
for entry in method_inventory(classes, pattern="get")[:8]:
print(f" {entry}")
# ── scan_file on a temp file ──────────────────────────────────────────────
print("\n--- scan_file on temp source ---")
src_v1 = '''\
class Animal:
def speak(self): pass
def move(self): pass
class Dog(Animal):
def fetch(self): pass
def helper(): pass
'''
src_v2 = '''\
class Animal:
def speak(self): pass
def move(self): pass
def breathe(self): pass
class Cat(Animal):
def purr(self): pass
def helper(): pass
def new_util(): pass
'''
with tempfile.TemporaryDirectory() as tmp:
v1 = Path(tmp) / "creatures.py"
v2 = Path(tmp) / "creatures_new.py"
v1.write_text(src_v1)
v2.write_text(src_v2)
classes_v1, funcs_v1 = scan_file(v1, "creatures")
print(f" v1 classes: {[c.name for c in classes_v1]}")
print(f" v1 funcs: {[f.name for f in funcs_v1]}")
# ── diff_api ──────────────────────────────────────────────────────────
print("\n--- diff_api(v1, v2) ---")
delta = diff_api(v1, v2, "creatures")
print(delta.summary())
# ── find_class ────────────────────────────────────────────────────────
print("\n--- find_class ---")
animal = find_class(classes_v1, "Animal")
if animal:
print(f" {animal.name}: methods={animal.method_names}")
print("\n=== done ===")
For the ast alternative — ast.parse(source) builds a full AST for the entire module, allowing inspection of every expression, decorator, default value, and nested class, while ast.NodeVisitor / ast.NodeTransformer traverse or rewrite the tree — use ast when you need to analyze or transform code structure beyond class/function definitions (e.g., extract all decorator usage, count return statements, or rewrite expressions); use pyclbr for quick, lightweight class-and-method metadata without building a full AST or dealing with visitor boilerplate. For the inspect alternative — inspect.getmembers(module, inspect.isclass), inspect.getsourcelines(cls), and inspect.getmro(cls) provide live runtime introspection including inherited attributes, MRO, and actual source lines — use inspect when the module is already imported and you need live runtime data (docstrings, signatures, MRO); use pyclbr for static analysis of modules you cannot safely import (untrusted code, heavy dependencies, compile-time errors) or when you want to scan source files without executing them. The Claude Skills 360 bundle includes pyclbr skill sets covering ClassInfo/FunctionInfo with scan_module()/scan_file() static scanners, HierarchyNode with build_hierarchy() inheritance tree builder, MethodEntry with method_inventory()/find_class() method lookup, and ApiDiff with diff_api() API surface comparator. Start with the free tier to try class browser patterns and pyclbr pipeline code generation.