skip to content

How can a Python function find out which function called it, using frame objects?

level: juniorimportance: should knowfreq 30%

answer

  1. one of these per active call
  2. the chain points outward, not inward
  3. the caller link on a frame
  4. f_back then f_code.co_name
  5. sys._getframe(1) skips a level

basics

~10 s

Get 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 s

CPython 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 lines
python
import 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_shard

go deeper

for a junior

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.

for a middle

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.

for a senior

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.

for a principal

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

context