Math Engine, eval()-freier Ausdrucks-Interpreter für Python
Eine von Grund auf gebaute, sichere Auswertungs-Engine für mathematische Ausdrücke: Tokenizer, Recursive-Descent-Parser, AST, linearer Gleichungslöser und typsicheres Ausgabesystem, ganz ohne Pythons eval(). Live auf PyPI, 399 Tests, 90 % Coverage, grün über fünf Python-Versionen.
Die Herausforderung
Der naheliegende Weg, in Python einen Ausdruck wie 3 + 4 * 2 auszuwerten, ist eine einzige Zeile: eval("3 + 4 * 2"). Genau diese Zeile ist das Problem. eval() führt beliebigen Python-Code aus, eine als Zahleneingabe getarnte Zeichenkette wie __import__('os').system('rm -rf …') wird klaglos ausgeführt. Für jede Anwendung, die Ausdrücke aus einer Datei, einem Formularfeld, einer API oder einem Konfigurations-String entgegennimmt, ist eval() damit ein direkter Code-Execution-Vektor, kein Taschenrechner.
Der zweite, leisere Defekt ist Korrektheit. eval() und Pythons float rechnen binär: 0.1 + 0.2 ergibt 0.30000000000000004, 1/3 ist abgeschnitten, große Ganzzahlen kippen in wissenschaftliche Notation. Für einen Taschenrechner, eine Finanz-Formel oder einen Lehrkontext ist das nicht „fast richtig", sondern falsch.
Der dritte Defekt ist Diagnostik. Wer eval() einen kaputten Ausdruck gibt, bekommt einen Python-Traceback an einer internen Zeilennummer, nicht die Stelle im Eingabe-String, an der das Problem sitzt. Für ein Tool, das Endnutzer-Eingaben verarbeitet, ist das unbrauchbar.
Die Aufgabe also: eine vollständige Auswertungs-Engine von Grund auf, die (1) niemals fremden Code ausführt, (2) exakt statt binär-näherungsweise rechnet, (3) jeden Fehler zeichengenau lokalisiert, und (4) das alles in Bibliotheks-Qualität, getestet, dokumentiert, versioniert und auf PyPI installierbar. Nicht ein Wochenend-Parser, sondern eine Engine mit der Disziplin eines kleinen Compilers.
Die Umsetzung
eval()-frei per Konstruktion
Die gesamte Bibliothek ruft an keiner Stelle Pythons eval(), exec() oder compile() auf, das ist kein nachträglicher Filter, sondern die Architektur selbst. Eingabe-Strings durchlaufen eine geschlossene Pipeline (Input → Tokenizer → Parser → Evaluator/Solver → Formatter → Output-Converter), deren Alphabet ein endliches Set aus Zahlen, Operatoren, Klammern und einer Whitelist von Funktionsnamen ist. Ein angreifergesteuerter String kann im schlimmsten Fall einen typisierten MathError auslösen, niemals Code-Ausführung. Selbst die einzige Stelle, die eine vom Nutzer gelieferte Datenstruktur parst, nutzt das sichere ast.literal_eval, das ausschließlich Literale akzeptiert.
Recursive-Descent-Parser mit 10-Ebenen-Präzedenzkette
Operator-Präzedenz wird nicht per Regex oder Shunting-Yard-Tabelle hineingehackt, sondern strukturell als zehn ineinander geschachtelte Parser-Closures kodiert, jede mit genau einer Präzedenzebene: von parse_gleichung (=) über bitweise Operatoren, Schiebeoperationen, Summe und Term bis zu parse_power (**) und parse_factor. Links- vs. Rechtsassoziativität fällt aus der Struktur: wer in einer Schleife konsumiert, ist linksassoziativ (a - b - c = (a - b) - c); parse_power rekursiert nach rechts und macht ** korrekt rechtsassoziativ. Eine bewusste Entscheidung: ^ ist Bitwise XOR, nicht Potenz, exakt wie in C und Python.
Decimal-Präzision mit dynamischem Scaling
Jede Zahl ist vom Tokenizer bis zur Ausgabe ein decimal.Decimal, nie ein float, deshalb ist 0.1 + 0.2 exakt 0.3. Die Präzision des Decimal-Kontexts wird pro Berechnung neu bestimmt (zwischen 100 und 10.000 Stellen, je nach Eingabe), dazu eine harte Eingabe-Obergrenze von 20.000 Stellen. Der Sinn: ein langes Ergebnis wird nie still abgeschnitten, ein kurzes verbraucht nie unnötig Speicher. Genau die Klasse Korrektheit, die float-basierte Rechner hier leise verlieren.
Zeichen-genaue Fehlerpositionierung
Parallel zur Token-Liste führt der Tokenizer eine Span-Liste: zu jedem Token ein (start_col, end_col, original_text)-Tripel. Jeder AST-Knoten und jeder MathError trägt position_start / position_end. Der Ertrag: ein Fehler sagt nicht „Syntaxfehler irgendwo", sondern zeigt auf das exakte Zeichen. Diese Buchführung ist der Grund, warum die Engine über eine API hinweg debugbar ist. Über ein einziges Setting (readable_error) schaltet dieselbe Positionsinfo zwischen zwei Verträgen um: typisierte Exceptions für die Bibliothek, eine visuelle Diagnostik mit ^-Zeiger unter der fehlerhaften Spalte für die Konsole.
Typisiertes, katalogisiertes Fehlersystem
Eine Basisklasse MathError plus genau sieben Domänen-Subklassen, darunter ein Katalog von 78 eindeutigen, vierstelligen Fehlercodes in neun Familien. Die Ziffern sind strukturiert: erste Ziffer = Familie, zweite = Komponente, Rest = laufende Nummer. Code 3008 heißt damit „Calculator-Familie, Kern-Parser, mehr als ein '.' in einer Zahl". Diese Codes werden bewusst nie umnummeriert, sie sind Vertrag gegenüber UI und externen Log-Parsern. Die öffentliche calculate()-Funktion kapselt die ganze Pipeline in einem geschichteten except-Block, sodass kein roher ZeroDivisionError oder ValueError je den Aufrufer erreicht, alles landet typisiert in der MathError-Hierarchie.
Mehr als ein Taschenrechner
Auf demselben AST sitzen zwei weitere Fähigkeiten. Enthält ein Ausdruck ein = und eine Variable, löst die Engine die lineare Gleichung symbolisch: jeder Knoten liefert ein (Faktor, Konstante)-Paar, der Solver bringt beide Seiten in die Form A·x + B = C·x + D und berechnet x. Nicht-Linearität wird strukturell abgefangen (Variable·Variable, Variable im Nenner, Variable im Exponenten), entartete Fälle sauber benannt („No Solution", „Inf. Solutions"). Dazu ein Programmer's-Calculator-Modus mit fester Wortbreite (8/16/32/64 Bit), Zweierkomplement und bitweisen Operatoren, sodass 127 + 1 im 8-Bit-signed-Modus korrekt zu -128 überläuft. Ein prefix-gesteuertes Output-System (dec:, int:, hex: …) bestimmt den Python-Rückgabetyp und verweigert verlustbehaftete Konvertierungen, statt still abzuschneiden.
Engineering Highlights & Test-Disziplin
Zuverlässigkeit war hier kein Feature, sondern die Daseinsberechtigung, eine sichere Engine, der man nicht vertrauen kann, ist nutzlos.
- 399 pytest-Tests, 90 % Coverage. Die Suite wurde von 234 auf 399 Tests erweitert, die Coverage von 69 % auf 90 % gehoben. Ein eigener Helper
assert_error_location(expr, code, start, end)prüft nicht nur, dass ein Ausdruck fehlschlägt, sondern dass er mit dem exakten Fehlercode an der exakten Zeichenposition fehlschlägt, die Positionsdaten sind selbst Teil des Test-Vertrags. - CI-Matrix über fünf Python-Versionen. GitHub Actions fährt die volle Suite bei jedem Push und Pull Request gegen Python 3.8, 3.9, 3.10, 3.11 und 3.12; der Coverage-Report geht an Codecov. Toter und Work-in-Progress-Code wird ehrlich aus der Coverage genommen, statt die Zahl zu schönen.
- Saubere Schichtung, gebrochene Zyklen. Klar getrennte Module (calculator / utility / cli / plugins); zirkuläre Importe werden über bewusst verzögerte Importe aufgelöst. Jede Klasse und Funktion trägt einen Docstring, eine eigenständige
DOCUMENTATION.mdhält Architektur, vollständige API, Parser-Interna und den kompletten Fehlercode-Katalog fest. - Bibliotheks-Qualität bei Auslieferung. Reines-Python-Wheel, drei Konsolen-Einstiegspunkte, exakt zwei Laufzeit-Abhängigkeiten (
rich,prompt_toolkit). Das interaktive REPL bietet persistente History und Tab-Completion. Sechs Minor-Releases (0.1.0 → 0.6.7) in rund fünf Monaten, durchgehend nach Semantic Versioning.
Das Ergebnis
- Live auf PyPI als
math-engine, installierbar perpip install math-engine, MIT-lizenziert, reines-Python-Wheel für Python 3.8+, mit drei Konsolen-Kommandos out of the box. - eval()-frei per Konstruktion. Geschlossenes Eingabe-Alphabet, der schlimmste Fall einer feindlichen Eingabe ist ein typisierter Fehler, niemals Code-Ausführung.
- 399 Tests, 90 % Coverage, grün über fünf Python-Versionen (3.8–3.12) bei jedem Push, mit Test-Fällen, die exakte Fehlercodes an exakten Zeichenpositionen festschreiben.
- Exakte Decimal-Arithmetik mit adaptiver Präzision (100 … 10.000 Stellen) und 20.000-Stellen-Eingabegrenze, keine stille Float-Drift, keine stille Trunkierung.
- Zeichengenaue Diagnostik: 78 Fehlercodes in neun Familien, achtklassige typisierte Exception-Hierarchie,
position_start/position_endan jedem Fehler. - Rund 4.200 LOC Produktionscode in sauber geschichteten Modulen, abgesichert durch ~2.400 LOC Tests, dazu eine vollständige technische Dokumentation und ein katalogisiertes Fehlersystem.
Weitere Projekte
Multi-Process Browser-Automation Framework
17k LOC Python-Framework für parallele, fehlertolerante Browser-Workflows, race-safe Worker-Koordination, Cross-Process Crash-Bridge und vollständige Operator-UX über Streamlit.
Book Lister AI
Desktop-App, die gebrauchte Bücher in unter 30 Sekunden scannt, per Gemini-Vision erfasst, live bepreist und bei eBay listet, +400 % Durchsatz.
Legacy-DB Reverse Engineering & Migration
1,47 Mio. Bauteile aus einer 1,2 GB passwortgeschützten Hersteller-Datenbank befreit und ins neue System migriert, null Regelverstöße, vollständig auditierbar.