Support matrix¶
Source recovery depends on the function's source existing somewhere inspect can find it. That is a property of the execution context, and no library can change it. This page is the contract: eighteen contexts, measured across CPython 3.10 through 3.14, with what redef does in each.
Contexts that work¶
| Context | Notes |
|---|---|
| module imported from a file | the baseline |
python script.py |
|
python -OO script.py |
future flags and optimization inherited |
| module imported from a zip | |
python -c "..." |
3.13+ only; earlier versions have no source |
exec + a hand-installed linecache entry |
how notebooks stay readable |
linecache._register_code + a real module |
3.13+, the supported registration path |
| IPython / Jupyter cell | |
| nested function | dedent + offset repairs applied |
| method in a class |
Contexts that raise, and what the error says¶
Every failure raises SourceUnavailable with a reason and, where one exists, a remedy:
| Context | reason |
Remedy |
|---|---|---|
sourceless .pyc (frozen apps: PyInstaller, Nuitka) |
frozen-or-sourceless |
none; state it in your tool's docs |
python < script.py |
stdin |
none |
plain REPL (code.InteractiveConsole) |
repl |
none |
plain exec into a bare dict |
bare-exec |
see below |
linecache._register_code into a bare namespace |
bare-exec |
see below |
The bare-exec remedy, verbatim from the error message: exec into a namespace registered in sys.modules and register the source with linecache. Both halves are required, because inspect.findsource consults the registry only when the function's globals belong to a real module:
import linecache, sys, types
mod = types.ModuleType("generated")
sys.modules["generated"] = mod
code = compile(src, "<generated>", "exec")
linecache.cache["<generated>"] = (len(src), None, src.splitlines(True), "<generated>")
exec(code, mod.__dict__)
Refusals that are not about availability¶
| Situation | Error | Why |
|---|---|---|
| lambda | NotAFunction |
getsourcelines returns the physical line; two lambdas on one line are indistinguishable |
| closure, at rewrite time | ClosureUnsupported |
recompiling a lone def turns free variables into global lookups that fail at call time |
function carrying __wrapped__ |
InnermostViolation |
a wrapper below the rewrite means the source no longer describes the object |
| stale source (wrong name) | SourceMismatch |
a linecache entry outlived its cell; the text defines a different function |
| stale source (same name, changed body) | SourceMismatch |
caught by verification, invisible to a name check |
recover() alone accepts closures (it reports freevars in its metadata and verification compiles them in a synthetic enclosing scope); only recompilation refuses them. Acquisition-only tools, ones that analyze the tree without re-defining the function, can therefore use recover on closures freely, self-recursive nested functions included.
Reading the matrix honestly¶
The envelope is 10 of 18 on CPython 3.14 and smaller on 3.10 through 3.12 (python -c and the registration contexts arrive at 3.13). If your tool decorates functions, this is your tool's envelope too, whoever implements the pipeline. What redef adds at the edges is that every cell outside the envelope is a named error instead of an OSError, a wrong function, or a NameError three calls later.