Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Worum es geht. Systematisch herausfinden, warum ein Programm nicht tut, was es soll. Zuerst eine Ordnung der Fehlerarten — denn davon hängt ab, welches Werkzeug hilft —, dann assert und print, der Debugger pdb, der Traceback, Logging, und zum Schluss ein Blick auf den Bytecode.

Was Sie danach können. Einen Fehler einer der sieben Klassen zuordnen und daraus das passende Werkzeug ableiten; einen Traceback von der richtigen Seite lesen; in pdb mit einem Dutzend Befehlen arbeiten und sich nach einem Absturz post mortem in den Zustand von damals setzen; und Logging statt print einsetzen.

Die Java-Entsprechung in einem Satz. Der Stack Trace liest sich wie in Java, nur umgekehrt — die auslösende Zeile steht unten; und wo Java Log4j oder SLF4J einbindet, ist logging Teil der Standardbibliothek.

Es gibt sehr unterschiedliche Arten von Fehlverhalten, die ein Programm zeigen kann, aber man kann allen davon systematisch begegnen.

Arten von Fehlern in der Softwareentwicklung

  1. Syntaktische Fehler

    • Der Interpreter weist uns auf diese Fehler hin, wenn wir nicht versehentlich gültigen Code geschrieben haben oder wenn der fehlerhafte Code nicht vom Interpreter erreicht wird (in der Ausführungskonfiguration). Dagegen helfen weitere statische Analysewerkzeuge wie Linter (die den Code lesen, ohne ihn auszuführen, und mögliche Fehlerquellen aufzählen), die oft auch in IDEs integriert sind.

  2. Laufzeitfehler

    • Wie die Division durch 0, Zugriff auf einen Nullpointer, Voller Speicher etc. Dabei hilft uns Exception Handling mit raise und try..except. In diesen Fällen gibt es eine ‘Stack Trace’ aus der sich ablesen lässt, welcher Code genau akut zu diesem Fehler führte. Das ist noch nicht unbedingt die Stelle Code, die man ändern möchte um den Fehler zu beheben, aber immerhin die erste Anlaufstelle, um zu verstehen, was genau passiert ist. Wir modellieren, was passiert ist, und vergleichen das mit unserem mentalen Modell, was passieren sollte, und finden so die Stelle, die wir wirklich ändern wollen (wo unser mentales Modell nicht mit der Wirklichkeit übereinstimmt; entweder wir ändern unser mentales Modell oder den Code).

  3. Semantische Fehler (Logikfehler)

    • Beispiel: Wenn man z.B. x = y + z geschrieben hat, aber x = y * z die richtige Rechnung wäre (um einen bestimmten Algorithmus zu implementieren). Das Programm läuft ohne Fehler, aber die Ausgabe entspricht nicht den Erwartungen. Hier kann man ebenfalls systematisch die Stelle im Code finden, ab der die Erwartungen (unser mentales Modell) nicht mehr der Realität entsprechen. Häufig lassen sich kleinere Methoden isoliert testen, um solche Fehler gering zu halten.

  4. Umgebungsfehler (‘also bei mir klappt alles’)

    • Abhängig von installierten Paket(version)en, Betriebssystem, Zustand der Internetverbindung, Rechte im Dateisystem, API Limits, kann sich ein Programm recht unterschiedlich verhalten. Anwendungen, die in sehr heterogenen Umgebungen eingesetzt werden, müssen daher aufwändig in diesen Umgebungen getestet werden. Dazu sind Continuous Integration und Containerization nützlich.

  5. Nicht-deterministische Fehler (‘Heisenbugs’)

    • Fehler, die sehr fragil von bestimmten Laufzeitparametern abhängen, oft race conditions in Code mit mehreren Threads oder Netzwerkverbindungen. Da print-Statements oft das Timing stark verändern, sind diese Bugs schwer zu debuggen. Häufig ist eine Entkopplung und Vereinfachung von Systemen der einzige Weg, um systematisch race conditions zu vermeiden. Auch intensives Logging mit präzisen Timestamps kann hilfreich sein, um solchen Fehlern auf den Grund zu gehen.

  6. Requirements-Fehler

    • Das Programm tut, was es soll, laut Entwickler; der Nutzer/Auftraggeber hatte sich aber etwas anderes vorgestellt - die Anforderungen sind nicht korrekt umgesetzt. Während es wenig formale Werkzeuge gibt, diese Sorte Fehler zu vermeiden, ist es eine wichtige Fehlerkategorie in der Praxis. In der Regel hat man jede Menge implizite Annahmen, was ‘offensichtlich’ ist. Es kann hilfreich sein, das offensichtliche explizit zu machen.

  7. Bedienfehler

    • Das Programm tut, was vorgesehen ist laut Entwickler; der Nutzer hat das Interface / die Eingabedaten aber nicht so verwendet, wie vorgesehen und dadurch treten Fehler oder fehlerhafte Ausgaben auf. Wenn bei einem Bedienfehler kryptische Fehlermeldungen auftreten, so ist dies eine Version von Requirements-Fehler, denn fast immer (außer bei kurzen Skripten) möchte man Nutzereingaben bzw. Nutzerverhalten so verarbeiten, dass alle möglichen (auch die sinnlosen) Eingaben/Eingabedaten zu gültigen Programmzuständen führen. Dieser Fehlerklasse kann man frühzeitig entgegen wirken, indem man Assertions über Nutzereingaben in den Code schreibt (die dann ggf. aufgefangen werden, um dem Nutzer entsprechendes Feedback zu geben, wenn der ‘Vertrag’ nicht eingehalten wurde). Traditionell ist der Bedienfehler auch als Layer-8-Fehler (PEBKAC) bekannt.

Man könnte hier noch mehr aufzählen, z.B. Varianten von schlecht organisiertem Code, was wiederum eigene Fehlerquellen/-arten mit sich bringt, aber das bringt uns hier nicht so viel.

Debugger helfen im Wesentlichen bei Fehlerklasse 2, dem Laufzeitfehler. Häufig präsentieren sich auch die anderen Fehler auf die ein oder andere Weise als Laufzeitfehler, sodass wir sehr davon profitieren, wenn wir diese systematisch beheben können. Vereinfacht gesagt beruht Debugging auf der wissenschaftlichen Methode: Wir stellen Hypothesen auf und versuchen, diese zu falsifizieren. Dabei werden unsere Hypothesen immer besser/enger. Ein Debugger ist nur ein technisches Hilfsmittel, um schneller Hypothesen zu falsifizieren.

Debugging mit assert und print

Eine typische Hypothese beim Debugging hat z.B. die Form

``Der Inhalt der Variable x in Zeile 12 ist der leere String’’

das können wir leicht testen, indem wir in Zeile 12 die Zeile

assert x == ""

einfügen, und betrachten ob es einen AssertionError gibt. Wenn wir in einer komplexen Umgebung arbeiten, kann es leider sein, dass so ein AssertionError aufgefangen wird, und vielleicht wollen wir auch noch mehr über die Natur des Problems wissen; daher könnten wir auch den Wert von x ausgeben in Zeile 12:

print("Hoping x is the empty string, it is", x)

Noch schöner wäre es, diese Information direkt an eine Assertion zu knüpfen - der gegebene String wird in den AssertionError verpackt.

assert x == "", "We were all expecting x to be empty by now..."

Debugging mit print ist für kurze Skripte genau das richtige, aber für größere Anwendungen ist es deutlich komfortabler mit logging zu arbeiten (das ist wie Java’s Log4j). Es sollten dann eigentlich keine prints mehr zum Debugging verwendet werden.

Breakpoints und pdb

Mit print-Debugging kommt man leicht dazu, dass man das Programm laufen lässt, bis zu der Stelle an der man gerade etwas mit print ausgibt um zu sehen, ob die Erwartungen erfüllt werden. Dann studiert man die Ausgabe und möchte evtl. noch mehr Variablen betrachten. Dazu ändert man den Code und lässt das Programm erneut laufen - sehr ineffizient.

Schöner ist es, an der Stelle im Programm anzuhalten, damit man den Code vor dem Fehler nicht mehrfach wieder ausführen muss. Dazu schreiben wir nur einmalig breakpoint() in den Code. Sobald der Interpreter dahin kommt, springt pdb an, der (interaktive) Python Debugger. Hier kann man die Werte aller Variablen inspizieren, und z.B. Zeile für Zeile durch den Code gehen.

In pdb gibt help eine Übersicht über die verfügbaren Befehle, help <befehl> die Einzelheiten. Wer die folgenden gut ein Dutzend kennt, kommt praktisch immer durch — mehr braucht man selten:

Befehllangwas er tut
nnexteine Zeile weiter, ohne in Funktionsaufrufe hineinzugehen
sstepeine Zeile weiter, mit Eintritt in den Aufruf
rreturnbis zum return der aktuellen Funktion durchlaufen
ccontinueweiterlaufen bis zum nächsten Breakpoint
unt 42untillaufen, bis Zeile 42 erreicht ist
p xprintAusdruck auswerten und ausgeben
pp xpretty printdasselbe, aber umgebrochen — für Dicts und Listen
llistdie Zeilen um die aktuelle Position herum
lllonglistdie ganze aktuelle Funktion
wwhere (= bt)Stack Trace: wer hat wen aufgerufen
u / dup / downim Stack eine Ebene nach oben bzw. unten wechseln
aargsdie Argumente der aktuellen Funktion
b 42breakBreakpoint in Zeile 42; b 42, i > 5 macht ihn bedingt
interact–eine normale Python-Shell im aktuellen Frame öffnen
qquitabbrechen

Die wichtigste Falle dabei: Ein nackter Variablenname wird als Befehl gelesen, wenn er auf einen passt. Eine Variable namens c gibt man mit p c aus, sonst läuft das Programm weiter. Aus demselben Grund ist p auch dann nützlich, wenn man gar nichts formatieren will.

pdb muss man nicht über breakpoint() im Code betreten. Vier Aufrufvarianten deckt man mit der Kommandozeile ab:

python -m pdb skript.py              # von der ersten Zeile an im Debugger
python -m pdb -c continue skript.py  # laufen lassen, bei der Exception anhalten
python -m pdb -c 'until 42' skript.py
PYTHONBREAKPOINT=0 python skript.py  # alle breakpoint()-Aufrufe ignorieren

Seit Python 3.14 kommt eine fünfte Variante hinzu, die vorher nicht möglich war: sich an einen schon laufenden Prozess hängen, ohne ihn neu zu starten.

python -m pdb -p 1234        # 1234 ist die PID, z.B. aus os.getpid()

Das ist die für den Alltag sichtbarste Folge von PEP 768, das dafür eine Schnittstelle ohne Laufzeitkosten in CPython eingebaut hat (darunter liegt sys.remote_exec). Gedacht ist es für den Fall, in dem ein Neustart nicht in Frage kommt — ein Dienst in Produktion, ein Prozess, der seit Stunden rechnet und sich gerade aufhängt. Eine Einschränkung nennt die Dokumentation ausdrücklich: Hängt der Prozess in einem Systemaufruf oder wartet auf I/O, greift das Anhängen erst, wenn die nächste Bytecode-Instruktion ausgeführt wird oder ein Signal eintrifft. Die PID findet man mit os.getpid() im Programm selbst (siehe Arbeiten mit Prozessen) oder von außen mit pgrep -f.

Besonders nützlich ist die zweite Zeile: Sie startet das Programm, lässt es normal durchlaufen und wirft einen erst dann in den Debugger, wenn es abstürzt — mit allen lokalen Variablen genau so, wie sie im Moment des Fehlers waren (post mortem debugging). Umgekehrt ist PYTHONBREAKPOINT=0 der Schalter, mit dem man vergessene breakpoint()-Aufrufe entschärft, ohne den Code anzufassen; PYTHONBREAKPOINT=pudb.set_trace würde stattdessen einen anderen Debugger starten. Das ist der Sinn der Umleitung über breakpoint(), statt direkt pdb.set_trace() zu schreiben (PEP 553).

Drei weitere Werkzeuge gehören in dieselbe Ecke:

Wer pdb unbequem findet, hat zwei Auswege. Jede der in IDEs und Editoren besprochenen Umgebungen bringt einen grafischen Debugger mit, der auf demselben Mechanismus aufsitzt und Breakpoints, Variablen und Stack in eigenen Fenstern zeigt — der Preis ist, dass man ihn nicht über SSH auf einem Server benutzt. Und für die Lesbarkeit der Fehlermeldungen selbst genügt oft schon rich.traceback: Zwei Zeilen am Programmanfang, danach sind Tracebacks farbig, mit Syntax-Highlighting und mit den lokalen Variablen jedes Frames.

from rich.traceback import install

install(show_locals=True)

Exceptions im Debugger

Traceback

Während in Java die stack traces mit dem Fehler anfangen und dann der Stack weiter (nach unten) wächst, ist es in Python umgekehrt, und der Fehler steht als letztes im Traceback (chronologisch, most recent call last).

Häufig ist der konkrete Fehler, der ganz unten steht, innerhalb einer Bibliothek, deren Code man gar nicht unbedingt so gut kennt. Anstatt nun den Code der Bibliothek zu lesen, sucht man zunächst nach der Schnittstelle, also dem Code, der die Bibliothek aufruft -- den hier ist mutmaßlich eine Annahme verletzt (zumindest wenn wir erstmal davon ausgehen, dass der Fehler bei uns liegt und nicht bei der externen Bibliothek).

Beispiel: Indexfehler

Wir debuggen die folgende Methode:

def find_peaks(values):
    peaks = []
    for i in range(len(values)):
        current_val = values[i]
        next_val = values[i+1]  # <--- Oh!
        if current_val > next_val:
            peaks.append(current_val)
    return peaks

Diesen Code können wir ausführen, es wird ohne Probleme eine Methode find_peaks deklariert. Soweit, so gut. Wir rufen den Code auf:

data = [10, 50, 30, 20, 40]
print(find_peaks(data))
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 5, in find_peaks
IndexError: list index out of range

Zeile 5 ist die mit # <--- Oh! markierte Zeile. Der IndexError kann sich nur auf den Ausdruck values[i+1] beziehen, d.h. es gibt innerhalb der for-Schleife irgendwann ein i sodass i+1 außerhalb des halboffenen Intervalls [0,len(values))[0, \texttt{len(values)}) liegt. Das ist äquivalent dazu, dass i+1 >= len(values), also i >= len(values) - 1. Da die Schleife durch range(len(values)) iteriert, nimmt i die ganzzahligen Werte von 0 bis len(values)-1 einschließlich an. Für genau den letzten Wert gilt i = len(values) - 1, dabei tritt der Fehler auf.

Anstatt das selbst logisch zu analysieren, können wir natürlich an der fraglichen Stelle einfach print(i) aufrufen und somit sehr schnell empirisch den Fehler finden. Wenn wir uns nun vorstellen, dass wir nicht so leicht auf die Idee kämen, dass i das Problem ist, würden wir vielleicht mit breakpoint() als erste Zeile im Schleifenkörper arbeiten und dann in Ruhe alle lokalen Variablen inspizieren. In Ruhe von Hand mehrfach durch die Schleife gehen ist nicht so effizient, daher wäre vielleicht ein conditional breakpoint nützlich, etwa mit der Bedingung i >= len(values)-2, wenn man schon kapiert hat, dass das Problem gegen Ende der Schleife auftritt. Entweder bietet unsere IDE conditional breakpoints an, oder wir schreiben if(condition): breakpoint().

Der beste Fix für den hier vorgestellten Fehler ist eigentlich nicht, den Index zu korrigieren, bis zu dem iteriert wird - es ist, die Iteration semantisch sorgfältiger zu beschreiben; hier wäre es vorteilhafter, mit pairwise (oder einfacher zip) zu arbeiten und auf explizite Indices vollständig zu verzichten.

Fehlerbehandlung für den Kontrollfluss

Seit Python 3.11 sind sogenannte Zero-Cost Exceptions umgesetzt: Die interne Maschinerie wurde so optimiert, dass das Eintreten in einen try-Block praktisch keine Rechenzeit mehr kostet. Ein Performance-Overhead entsteht erst dann, wenn tatsächlich eine Exception geworfen wird. Dies macht das try..except-Muster als legitimes Kontrollfluss-Werkzeug attraktiv.

Fehlermeldungen in Python 3.14

In Python 3.9 wurde ein neuer Parser eingeführt, mit dessen Hilfe in Python 3.10, 3.11 und 3.12 kleinere Verbesserungen bei den Fehlermeldungen erreicht werden konnten. In 3.13 sind die Formatierungen (auf mehreren Zeilen) verbessert worden. In Python 3.14 wurden besondere Vorkehrungen getroffen für SyntaxError, ValueError und TypeError, sodass es gerade für Anfänger leichter ist, die Fehler zu beheben.

Daher ist es möglicherweise eine gute Idee, direkt mit Python 3.14 zu arbeiten, wenn man sich noch mit der Syntax schwer tut.

Logging

Logging (das Protokollieren von Programmabläufen) hilft enorm beim Debugging, weil man nicht erst mühsam Introspektion in den Programmablauf durch zusätzlichen Debug-Code benötigt, wenn die Logs schon aussagekräftig sind. Zudem lassen sich in Logfiles teilweise Muster erkennen, die man durch gezieltes Debuggen nicht notwendig gesehen hätte (wenn etwa eine Produktiv- von einer Testumgebung abweicht).

Genau dieser Schritt wird auf Übungsblatt 4 verlangt: Das CYOA-Adventure soll Logging statt print verwenden. Im Lebenszyklus eines jeden Projekts kommt irgendwann der Moment, an dem nackte print-Statements größtenteils durch spezifischere Logging-Direktiven ersetzt werden. Das ist eine Art von Refinement, bei der nun zusätzlich spezifiziert werden muss, auf welcher Ebene geloggt werden soll (Error, Warning, Info, Debug). Außerdem lässt sich Logging nicht-blockierend konfigurieren, während print ein Programm erheblich verlangsamen kann.

import logging
logger = logging.getLogger(__name__)
logging.basicConfig(filename='myapp.log', level=logging.DEBUG)

logger.info('Started')
x = 2*3
logger.warning(f"x is {x}, which is {'larger than' if x > 6 else 'smaller or equal to'} 6")
logger.info('Finished')

with open('myapp.log') as logfile:
    for i, line in enumerate(logfile):
            print(i, line.strip())
        
# Output:
# 0 INFO:__main__:Started
# 1 WARNING:__main__:x is 6, which is smaller or equal to 6
# 2 INFO:__main__:Finished

Wenn man in eine Datei loggt, kann diese irgendwann zu groß werden, weshalb Logfiles rotiert werden, z.B. mit einem RotatingFileHandler.

Es lässt sich auch nah am print-Modus bleiben, indem man keine Datei verwendet, sondern den Standard-Output. Das ist für Loglevel INFO der Default, wenn man keine Datei angibt.

Generell lässt sich der Output mit Templates formatieren:

import logging

FORMAT = '%(asctime)s %(clientip)-15s %(user)-8s %(message)s'
logging.basicConfig(format=FORMAT, level=logging.INFO)

logger = logging.getLogger(__name__)
extra_data = {'clientip': '192.168.0.1', 'user': 'someuser'}
logger.info('Something happens', extra = extra_data)
# Output: 2026-05-08 17:25:02,537 192.168.0.1     someuser Something happens

In der Fehlerbehandlung wollen wir die Stack Trace mitloggen, was exception automatisch macht:

import logging
logging.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO)
logger = logging.getLogger(__name__)

try:
    1 / 0
except ZeroDivisionError:
    logger.exception("Damn!")
finally:
    print("Weiter geht's")

# Output:
# 2026-05-08 17:27:37,853 - Damn!
# Traceback (most recent call last):
#  File "<stdin>", line 2, in <module>
# ZeroDivisionError: division by zero
# Weiter geht's

Das Paket logging aus der Standardbibliothek verhält sich ähnlich zu Log4J aus der Java-Welt, aber es gibt natürlich noch andere Logging-Bibliotheken für Python wie z.B. Loguru oder Structlog, was besonders dann nützlich ist, wenn man strukturierte Daten loggen möchte. Wenn man logging in einer Einzel-Prozess-Anwendung einsetzt, ist es thread-safe, d.h. mehrere Threads können auf dem selben Logfile schreiben. Mehrere Prozesse können zwar in dieselbe Datei schreiben, aber ohne Koordination vermischen sich die Zeilen; dafür gibt es QueueHandler und QueueListener aus logging.handlers.

Dynamische Code-Ausführung

Gelegentlich ist es beim Debugging oder in hochdynamischen Architekturen nötig, Quelltext erst zur Laufzeit zu evaluieren. Python bietet dafür drei Built-ins: eval, exec und compile.

x = 5
print(eval("x + 10")) # Output: 15
code_str = "print('Schöner Code')"
exec(code_str) # Output: Schöner Code

Man kann mit der vollen Methodensignatur exec(object, globals=None, locals=None) auch Variablen (also den Scope) mitgeben.

compile(source, filename, mode) übersetzt Quellcode in ein Code-Objekt. Der Parameter mode entscheidet über die Art der Übersetzung: 'eval' (für einzelne Ausdrücke), 'exec' (für Anweisungsblöcke) oder 'single' (für eine einzelne interaktive Anweisung). Das resultierende Objekt kann an eval oder exec übergeben werden. Dies ist performanter, wenn derselbe dynamische Code mehrfach ausgeführt werden soll, da der Parse- und Kompilierungsschritt nur einmalig anfällt.

Bytecode-Analyse und Introspektion

Um tiefer in die Funktionsweise des CPython-Interpreters zu blicken, etwa um unerklärlichen Performance-Flaschenhälsen auf den Grund zu gehen oder um zu lernen, was wirklich passiert, lässt sich der erzeugte Bytecode mit dem Disassembler dis analysieren.

import dis

def add(a, b):
    return a + b

dis.dis(add)
# Output:
#   3           RESUME                   0
#
#   4           LOAD_FAST_BORROW_LOAD_FAST_BORROW 1 (a, b)
#               BINARY_OP                0 (+)
#               RETURN_VALUE

Für den direkten programmatischen Zugriff auf diesen Low-Level-Zustand besitzt jedes Funktionsobjekt das Attribut __code__, welches das Metadaten- und Code-Objekt speichert. Die rohen Bytecode-Instruktionen (als Byte-String) lassen sich über __code__.co_code abrufen:

code = add.__code__
print(code.co_code)     # Output: b'\x80\x00W\x01,\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00#\x00'
print(code.co_varnames) # Output: ('a', 'b')

Auch diese Bytes sind versionsabhängig, und noch deutlicher als das Disassemblat: Unter 3.13 ist derselbe String zehn Bytes lang, unter 3.14 achtzehn. co_code ist der richtige Weg, um zu zeigen, dass es diese Ebene gibt — und der falsche, um etwas darauf aufzubauen.

Zusammenfassung und Kontrollfragen

  1. Debugging ist die wissenschaftliche Methode im Kleinen: Hypothese aufstellen, falsifizieren, Hypothese verengen — ein Debugger ist nur das Werkzeug, das diesen Zyklus beschleunigt.

  2. Die sieben Fehlerarten haben unterschiedliche Gegenmittel: Linter gegen syntaktische, Exception-Handling gegen Laufzeitfehler, isolierte Tests gegen Logikfehler, CI und Container gegen Umgebungsfehler.

  3. breakpoint() ist die Stelle, an der man in pdb landet, und PYTHONBREAKPOINT=0 entschärft vergessene Aufrufe, ohne den Code anzufassen.

  4. python -m pdb -c continue skript.py lässt das Programm normal laufen und hält erst beim Fehler — mit allen lokalen Variablen so, wie sie in diesem Moment waren.

  5. Logging ist print mit Pegel, Zeitstempel, Herkunft und einem Schalter; innerhalb eines except-Blocks ist logger.exception(...) die richtige Wahl, weil sie den Traceback mitnimmt.

Kontrollfragen

  1. Sie erhalten von einem Nutzer einen Traceback über zwanzig Zeilen. Wo fangen Sie an zu lesen, und warum ist die dort genannte Zeile oft nicht die, die Sie ändern müssen?

  2. Ein Programm rechnet eine halbe Stunde und stürzt dann ab. Mit welchem Aufruf starten Sie es beim nächsten Mal, damit Sie im Moment des Absturzes im Debugger landen, statt den Fehler mit print-Zeilen nachzustellen? Und welche Möglichkeit gibt es seit Python 3.14, wenn der Prozess noch läuft und sich nur aufgehängt hat?

  3. Nennen Sie drei Gründe aus diesem Kapitel, warum logging einem print überlegen ist, sobald ein Programm länger lebt als ein Wegwerf-Skript.