How do you use gc.get_referrers() to find what still holds a leaking object?
answer
- Census says what, this says who
- Ask the collector who points here
- The first answer is usually a dict
- Climb again to name the owner
- Your own frame is a referrer too
basics
~20 sTake one instance of the accumulating type and call gc.get_referrers() on it to get the tracked objects pointing at it, then repeat on each result to climb towards a named holder. Most first-level hits are anonymous dicts.
solid answer
~40 s`gc.get_referrers(obj)` returns every gc-tracked object that refers directly to `obj`. In practice the first level is almost always uninformative -- a `dict` -- because the holder keeps the object in its instance `__dict__` or in a cache mapping, so you call `gc.get_referrers()` again on that dict to learn whose it is, and repeat until you reach a module, a function's closure cell or a named container. Two disciplines make it usable: run `gc.collect()` first so dead cycles do not appear as holders, and expect your own frame, argument tuples and any list you built to show up as referrers -- filter them out or the search chases itself. `gc.get_referents()` is the inverse, walking down from a suspected holder to what it keeps alive. Delete every intermediate list when done.
code
python · 22 linesimport gc
class LabResult:
pass
_cache = {}
def load(sample_id):
_cache[sample_id] = LabResult()
load("S-1")
gc.collect()
leaked = next(o for o in gc.get_objects() if isinstance(o, LabResult))
holders = gc.get_referrers(leaked)
print([type(h).__name__ for h in holders])
for holder in holders:
if isinstance(holder, dict):
print([type(up).__name__ for up in gc.get_referrers(holder)])
del leaked, holdersgo deeper
Know that the collector can answer 'who points at this object' via gc.get_referrers, and that the opposite question -- 'what does this object point at' -- is gc.get_referents. Recognising the two names and their direction is enough at this level.
Explain the climb: the first referrer is typically a dict, so you call gc.get_referrers on that dict to find the instance or module owning it. Mention that you must collect first and that your own frame and temporary lists appear as referrers.
An interviewer expects a whole diagnosis, not a function name: narrow by type, probe one instance, climb to a named holder, then translate that chain into a fix. Know the tool's limits and its cost on a live process.
Own the strategy: what a service exposes so this hunt needs no debugger attached to production, which corroborating signals deserve alarms of their own, and where caches may live so unbounded retention is a review question.
### The question this answers A type census tells you *what* is accumulating. It never tells you *who* is holding it, and that is the answer you actually need, because the fix lives at the holder. `gc.get_referrers(obj)` is the direct tool: given an object, it asks the collector which tracked objects contain a reference to it. A worked case makes the shape clear. A clinical-lab result loader runs as a long-lived service; RSS climbs steadily across a shift while throughput is flat, and the batch cache reports a healthy 83% hit rate. A census shows result instances growing linearly with samples processed. So take one instance and climb. ```python import gc gc.collect() leaked = next(o for o in gc.get_objects() if isinstance(o, LabResult)) holders = gc.get_referrers(leaked) print([type(h).__name__ for h in holders]) # ['dict', 'dict'] ``` ### Why the first level is almost always a dict Python objects rarely refer to each other directly. An instance attribute lives in that instance's `__dict__`; a cache entry lives in a mapping; a module-level name lives in the module's globals dict. So the referrer you get back is the *dict*, not the thing that owns the dict, and the name of the type tells you nothing. The move is to climb again: ```python for holder in holders: if isinstance(holder, dict): print([type(up).__name__ for up in gc.get_referrers(holder)]) ``` One or two more levels typically land on something identifiable: a `module` (a module-level registry), an instance of your own class (an attribute on a long-lived object), a `function` or a `cell` (a closure that captured the payload), or a `frame` (something is still on a stack). In the lab-loader case the chain ends at the module holding a memo dict that keyed results by sample id and never evicted -- the same dict responsible for the 83% hit rate, which is why nobody suspected it. The cache was doing its job; nobody had given it a ceiling. ### The noise, and how to control it `gc.get_referrers()` is honest to a fault: it reports *your* references too. - The frame you are running in refers to `leaked`, so a `frame` (or the module globals dict, at module level) appears. - Any list you built while searching -- the result of `gc.get_objects()`, a filtered list of candidates -- is a referrer. - The argument tuple of the call itself can appear. So: collect first, keep the search code in a small function so the noise is one identifiable frame rather than your whole session, compare by identity against the locals you know about, and `del` intermediate lists as soon as you are done with them. Holding those lists is not a theoretical concern -- they pin everything they contain and will distort the next census you take. ### The limits worth stating out loud **Only tracked referrers are visible.** The collector knows about container objects. An object reachable only from a slot on an untracked object, from a C-level structure inside a native extension, or from a thread-local in an extension module will show fewer referrers than the refcount implies -- sometimes none at all. Comparing `sys.getrefcount(obj)` with the number of referrers found is a cheap sanity check: a large gap says something invisible to the collector is holding it. **It is expensive.** Finding referrers means scanning the tracked heap, so treat it as a targeted probe on one or two objects, never a loop over thousands. **`gc.get_referents()` goes the other way.** Given a suspected holder, it lists what that object refers to directly. It is the tool for confirming a hypothesis ("does this cache still contain results from yesterday's batch?") and for writing a small breadth-first walk from a known root down to the payload, which is often easier to read than climbing up through anonymous dicts. ### Interpreting what you find The holder chain names the fix, and the fix is a policy at the holder, not a call to the collector: - module globals or a class attribute -> an unbounded registry; cap it, drain it per batch, or delete it; - a closure cell -> a callback, retry handler or partially applied function captured a large payload; capture the small key instead of the object; - a frame -> something is still on a stack, often a generator that was never exhausted or closed, or an exception being held; - a long-lived instance of your own class -> an attribute that outlives the request it belongs to. One more corroborating signal from the same case: because each cached result also held an open file object from the load path, the file-descriptor count climbed in lockstep with RSS. When a leak has a matching resource that was never closed, the descriptor or handle count is often the faster and cheaper alarm.
- Why is the first referrer you get back usually just a dict?Because Python objects mostly refer to each other through mappings: an instance attribute lives in that instance's `__dict__`, a cache entry in a mapping, a module-level name in the module globals dict. The dict is the direct referrer, and it carries no identity of its own, so you call `gc.get_referrers()` on the dict to find the instance or module that owns it. Two or three levels usually reaches something nameable.
- When would gc.get_referents() be the better tool than gc.get_referrers()?When you already have a suspect. `gc.get_referents()` lists what a given object points at, so you can confirm that a specific cache or long-lived instance still holds yesterday's results, or write a breadth-first walk down from a known root to the payload. Walking down is also far cheaper: it reads one object's references instead of scanning the tracked heap to find who points in.
- An object's refcount is 40 but gc.get_referrers() returns three objects. What does that mean?That most of the references live somewhere the collector does not track: slots or C-level fields in a native extension, structures inside the interpreter, or untracked containers. `gc.get_referrers()` can only report tracked objects, so a large gap between `sys.getrefcount()` and the referrer count is the signal to stop climbing in pure Python and start looking at the extension or interop layer instead.
- How do you keep the search itself from distorting the result?Run `gc.collect()` first so dead cycles do not appear as holders, do the work inside a small function so your own references are one identifiable frame rather than a whole session's locals, and `del` every intermediate list -- the census list and each referrer list -- as soon as you are done. Those lists pin everything in them, so keeping one around inflates the next census and can hide the very object you are chasing.
Finding the holder is a chain of custody run backwards: each step names only the folder the item sits in, so you keep asking whose folder that is until you reach a desk with a nameplate.
saying these in an interview costs you the question
- Reports the first dict as the culprit and stops
- Forgets their own frame and lists are referrers
- Runs get_referrers in a loop over thousands of objects
- Assumes every holder is visible to the collector
- Calls gc.collect() and expects reachable objects to vanish
- Confuses referrers with referents and walks the wrong way