Abschnitt 19 · Lektion 4
Die Monatsübersicht
Meistens will man von einem Kontoauszug etwas über einen Monat wissen, etwa wohin das Geld im September gegangen ist. Eingelesen und eingeordnet ist inzwischen alles. Nur läuft der Auszug über drei Monate. Für die Antwort muss das Werkzeug also aussuchen, zusammenzählen und das Ergebnis ordentlich hinschreiben.
Zusammenzählen nach einer Kategorie kennst du aus Lektion 7.5, eine Tabelle mit tabulate aus Abschnitt 13. Neu ist, dass zwei Merkmale zusammenkommen und dass Geld in beide Richtungen fließt. Die Beispiele nehmen dafür das Haushaltsbuch, diesmal über ein ganzes Jahr.
Zwei Merkmale, ein Schlüssel
from datetime import date
from decimal import Decimal
# Ein Jahr Haushaltsbuch, Ausgaben mit Minus. Die 15 Euro im September 2026 hat
# Ole für seine Kinokarte zurückgezahlt.
EINTRAEGE = [
(date(2025, 9, 5), "Lebensmittel", Decimal("-48.20")),
(date(2025, 9, 20), "Freizeit", Decimal("-32.00")),
(date(2025, 10, 2), "Lebensmittel", Decimal("-51.75")),
(date(2026, 8, 28), "Lebensmittel", Decimal("-44.10")),
(date(2026, 9, 4), "Lebensmittel", Decimal("-56.30")),
(date(2026, 9, 12), "Freizeit", Decimal("-30.00")),
(date(2026, 9, 13), "Freizeit", Decimal("15.00")),
(date(2026, 9, 25), "Lebensmittel", Decimal("-21.85")),
] from haushalt import EINTRAEGE
summen = {}
for datum, kategorie, betrag in EINTRAEGE:
schluessel = (datum.strftime("%Y-%m"), kategorie)
summen[schluessel] = summen.get(schluessel, 0) + betrag
for schluessel, betrag in sorted(summen.items()):
monat, kategorie = schluessel
print(monat, kategorie, betrag) Gruppiert wird wie beim Zusammenzählen nach einer Kategorie. Nur besteht der Schlüssel jetzt aus zwei Teilen, dem Monat und der Kategorie.
Ein Tupel wie ("2026-09", "Lebensmittel") darf Schlüssel eines Dictionarys sein, weil es sich
anders als eine Liste nicht ändern lässt. Und es sortiert sich von selbst erst nach dem Monat und
dann nach der Kategorie. In der Schleife kommen die beiden Teile mit monat, kategorie = schluessel
wieder aus dem Tupel heraus.
Der Monat kommt aus dem Datum, mit strftime aus Lektion 10.4, diesmal mit "%Y-%m". Vorn steht
also das Jahr und dann der Monat mit zwei Stellen. So wird er auch als Text richtig sortiert, aus
demselben Grund wie das Datum mit dem Jahr vorn, das in Abschnitt 16 in SQLite stand.
Der Monat braucht sein Jahr
from datetime import date
from decimal import Decimal
# Ein Jahr Haushaltsbuch, Ausgaben mit Minus. Die 15 Euro im September 2026 hat
# Ole für seine Kinokarte zurückgezahlt.
EINTRAEGE = [
(date(2025, 9, 5), "Lebensmittel", Decimal("-48.20")),
(date(2025, 9, 20), "Freizeit", Decimal("-32.00")),
(date(2025, 10, 2), "Lebensmittel", Decimal("-51.75")),
(date(2026, 8, 28), "Lebensmittel", Decimal("-44.10")),
(date(2026, 9, 4), "Lebensmittel", Decimal("-56.30")),
(date(2026, 9, 12), "Freizeit", Decimal("-30.00")),
(date(2026, 9, 13), "Freizeit", Decimal("15.00")),
(date(2026, 9, 25), "Lebensmittel", Decimal("-21.85")),
] from haushalt import EINTRAEGE
summen = {}
for datum, kategorie, betrag in EINTRAEGE:
schluessel = (datum.month, kategorie)
summen[schluessel] = summen.get(schluessel, 0) + betrag
for schluessel, betrag in sorted(summen.items()):
monat, kategorie = schluessel
print(monat, kategorie, betrag) Dieser Fehler fällt erst auf, wenn die Daten über einen Jahreswechsel gehen. datum.month ist für
den September 2025 und den September 2026 dieselbe 9, und schon stehen die Lebensmittel zweier Jahre
zusammen in einer Zeile: -126.35 statt -78.15.
Im Programm sieht nichts falsch aus. Wer nur die Daten eines Jahres hat, merkt es nie. Wer zwei hat, wundert sich über einen ziemlich teuren September.
Ein Kontoauszug geht oft über den Jahreswechsel, und spätestens wenn du dir die Exporte eines Jahres
zusammenkopierst, hast du jeden Monat zweimal. Mit dem Jahr im Schlüssel kann das nicht passieren.
Beim Aussuchen ist es dasselbe. Gefragt wird, ob der ganze Text "2026-09" passt, nicht nur die 9.
Einnahmen zählen extra
from datetime import date
from decimal import Decimal
# Ein Jahr Haushaltsbuch, Ausgaben mit Minus. Die 15 Euro im September 2026 hat
# Ole für seine Kinokarte zurückgezahlt.
EINTRAEGE = [
(date(2025, 9, 5), "Lebensmittel", Decimal("-48.20")),
(date(2025, 9, 20), "Freizeit", Decimal("-32.00")),
(date(2025, 10, 2), "Lebensmittel", Decimal("-51.75")),
(date(2026, 8, 28), "Lebensmittel", Decimal("-44.10")),
(date(2026, 9, 4), "Lebensmittel", Decimal("-56.30")),
(date(2026, 9, 12), "Freizeit", Decimal("-30.00")),
(date(2026, 9, 13), "Freizeit", Decimal("15.00")),
(date(2026, 9, 25), "Lebensmittel", Decimal("-21.85")),
] from haushalt import EINTRAEGE
netto = {}
ausgaben = {}
einnahmen = 0
for datum, kategorie, betrag in EINTRAEGE:
if datum.strftime("%Y-%m") != "2026-09":
continue
netto[kategorie] = netto.get(kategorie, 0) - betrag
if betrag < 0:
ausgaben[kategorie] = ausgaben.get(kategorie, 0) - betrag
else:
einnahmen += betrag
print("Alles zusammengezählt:", netto)
print("Nur die Ausgaben:", ausgaben)
print("Dazu Einnahmen:", einnahmen) Im September 2026 hat Ole 15 Euro für seine Kinokarte zurückgezahlt, gebucht unter Freizeit. Wenn man alle Beträge einer Kategorie zusammenzählt, stehen bei Freizeit 15 Euro. Das stimmt aber nicht, denn es gingen 30 Euro hinaus und 15 kamen herein.
Für die Frage, wofür das Geld draufging, zählen nur die Ausgaben. Alles, was hereinkam, steht für sich. Am Kontoauszug ist es mit einer Erstattung der Nebenkosten genauso. Wer sie gegen die Miete rechnet, wohnt auf dem Papier billiger als in Wirklichkeit.
Die Trennung kostet ein if. Beträge unter null zählen zu den Ausgaben, alles andere zu den
Einnahmen. Die Ausgaben kommen dabei ohne Minus in die Summe, damit in der Übersicht keine negativen
Zahlen stehen. Was am Ende bleibt, ist dann einfach Einnahmen minus Ausgaben.
Beträge rechts in der Spalte
from decimal import Decimal
from tabulate import tabulate
def euro(betrag):
"""Schreibt einen Betrag deutsch, mit Tausenderpunkt: 1.234,50 €."""
text = f"{betrag:,.2f}"
return text.replace(",", "_").replace(".", ",").replace("_", ".") + " €"
zeilen = [
["Wohnen", euro(Decimal("1204.00"))],
["Lebensmittel", euro(Decimal("78.15"))],
["Freizeit", euro(Decimal("30.00"))],
]
print(tabulate(zeilen, headers=["Kategorie", "Ausgaben"], tablefmt="rounded_outline"))
print(tabulate(zeilen, headers=["Kategorie", "Ausgaben"], tablefmt="rounded_outline", colalign=("left", "right"))) In der ersten Tabelle mit tabulate standen die Beträge als Zahlen da, und tabulate hat sie von
selbst rechts ausgerichtet. Deutsch geschrieben mit euro() sind sie aber Text, und Text stellt
tabulate links. Dann stehen 1.204,00 € und 30,00 € mit dem Anfang untereinander, und die Stellen
lassen sich nicht mehr vergleichen.
Mit colalign gibst du für jede Spalte an, wohin sie gehört, "left" oder "right", in der
Reihenfolge der Spalten.
Ein Text statt einer Ausgabe
Die Übersicht in bericht.py gibt selbst nichts aus. Sie gibt einen Text zurück, und erst main.py
bringt ihn auf den Bildschirm. Das wirkt wie ein Umweg, hat aber zwei Gründe.
Einen Text kann ein Test prüfen, mit in oder ==. Bei einer Ausgabe auf dem Bildschirm geht das
nur über Umwege, und in der nächsten Lektion kommen die Tests. Außerdem lässt sich derselbe Text
später auch in eine Datei schreiben oder in eine Mail stecken, ohne dass uebersicht davon wissen
muss.
Mehrere Zeilen werden mit "\n".join(...) zu einem Text, so wie früher die Teile eines Satzes.
Was die Aufgabe will
In bericht.py wartet uebersicht(buchungen, monat). Die Funktion soll die Buchungen eines Monats
aussuchen, die Ausgaben je Kategorie zusammenzählen und die Einnahmen für sich, und dann alles als
Text zurückgeben, mit einer Tabelle von tabulate in der Mitte.
Zum Mitnehmen
Zusammengefasst wird nach einem Merkmal oder nach mehreren, und bei mehreren ist der Schlüssel des Dictionarys ein Tupel. Weil ein Monat erst mit seinem Jahr eindeutig ist, steht dort strftime("%Y-%m") und nicht datum.month. Einnahmen und Ausgaben werden getrennt gezählt, damit keine Rückzahlung eine Ausgabe versteckt, und weil tabulate Text links stellt, brauchen deutsche Beträge colalign.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.