TL;DR: {{ }} wird nur in f-Strings zu { } expandiert. In normalen Python-Strings bleiben {{ }} als literale zwei Klammern stehen. Wenn du Code für andere Sprachen generierst und Strings mischst, entsteht ungültiger Code — ohne jede Fehlermeldung.
Das Problem
Ein Script auf srv-r2d2.deathstar.lan verarbeitet Tickets aus einem Helpdesk-System. Dafür baut es Ruby-Code zusammen, der via docker exec ... rails runner ausgeführt wird.
Der Code holt neue Tickets aus der Datenbank:
ruby = (
"new_ids = Ticket::State.where(name: 'new').pluck(:id);"
"tickets = Ticket.where(state_id: new_ids)"
".where('processed IS NULL OR processed = FALSE')"
f".order(created_at: :asc).limit({MAX_TICKETS});"
"puts tickets.map {{ |t| {{id: t.id, title: t.title}} }}.to_json"
)
Der Rails-Runner wird aufgerufen. Kein Fehler in Python. Das Script gibt aus: „Keine neuen Tickets." — obwohl definitiv welche in der Datenbank sind.
Die Diagnose
Schritt 1: Die Datenbank direkt befragen — Rails-Runner manuell ausführen. Findet 2 Tickets.
Schritt 2: Die Python-Parsing-Logik isoliert testen — gibt korrekte Ergebnisse zurück.
Schritt 3: Den generierten Ruby-Code ausgeben, der tatsächlich an den Runner übergeben wird:
print(repr(ruby))
Ausgabe:
"...puts tickets.map {{ |t| {{id: t.id, title: t.title}} }}.to_json"
Moment. {{ |t|? Das sind zwei öffnende geschweifte Klammern. Ungültiges Ruby.
Die Ursache
In Python-f-Strings ist {{ }} der Escape für eine literale geschweifte Klammer:
f"{{ }}" # → "{ }" (geschweifte Klammer, kein Ausdruck)
f"{name}" # → Wert von name
Aber: In normalen Strings (ohne f-Prefix) ist {{ }} einfach zwei Klammern — kein Escaping, keine Expansion:
"{{ }}" # → "{{ }}" (zwei Klammern!)
Im fehlerhaften Code ist nur eine Zeile ein f-String — nämlich die mit {MAX_TICKETS}:
f".order(created_at: :asc).limit({MAX_TICKETS});"
Die anderen Zeilen sind normale Strings. Die {{ }} in der letzten Zeile werden nicht expandiert und der Ruby-Code enthält literal {{ |t| {{...}} }} — was Ruby nicht versteht.
Rails runner gibt einen SyntaxError zurück, der Python-Code fängt ihn ab, gibt None zurück, und das Script sagt höflich „Keine neuen Tickets."
Die Lösung
Option 1: Den String als f-String markieren (auch wenn keine Python-Ausdrücke enthalten sind):
f"puts tickets.map {{ |t| {{id: t.id, title: t.title}} }}.to_json"
Jetzt sind {{ }} Escapes in einem f-String und werden zu { }.
Option 2: Direkt { } schreiben, da kein Python-Ausdruck nötig ist:
"puts tickets.map { |t| {id: t.id, title: t.title} }.to_json"
Simpler und eindeutiger.
Option 3: Den ganzen Ruby-Code in eine separate Datei auslagern statt ihn als String zu bauen. Dann entfällt das Problem komplett — aber das ist ein größerer Refactor.
Warum ist das so tückisch?
Normalerweise wirft Python sofort einen Fehler wenn etwas nicht stimmt. Hier passiert das nicht, weil:
- Python generiert den Code korrekt aus seiner Perspektive —
{{ }}in einem normalen String ist vollkommen gültig - Der Fehler liegt im generierten Code, nicht in Python selbst
- Der Ruby-SyntaxError kommt als Return-Code 1 von
subprocess.runzurück — aber der Code prüft den Return-Code nicht, er schaut nur auf den stdout-Output - Bei leerem stdout wird stillschweigend
Nonezurückgegeben
Das Symptom — „findet keine Tickets" — ist beliebig weit von der Ursache entfernt.
Lessons Learned
1. Beim Code-Generieren den Output immer prüfen
print("Generated code:", repr(generated_code))
Das sollte in der Entwicklungsphase immer aktiv sein. Für Debugging:
python3 -c "from myscript import build_ruby_query; print(build_ruby_query())"
2. Return-Code von Subprocessen prüfen
result = subprocess.run([...], capture_output=True, text=True)
if result.returncode != 0:
log.error(f"Rails runner failed: {result.stderr}")
return None
Das hätte sofort auf den SyntaxError hingewiesen.
3. Gemischte f-String / non-f-String Strings sind fehleranfällig
Wenn ein Code-Template über mehrere verkettete Strings gebaut wird und nur ein Teil ein f-String ist, ist das eine Falle. Entweder:
- Alle Teile als f-Strings kennzeichnen
- Oder
str.format()/ Template-Strings verwenden - Oder direkte
{ }ohne Escaping schreiben
4. Die einfachste Lösung ist oft die beste
Im konkreten Fall war die Zeile "puts tickets.map { |t| {id: t.id} }.to_json" mit einfachen { } die klarste Lösung — weil hier gar keine Python-Variable interpoliert werden musste.
Checkliste: Python-Code-Generierung
- Enthält der generierte Code
{{ }}oder}}? → Prüfen ob der String ein f-String ist -
subprocess.run→ Return-Code auswerten -
stderrmitloggen wennreturncode != 0 - Generierten Code im Debug-Modus ausgeben
- Wenn möglich: Code in Datei auslagern statt als String bauen