Python: Warum wird ein veränderlicher Standardwert zwischen Aufrufen geteilt?

Eine Funktion mit def f(x, in_liste=[]) bekommt nicht bei jedem Aufruf eine frische Liste. Sie bekommt jedes Mal dieselbe, für die gesamte Laufzeit des Programms. Hier der Grund — und was man stattdessen tut.

Die Überraschung

def sammle(element, in_liste=[]):
    in_liste.append(element)
    return in_liste
collect('a') : ['a']
collect('b') : ['a', 'b']
collect('c') : ['a', 'b', 'c']

Drei scheinbar unabhängige Aufrufe, eine Liste.

Wo die Liste tatsächlich liegt

Die Regel lautet: Standardwerte werden einmal ausgewertet, wenn die def-Anweisung läuft — nicht bei jedem Aufruf. Das Ergebnis wird am Funktionsobjekt abgelegt, und du kannst es dir ansehen:

collect.__defaults__ : (['a', 'b', 'c'],)
id of the default    : 135662073621888
id of what came back : 135662073621888   <- the same object

__defaults__ ist ein Tupel der Standardwerte und ein Attribut der Funktion wie jedes andere. Nichts setzt es zwischen den Aufrufen zurück, denn das war nie vorgesehen.

Sobald man es als Attribut des Funktionsobjekts sieht und nicht als Anweisung die noch ausgeführt wird, hört das Verhalten auf, seltsam zu sein. def ist eine Anweisung die läuft: Sie wertet die Standardwerte aus, baut ein Funktionsobjekt und bindet einen Namen daran.

Es geht eigentlich nicht um Veränderlichkeit

Über die Veränderlichkeit wird das Problem üblicherweise beschrieben, und das verdeckt einen zweiten Fall:

def mit_zeit(wann=datetime.datetime.now()):
    return wann
two calls 50 ms apart return the same time : True
value : 2026-08-31 08:23:44.298407

Es wurde nichts verändert. datetime.now() lief einmal, beim Import des Moduls, und jeder Aufruf seither liefert genau diesen Moment. Ein lange laufender Prozess meldet damit vergnügt einen Zeitstempel von vor Tagen.

Dasselbe gilt für einen Standardwert uuid4(), time.time() oder alles andere, von dem du erwartet hast, es werde frisch ausgewertet.

Die Veränderlichkeit entscheidet nur darüber, ob es dir auffällt:

defaults : ([], {}, set(), '', 0, ())

Alle sechs werden geteilt. str, int und tuple lassen sich nicht an Ort und Stelle ändern, das Teilen hat dort also keine sichtbare Wirkung. Bei list, dict und set schon.

Die Lösung

def sammle_ok(element, in_liste=None):
    if in_liste is None:
        in_liste = []
    in_liste.append(element)
    return in_liste
collect_ok('a') : ['a']
collect_ok('b') : ['b']
collect_ok.__defaults__ : (None,)

None ist unveränderlich, das Teilen kostet also nichts, und das [] entsteht jetzt im Rumpf — der bei jedem Aufruf läuft.

Dieselbe Form funktioniert beim Zeitstempel:

def mit_zeit_ok(wann=None):
    if wann is None:
        wann = datetime.datetime.now()
with the None sentinel, same? False

Wenn None ein gültiger Wert ist

Manchmal ist None ein sinnvolles Argument, und du musst “nicht übergeben” von “als None übergeben” unterscheiden. Dann nimmst du ein eigenes Markierungsobjekt:

FEHLT = object()

def einstellung(wert=FEHLT):
    if wert is FEHLT:
        return "kein Wert uebergeben"
    return f"bekommen: {wert!r}"
setting()     : no value passed
setting(None) : got None
setting(0)    : got 0

Ein blankes object() genügt — es ist eindeutig, es ist mit nichts anderem gleich, und is ist der richtige Test. Das ist dasselbe Muster wie die Markierung im Artikel darüber, ob eine Liste leer ist.

Dieselbe Regel beißt im Klassenrumpf

Dort begegnet man ihr ein zweites Mal, ohne sie wiederzuerkennen:

class Korb:
    elemente = []            # von jeder Instanz geteilt
    def hinzu(self, x):
        self.elemente.append(x)
b1.items : ['apple']
b2.items : ['apple']   <- the same list
same object? True

Auch ein Klassenrumpf wird einmal ausgeführt, elemente ist also eine Liste die der Klasse gehört. self.elemente.append(...) findet sie über die Instanz und verändert das Klassenattribut.

Weis sie stattdessen in __init__ zu:

class KorbOk:
    def __init__(self):
        self.elemente = []
with __init__ : c1=['apple'] c2=[]

Eine @dataclass nimmt dir das ab und verweigert einen veränderlichen Standardwert rundheraus, mit dem Hinweis auf field(default_factory=list).

Es ist nicht immer ein Fehler

Der Mechanismus hat berechtigte Anwendungen, und die sollte man kennen, damit aus der Regel kein Aberglaube wird.

Ein Cache, der an der Funktion hängt:

def fib(n, _cache={0: 0, 1: 1}):
    if n not in _cache:
        _cache[n] = fib(n - 1) + fib(n - 2)
    return _cache[n]
fib(30) with a default-dict cache : 832040
cache size after : 31

Das funktioniert, und ich würde trotzdem functools.lru_cache nehmen — es sagt, was es meint, und stellt kein Implementierungsdetail in die Signatur.

Die andere Variante, die man in heißen Schleifen sieht, ist das Binden eines globalen Namens an einen lokalen:

def schnell(n, _len=len):
    return _len(n)

Damit wird aus einer globalen Suche eine lokale. Eine echte Optimierung, und fast nie den Lärm wert.

Der führende Unterstrich ist in beiden Fällen die Konvention für “diesen Parameter sollst du nicht übergeben”.

Ein Werkzeug suchen lassen

Jeder Linter kennt das:

  • ruff und flake8-bugbear: B006, Do not use mutable data structures for argument defaults
  • pylint: W0102, dangerous-default-value

Beide sind in den üblichen Konfigurationen aktiv. Bei einer Codebasis mit ein paar Jahren auf dem Buckel lohnt sich ein Lauf allein für diese Prüfung.

Hinweis zu Netcup (Werbung)

Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).