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 for

View on GitHub (pinned to f399c37189)

Solutions

  1. Make removal idempotent: check succ in seq._succs.get(pred, ()) before removing.
  2. Maintain an edges set and remove from it, only calling Sequencer.remove when present.
  3. 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

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


AI-assisted analysis of pypa/pip@f399c37189 (2026-08-08). Data as JSON: /api/errors/c265cd109a8e8edc. Report an issue: GitHub.