How can a Python function find out which function called it, using frame objects?
answer
- one of these per active call
- the chain points outward, not inward
- the caller link on a frame
- f_back then f_code.co_name
- sys._getframe(1) skips a level
basics
~10 sGet a frame object with inspect.currentframe() or sys._getframe(), follow its f_back attribute to the caller's frame, then read f_code.co_name for the caller's name and f_lineno for the call site.
solid answer
~40 sCPython creates a frame object for every active function call, and frames are chained: `f_back` points at the frame that made the call. `inspect.currentframe()` hands you your own frame, so `inspect.currentframe().f_back.f_code.co_name` is the caller's name; `sys._getframe(1)` is the same thing in one step, and it raises `ValueError` if the stack is not that deep. A frame also exposes `f_lineno` (the line executing there, which for a caller frame is the call site) and `f_globals` (the module namespace, so `f_globals["__name__"]` attributes the call to a module). Keep it to diagnostics: `sys._getframe` is a CPython implementation detail, every wrapper layer you add shifts the frame count, and holding a frame keeps all of its locals alive.
code
python · 12 linesimport inspect
def who_called_me():
return inspect.currentframe().f_back.f_code.co_name
def rebuild_shard():
return who_called_me()
print(rebuild_shard()) # rebuild_shardgo deeper
Be ready to name the two ways to get a frame and to walk one step outward: inspect.currentframe() or sys._getframe(), then f_back, then f_code.co_name. Knowing f_lineno gives the call site is a good extra.
Explain the mechanics: a frame per active call, chained by f_back, carrying the code object, line number and globals. Show why a wrapper shifts the frame count, and point at stacklevel as the maintained alternative.
Demonstrate restraint. Say where frame walking belongs (diagnostics, logging infrastructure) and where it does not (control flow), and mention that a retained frame pins every local on the chain behind it.
Own the policy question: frame introspection couples behaviour to code shape and to CPython. Decide whether a codebase may use it at all, and where the boundary sits between diagnostic tooling and ordinary library code.
Every time CPython calls a Python function it creates a **frame object**: the runtime record for one active invocation. A frame holds the function's local variables, a pointer to the code object being executed, the globals namespace that code was compiled against, the line currently executing, and a link to the frame that called it. Those links, chained together, *are* the live call stack. Reading that chain from inside running code is frame introspection, and it is how the standard library attributes a log record or a warning to the right source line. ### Getting hold of a frame There are two entry points. `inspect.currentframe()` returns the frame of the function that calls it, or `None` on a Python implementation that does not expose frames. `sys._getframe(depth)` returns a frame *depth* levels up the stack: `sys._getframe()` is your own frame, `sys._getframe(1)` your caller's, `sys._getframe(2)` your caller's caller; it raises `ValueError` when the stack is not that deep. For depth 0 the two are equivalent — `inspect.currentframe()` is a thin wrapper. The leading underscore on `sys._getframe` is the signal that matters: it is a CPython implementation detail, not a language guarantee. ### What a frame carries * **f_back** — the frame that called this one, or `None` at the outermost frame of that thread. Following `f_back` repeatedly walks outward toward the thread's entry point. * **f_code** — the code object being executed. Its `co_name` is the function's name as written at `def` time, `co_filename` the source file, `co_firstlineno` the line the `def` starts on. Note that `co_name` is the *compiled* name, not necessarily the name the function is bound to at the call site, and it does not include the class for a method. * **f_lineno** — the line executing in that frame. Read on a caller's frame it gives you the call site, which is what a logging helper actually wants. * **f_globals** — the module-level namespace dictionary that code sees. `f_globals["__name__"]` is a cheap way to attribute a call to the module it came from. * **f_locals** — the local variables of that frame. ### The idiom, and its off-by-one ```python import inspect def who_called_me(): return inspect.currentframe().f_back.f_code.co_name ``` Inside `who_called_me`, `inspect.currentframe()` is `who_called_me`'s own frame, so one `f_back` step lands on whoever called it. The equivalent is `sys._getframe(1).f_code.co_name`. The classic bug appears the moment you wrap this in another helper: every extra layer of indirection inserts a frame, so a hand-rolled logging wrapper that hardcodes one `f_back` step starts reporting the wrapper itself instead of real user code. This is exactly why the standard library does not ask you to guess — the `logging` calls and `warnings.warn` both accept a `stacklevel` argument so the caller states how many frames to skip, and the library does the walk. ### Reasons to be sparing **Portability.** `sys._getframe` and `inspect.currentframe()` are documented as implementation details. CPython has them; other implementations may omit them, provide only partial frames, or make them expensive because full frame objects defeat optimizations the runtime would otherwise perform. Library code that hard-depends on walking frames is code that only works on one interpreter. Where a frame walk is a convenience rather than a requirement, guard it and degrade to an explicit argument. **Cost.** A single `f_back` hop is a pointer dereference and effectively free. Building a full list of frame records is not: it is proportional to the depth of the stack and, by default, reads a source line for each frame. Reach for the narrow tool. **Lifetime.** A frame keeps everything in it alive: its locals, its arguments, and — through `f_back` — every frame outward from it. Stashing a frame in a cache, a log record or a retry buffer quietly pins whole object graphs. Extract the two or three strings you actually need (`co_name`, `f_lineno`, `f_globals["__name__"]`) and let the frame go; if you must hold one temporarily, `clear()` on the frame drops its local references once it is no longer executing. **Fragility.** Decorators, comprehensions and generator functions each get their own frames, so the depth from a given call site is not a constant you can hardcode across a codebase. Anything that counts frames is coupled to code shape, and refactoring silently changes the answer. Frame introspection is the right tool for diagnostics, logging infrastructure and debugging aids — the places where the alternative is threading a caller argument through every function in the system. It is the wrong tool for ordinary control flow: a function that changes what it does based on who called it is a function nobody can read.
- Why does a hand-rolled logging wrapper so often report itself instead of real user code?Because it hardcodes a fixed number of `f_back` steps, and every extra layer of indirection inserts a frame. Wrap the helper once more and the count is wrong. The standard library avoids the guess: `logging` calls and `warnings.warn` take a `stacklevel` argument, so the caller declares how many of its own frames to skip and the library does the walk.
- What should worry you about depending on sys._getframe in a library you publish?The leading underscore means it is a CPython implementation detail, not a language guarantee. Another implementation may not expose frames at all, or may pay a real optimization penalty to materialize them. Treat frame walking as a convenience with a fallback — an explicit caller argument, or a module-level name — rather than the mechanism your public behaviour depends on.
- Why is stashing a frame object in a cache or a log record risky?A frame holds its locals and, through `f_back`, every frame outward from it, so one retained frame can pin an entire call chain's objects long after the calls finish. Extract the few strings you need — `co_name`, `f_lineno`, the module name — and drop the frame; if you must hold one briefly, call `clear()` on it to release its local references.
A frame is the sticky note the interpreter leaves for one call in progress, and f_back is the note's arrow pointing at the note that sent it here.
saying these in an interview costs you the question
- Thinks f_back points at the next call rather than the caller
- Believes inspect.currentframe() returns the caller's frame
- Assumes sys._getframe exists on every Python implementation
- Confuses co_name with the variable the function is bound to
- Keeps frame objects in caches, log records or error objects