RuntimeError

An operation that was legal on its own became illegal because of the state around it — the classic is changing a dict while a for loop is walking it.

InheritsBaseException›Exception›RuntimeError
Runtime exceptionPython 3 (all)Live demo
RuntimeError(*args)
Raised by
mutating a dict/set during a for loop, StopIteration inside a generator, asyncio outside a loop
Message
dictionary changed size during iteration
Quick fix
loop over list(d) or build a new dict
Watch out
except RuntimeError also catches RecursionError and NotImplementedError

Demo

Live evaluation
Delete matching items while looping over the dict itself. Any deletion breaks the loop — even of the last key.
Try:
Inputs
qtyintdelete items with this stock
Code
stock = {'apple': 3, 'pear': 0, 'plum': 0}
for name in stock:
    if stock[name] == 7:
        del stock[name]
stock
Result
{'apple': 3, 'pear': 0, 'plum': 0}

In Trigger, deleting 'apple' (qty 3) is the only change, yet the loop still dies: the dict iterator checks the size on every next() call, so the error comes from the step AFTER the del. Handle deletes the same items without complaint, because list(stock) copied the keys before the loop started.

Constructor

NameTypeRequiredDescription
*argsobjectnoUsually one string saying what went wrong. Stored in e.args; str(e) is that string.

Attributes

AttributeTypeMeaning
argstupleThe constructor arguments — normally the one message string.
__cause__BaseException | NoneFor "generator raised StopIteration" this is the original StopIteration.
__context__BaseException | NoneThe exception being handled when this one was raised (implicit chaining).

Common patterns

Filter by building a new dict
Usually clearer than deleting in place, and it never iterates the dict it changes.
in_stock = {name: qty for name, qty in stock.items() if qty > 0}
Delete from a snapshot
When the same dict object must be kept (other code holds a reference), iterate a copy of the keys.
for name in list(stock):
    if stock[name] == 0:
        del stock[name]
Raise it for invalid state
Your own code can raise RuntimeError when an object is used in a state that makes the call meaningless and no more specific built-in fits.
class Connection:
    def send(self, data):
        if self.closed:
            raise RuntimeError('send() on a closed connection')
        self._sock.sendall(data)

Examples

1. Deleting while looping over a dict
d = {'a': 1, 'b': 2} for k in d: if d[k] == 2: del d[k]
Returns
RuntimeError: dictionary changed size during iteration
2. Adding to a set while looping
s = {1, 2, 3} for x in s: s.add(x * 10)
Returns
RuntimeError: Set changed size during iteration
3. Same size, different keys
d = {'a': 1} for k in d: del d[k] d[k + '!'] = 1
Returns
RuntimeError: dictionary keys changed during iteration
4. StopIteration leaking out of a generator (PEP 479)
def firsts(rows): for row in rows: yield next(iter(row)) list(firsts([[1], [], [3]]))
Returns
RuntimeError: generator raised StopIteration
5. The original StopIteration is the cause
def gen(): yield next(iter([])) try: list(gen()) except RuntimeError as e: cause = e.__cause__ cause
Returns
StopIteration()
6. asyncio outside an event loop
import asyncio asyncio.get_running_loop()
Returns
RuntimeError: no running event loop
7. Zero-argument super() outside a class
def f(): return super() f()
Returns
RuntimeError: super(): no arguments
8. Its subclasses
issubclass(RecursionError, RuntimeError), issubclass(NotImplementedError, RuntimeError), issubclass(PythonFinalizationError, RuntimeError)
Returns
(True, True, True)

Pitfalls

1. Changing values is fine, changing keys is not
Assigning to an existing key during the loop is allowed — the size and key set stay the same. Only adding or removing keys breaks the iterator.
Remove in loop
prices = {'a': 5, 'b': 0}
for k in prices:
    if prices[k] == 0:
        del prices[k]
prices
RuntimeError: dictionary changed size during iteration
Update values only
prices = {'a': 5, 'b': 0}
for k in prices:
    prices[k] = prices[k] * 2
prices
{'a': 10, 'b': 0}
2. next() inside a generator
A bare next() that runs out inside a generator no longer ends the generator quietly (Python 3.7+) — it becomes RuntimeError. Give next() a default, or return explicitly.
Bare next()
def firsts(rows):
    for row in rows:
        yield next(iter(row))
list(firsts([[1], []]))
RuntimeError: generator raised StopIteration
next(it, default)
def firsts(rows):
    for row in rows:
        yield next(iter(row), None)
list(firsts([[1], []]))
[1, None]
3. except RuntimeError is wider than it looks
RecursionError, NotImplementedError and PythonFinalizationError are all subclasses, so a handler meant for one specific runtime condition also hides an unimplemented method.
Broad handler
class Base:
    def save(self):
        raise NotImplementedError
try:
    Base().save()
    status = 'saved'
except RuntimeError:
    status = 'busy, retry later'
status
'busy, retry later'
Let the bug surface
class Base:
    def save(self):
        raise NotImplementedError
try:
    Base().save()
    status = 'saved'
except RuntimeError as e:
    if isinstance(e, NotImplementedError):
        raise
    status = 'busy, retry later'
status
NotImplementedError

When to use

Use it
  • Your object is called in a state where the operation cannot work (closed, not started, already finished)
  • An invariant broke at runtime and no more specific exception (ValueError, TypeError, …) fits
Reach for something else
  • Bad argument values → ValueError; wrong types → TypeError
  • Methods a subclass must provide → NotImplementedError
  • Catching it broadly — you also catch RecursionError and NotImplementedError

Notes

CPython impl
Objects/dictobject.c — the dict iterator stores the size it started with and raises on the next step if ma_used differs
Subclasses
RecursionError (3.5+), NotImplementedError, PythonFinalizationError (3.13+)
Not this one
'generator already executing' is a ValueError, not a RuntimeError

FAQ

Do not add or remove keys of a dict while a for loop is iterating it. Either loop over a snapshot — for k in list(d): — or build a new dict with a comprehension: d = {k: v for k, v in d.items() if keep(v)}. Updating the value of an existing key inside the loop is fine. The same applies to sets (Set changed size during iteration) and to d.keys(), d.values() and d.items(), which are live views of the dict.

History

3.5
RecursionError split out: previously a plain RuntimeError was raised for exceeding the recursion limit.
3.7
PEP 479 enabled for all code: StopIteration raised inside a generator becomes RuntimeError.
3.13
PythonFinalizationError added: previously a plain RuntimeError was raised for operations blocked during shutdown.