Python’s linecache module reads and caches source code lines by filename and line number — the same mechanism used by traceback, pdb, and tracemalloc internally. import linecache. getline: linecache.getline("app/main.py", 42) → string with trailing \n, or "" if not found; first arg is any filename the import system knows; n=1-indexed. getlines: linecache.getlines("app/main.py") → list of all lines. clearcache: linecache.clearcache() — free all cached content. checkcache: linecache.checkcache("app/main.py") — re-stat the file; checkcache() with no arg checks all cached entries; call after the file changes. updatecache: linecache.updatecache("app/main.py") — force reload. lazycache: linecache.lazycache("mymod", globals_dict) — register lines from __loader__ without reading; used by traceback for frozen/zip-imported modules. linecache.cache — the dict {filename: (size, mtime, lines, fullpath)}. Works transparently for zip-imported modules when the loader provides get_source(). Claude Code generates source annotators, traceback enrichers, inline profiler reporters, and interactive diff viewers.
CLAUDE.md for linecache
## linecache Stack
- Stdlib: import linecache
- Line: linecache.getline("file.py", 42) # " return x + y\n" or ""
- All: linecache.getlines("file.py") # list of all lines
- Refresh: linecache.checkcache("file.py") # re-stat after edit
- Clear: linecache.clearcache() # free all cached data
- Lazy: linecache.lazycache("mod", mod.__dict__) # loader-backed registration
linecache Source Line Pipeline
# app/linecacheutil.py — getline, annotate, context window, diff, traceback enricher
from __future__ import annotations
import linecache
import os
import textwrap
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any
# ─────────────────────────────────────────────────────────────────────────────
# 1. Line reading helpers
# ─────────────────────────────────────────────────────────────────────────────
def get_line(filename: str, lineno: int, strip: bool = False) -> str:
"""
Return a single source line (1-indexed). Empty string if not found.
strip: remove leading/trailing whitespace and trailing newline.
Example:
line = get_line("app/main.py", 42)
print(f"Line 42: {line!r}")
"""
line = linecache.getline(filename, lineno)
return line.strip() if strip else line
def get_lines(filename: str, start: int = 1, end: int | None = None) -> list[str]:
"""
Return a slice of lines from a file [start, end] (1-indexed, inclusive).
Returns all lines if end is None.
Example:
lines = get_lines("app/main.py", start=10, end=20)
"""
all_lines = linecache.getlines(filename)
# Convert to 0-indexed slice
s = max(0, start - 1)
e = end # None → all; end is inclusive so we don't subtract
return all_lines[s:e]
def file_exists_in_cache(filename: str) -> bool:
"""
Return True if the file is already loaded in linecache.
Example:
if not file_exists_in_cache("app/main.py"):
linecache.updatecache("app/main.py")
"""
return filename in linecache.cache
def refresh(filename: str | None = None) -> None:
"""
Invalidate stale cache entries.
Pass a filename to check only that file, or None to check everything.
Example:
# After editing main.py
refresh("app/main.py")
line = get_line("app/main.py", 1)
"""
linecache.checkcache(filename)
def clear() -> None:
"""Free all cached source content."""
linecache.clearcache()
# ─────────────────────────────────────────────────────────────────────────────
# 2. Context window — source lines around a target line
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class SourceContext:
filename: str
target: int # the highlighted line number
lines: list[tuple[int, str, bool]] # (lineno, text, is_target)
def __str__(self) -> str:
parts: list[str] = [f" {self.filename}"]
for lineno, text, is_target in self.lines:
marker = ">>>" if is_target else " "
parts.append(f" {marker} {lineno:4d} {text.rstrip()}")
return "\n".join(parts)
def get_context(
filename: str,
lineno: int,
before: int = 3,
after: int = 3,
) -> SourceContext:
"""
Return a SourceContext with lines surrounding a target line.
Example:
ctx = get_context("app/main.py", 42, before=4, after=4)
print(ctx)
"""
all_lines = linecache.getlines(filename)
total = len(all_lines)
start = max(1, lineno - before)
end = min(total, lineno + after)
lines_out: list[tuple[int, str, bool]] = []
for n in range(start, end + 1):
text = all_lines[n - 1] if n <= total else ""
lines_out.append((n, text, n == lineno))
return SourceContext(filename=filename, target=lineno, lines=lines_out)
# ─────────────────────────────────────────────────────────────────────────────
# 3. Source annotator — attach source lines to tracebacks / allocation sites
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class AnnotatedFrame:
filename: str
lineno: int
name: str # function / scope name
source: str # stripped source line (or "")
def __str__(self) -> str:
src = f" # {self.source}" if self.source else ""
return f" File {self.filename!r}, line {self.lineno}, in {self.name}{src}"
def annotate_traceback(tb_frames: list[tuple[str, int, str]]) -> list[AnnotatedFrame]:
"""
Attach source lines to a list of (filename, lineno, name) traceback tuples.
Example:
import traceback, sys
try:
1/0
except ZeroDivisionError:
frames = [(f.filename, f.lineno, f.name)
for f in traceback.extract_tb(sys.exc_info()[2])]
for af in annotate_traceback(frames):
print(af)
"""
result: list[AnnotatedFrame] = []
for filename, lineno, name in tb_frames:
src = linecache.getline(filename, lineno).strip()
result.append(AnnotatedFrame(filename=filename, lineno=lineno, name=name, source=src))
return result
def annotate_sites(
sites: list[tuple[str, int]],
) -> list[tuple[str, int, str]]:
"""
Bulk-annotate (filename, lineno) pairs with their source lines.
Returns [(filename, lineno, source_line), ...].
Example:
# From tracemalloc top_allocations
pairs = [(s.file, s.line) for s in allocation_sites]
for filename, lineno, src in annotate_sites(pairs):
print(f" {filename}:{lineno} {src.strip()!r}")
"""
return [
(filename, lineno, linecache.getline(filename, lineno))
for filename, lineno in sites
]
# ─────────────────────────────────────────────────────────────────────────────
# 4. File viewer — render numbered source with optional highlight range
# ─────────────────────────────────────────────────────────────────────────────
def render_source(
filename: str,
highlight: set[int] | None = None,
start: int = 1,
end: int | None = None,
width: int = 80,
) -> str:
"""
Render a source file (or slice) as a numbered listing.
highlight: set of line numbers to mark with ">>>".
Example:
src = render_source("app/main.py", highlight={10, 11}, start=5, end=15)
print(src)
"""
lines = get_lines(filename, start=start, end=end)
hl = highlight or set()
out: list[str] = []
for i, text in enumerate(lines):
n = start + i
marker = ">>>" if n in hl else " "
out.append(f"{marker} {n:4d} {text.rstrip()}")
return "\n".join(out)
# ─────────────────────────────────────────────────────────────────────────────
# 5. Cache inspector and lazy registration
# ─────────────────────────────────────────────────────────────────────────────
@dataclass
class CacheEntry:
filename: str
size: int | None # bytes (None for synthetic entries)
mtime: float | None # mtime (None for lazycache entries)
line_count: int
def __str__(self) -> str:
size_str = f"{self.size:,d}B" if self.size else "synthetic"
return f"{self.filename} lines={self.line_count} {size_str}"
def inspect_cache() -> list[CacheEntry]:
"""
Return a summary of all entries currently held in linecache.
Example:
for entry in inspect_cache():
print(entry)
"""
result: list[CacheEntry] = []
for filename, value in linecache.cache.items():
if isinstance(value, tuple) and len(value) >= 3:
size, mtime, lines = value[0], value[1], value[2]
result.append(CacheEntry(
filename=filename,
size=size,
mtime=mtime,
line_count=len(lines) if lines else 0,
))
return sorted(result, key=lambda e: e.filename)
def register_source(module_name: str, source: str) -> None:
"""
Register arbitrary source text into linecache under a synthetic filename.
Useful for making eval'd or dynamically generated code inspectable.
Example:
code = "x = 1\\ny = x + 2\\n"
register_source("<mycode>", code)
print(linecache.getline("<mycode>", 2)) # "y = x + 2\\n"
"""
lines = source.splitlines(keepends=True)
linecache.cache[module_name] = (
len(source),
None, # no mtime — synthetic
lines,
module_name,
)
def lazy_register(module_name: str, module_globals: dict) -> bool:
"""
Register a module's source via its __loader__ (lazycache).
Returns True if registration succeeded.
This allows traceback to display source lines for frozen/zip-imported modules
without reading the whole file upfront.
Example:
import mymodule
lazy_register(mymodule.__name__, mymodule.__dict__)
"""
return linecache.lazycache(module_name, module_globals)
# ─────────────────────────────────────────────────────────────────────────────
# Demo
# ─────────────────────────────────────────────────────────────────────────────
if __name__ == "__main__":
import tempfile
import traceback
import sys
print("=== linecache demo ===")
# ── create a temp source file ──────────────────────────────────────────────
with tempfile.NamedTemporaryFile(suffix=".py", mode="w", delete=False) as f:
f.write(
"# demo module\n"
"import math\n"
"\n"
"def compute(x: float) -> float:\n"
" '''Compute something.'''\n"
" return math.sqrt(x) + math.pi\n"
"\n"
"def bad() -> None:\n"
" raise ValueError('intentional error')\n"
)
tmpfile = f.name
try:
# ── get_line ───────────────────────────────────────────────────────────
print("\n--- get_line ---")
for n in [1, 4, 6, 99]:
line = get_line(tmpfile, n, strip=True)
print(f" line {n:2d}: {line!r}")
# ── get_lines slice ────────────────────────────────────────────────────
print("\n--- get_lines(4..6) ---")
for i, ln in enumerate(get_lines(tmpfile, start=4, end=6), start=4):
print(f" {i}: {ln.rstrip()}")
# ── get_context ────────────────────────────────────────────────────────
print("\n--- get_context(line 6, before=2, after=2) ---")
ctx = get_context(tmpfile, 6, before=2, after=2)
print(ctx)
# ── render_source ──────────────────────────────────────────────────────
print("\n--- render_source (lines 3-9, highlight {6}) ---")
rendered = render_source(tmpfile, highlight={6}, start=3, end=9)
for line in rendered.splitlines():
print(f" {line}")
# ── annotate_traceback ─────────────────────────────────────────────────
print("\n--- annotate_traceback ---")
try:
# Execute the bad() function by importing the temp file
spec_obj = None
import importlib.util
spec_obj = importlib.util.spec_from_file_location("_demo", tmpfile)
demo_mod = importlib.util.module_from_spec(spec_obj)
spec_obj.loader.exec_module(demo_mod)
demo_mod.bad()
except ValueError:
tb = sys.exc_info()[2]
frames = [
(fr.filename, fr.lineno, fr.name)
for fr in traceback.extract_tb(tb)
]
for af in annotate_traceback(frames):
print(af)
# ── inspect_cache ──────────────────────────────────────────────────────
print("\n--- inspect_cache ---")
entries = inspect_cache()
for e in entries[:4]:
print(f" {e}")
print(f" ... ({len(entries)} total cached files)")
# ── register_source (synthetic) ────────────────────────────────────────
print("\n--- register_source (synthetic eval code) ---")
synthetic = "result = 42\nprint('hello from eval')\n"
register_source("<eval-block>", synthetic)
line1 = linecache.getline("<eval-block>", 1)
line2 = linecache.getline("<eval-block>", 2)
print(f" line 1: {line1!r}")
print(f" line 2: {line2!r}")
# ── refresh + clear ────────────────────────────────────────────────────
print("\n--- checkcache + clearcache ---")
before = len(linecache.cache)
refresh(tmpfile)
print(f" cache size before clear: {before}")
clear()
print(f" cache size after clear: {len(linecache.cache)}")
finally:
os.unlink(tmpfile)
print("\n=== done ===")
For the inspect alternative — inspect.getsource(obj) and inspect.getsourcelines(obj) retrieve source code for live objects (functions, classes, modules) using inspect.getfile() to find the file and then linecache.getlines() internally — use inspect when you have a reference to the live Python object and want its full source block with correct indentation and decorators; use linecache directly when you have only a (filename, lineno) pair (as returned by traceback, tracemalloc, or cProfile) and need to fetch surrounding context lines without importing or instantiating the module. For the ast.get_source_segment / tokenize alternative — ast.get_source_segment(source, node) extracts the exact source text for an AST node given the full source string; tokenize.generate_tokens() provides token-level access including whitespace and comments — use these when doing static analysis or source transformation where you already have the full source text in memory; use linecache for on-demand line fetching during runtime observation (tracebacks, profiler annotation, debugger displays) where you want the OS page cache and linecache’s own LRU to minimize I/O. The Claude Skills 360 bundle includes linecache skill sets covering get_line()/get_lines()/refresh()/clear() reading primitives, SourceContext with get_context() context window builder, AnnotatedFrame with annotate_traceback()/annotate_sites() traceback enrichers, render_source() numbered listing renderer with highlight support, and CacheEntry with inspect_cache()/register_source()/lazy_register() cache management tools. Start with the free tier to try source annotation patterns and linecache pipeline code generation.