pypa/pip · error · ValueError
%r not a successor of %r
Error message
%r not a successor of %r
What it means
Raised as ValueError by Sequencer.remove (pragma: no cover) when the successor's predecessor set exists but does not contain the specific pred, OR the predecessor's successor set does not contain succ — i.e. the exact (pred, succ) edge was never added or was already removed. It is the per-edge analogue of error 233, catching a missing specific relationship rather than a wholly-unknown node.
Source
Thrown at src/pip/_vendor/distlib/util.py:1126
del self._succs[k]
def add(self, pred, succ):
assert pred != succ
self._preds.setdefault(succ, set()).add(pred)
self._succs.setdefault(pred, set()).add(succ)
def remove(self, pred, succ):
assert pred != succ
try:
preds = self._preds[succ]
succs = self._succs[pred]
except KeyError: # pragma: no cover
raise ValueError('%r not a successor of anything' % succ)
try:
preds.remove(pred)
succs.remove(succ)
except KeyError: # pragma: no cover
raise ValueError('%r not a successor of %r' % (succ, pred))
def is_step(self, step):
return (step in self._preds or step in self._succs or step in self._nodes)
def get_steps(self, final):
if not self.is_step(final):
raise ValueError('Unknown: %r' % final)
result = []
todo = []
seen = set()
todo.append(final)
while todo:
step = todo.pop(0)
if step in seen:
# if a step was already seen,
# move it to the end (so it will appear earlier
# when reversed on return) ... but not for the
# final step, as that would be confusing forView on GitHub (pinned to f399c37189)
Solutions
- Make removal idempotent: check succ in seq._succs.get(pred, ()) before removing.
- Maintain an edges set and remove from it, only calling Sequencer.remove when present.
- Wrap remove in try/except ValueError for best-effort cleanup.
Example fix
# before
seq.remove('A', 'B')
# after
if 'B' in seq._succs.get('A', ()):
seq.remove('A', 'B') Defensive patterns
Strategy: validation
Validate before calling
def safe_remove_edge(seq, pred, succ):
if pred in seq._succs and succ in seq._succs[pred]:
seq.remove(pred, succ) Type guard
def edge_exists(seq, pred, succ) -> bool:
return pred in seq._succs and succ in seq._succs.get(pred, ()) Try / catch
try:
seq.remove(pred, succ)
except ValueError as e:
if 'not a successor of' in str(e):
pass Prevention
- Make removal idempotent.
- Track edges in your own set.
When it happens
Trigger: Calling remove('A','B') after the edge was already removed; remove('A','B') when only add('A','C') and add('X','B') exist (no A->B edge); double-cleanup of the same dependency.
Common situations: Idempotent teardown code that calls remove multiple times; rebuild logic that re-removes edges from a previous run; race between two cleanup paths.
Related errors
- %r not a successor of anything
- Unknown: %r
- invalid requirement: %s
- unexpected trailing data: %s
- path '%s' cannot be absolute
AI-assisted analysis of pypa/pip@f399c37189 (2026-08-08).
Data as JSON: /api/errors/c265cd109a8e8edc.
Report an issue: GitHub.