Zum Inhalt springen
Python Fortgeschritten 5 min

Projekt: Kontaktbuch in Python

Erstelle ein vollständiges Kontaktbuch mit CRUD-Operationen, JSON-Speicherung, Validierung und CSV-Export - ein praxisnahes Python-Projekt

Aktualisiert:

Projekt: Kontaktbuch in Python

In diesem Projekt bauen wir ein vollständiges Kontaktbuch, das Kontakte verwalten, in Dateien speichern und exportieren kann. Du lernst dabei, wie man ein reales Programm mit sauberer Architektur entwickelt.

Projektübersicht und Ziele

Unser Kontaktbuch wird folgende Funktionen bieten:

  • CRUD-Operationen: Kontakte erstellen, anzeigen, bearbeiten und löschen
  • Suche und Filterung: Kontakte nach Name, E-Mail oder Telefon finden
  • Persistente Speicherung: Daten in einer JSON-Datei speichern und laden
  • Validierung: E-Mail-Adressen und Telefonnummern prüfen
  • Import/Export: Kontakte als CSV-Datei importieren und exportieren
  • OOP-Struktur: Sauberer Code mit Klassen

Schritt 1: Kontakt-Datenstruktur

Wir definieren zuerst, welche Informationen ein Kontakt haben soll:

# Ein Kontakt als Dictionary
kontakt_beispiel = {
    "vorname": "Max",
    "nachname": "Mustermann",
    "telefon": "0171-1234567",
    "email": "max@example.com",
    "adresse": "Musterstraße 1, 12345 Berlin",
    "notizen": "Kollege aus der Arbeit"
}

Mit einer Funktion können wir neue Kontakte erstellen:

def kontakt_erstellen():
    """Erstellt einen neuen Kontakt durch Benutzereingabe."""
    print("\n--- Neuen Kontakt erstellen ---")

    vorname = input("Vorname: ").strip()
    nachname = input("Nachname: ").strip()
    telefon = input("Telefon: ").strip()
    email = input("E-Mail: ").strip()
    adresse = input("Adresse: ").strip()
    notizen = input("Notizen: ").strip()

    kontakt = {
        "vorname": vorname,
        "nachname": nachname,
        "telefon": telefon,
        "email": email,
        "adresse": adresse,
        "notizen": notizen
    }

    return kontakt

Schritt 2: CRUD-Operationen

CRUD steht für Create, Read, Update, Delete — die vier grundlegenden Operationen für Datenverwaltung:

# Unsere Kontaktliste
kontakte = []

# CREATE -- Kontakt hinzufügen
def kontakt_hinzufuegen(kontakt):
    """Fügt einen Kontakt zur Liste hinzu."""
    kontakte.append(kontakt)
    print(f"\n  Kontakt '{kontakt['vorname']} {kontakt['nachname']}' "
          f"wurde hinzugefügt.")

# READ -- Alle Kontakte anzeigen
def alle_kontakte_anzeigen():
    """Zeigt alle Kontakte übersichtlich an."""
    if not kontakte:
        print("\n  Keine Kontakte vorhanden.")
        return

    print(f"\n{'=' * 60}")
    print(f"  KONTAKTBUCH ({len(kontakte)} Kontakte)")
    print(f"{'=' * 60}")

    for i, k in enumerate(kontakte, 1):
        print(f"\n  [{i}] {k['vorname']} {k['nachname']}")
        if k["telefon"]:
            print(f"      Tel:    {k['telefon']}")
        if k["email"]:
            print(f"      E-Mail: {k['email']}")
        if k["adresse"]:
            print(f"      Adresse: {k['adresse']}")
        if k["notizen"]:
            print(f"      Notizen: {k['notizen']}")
    print(f"\n{'=' * 60}")

# READ -- Einzelnen Kontakt anzeigen
def kontakt_details(index):
    """Zeigt die Details eines einzelnen Kontakts."""
    if 0 <= index < len(kontakte):
        k = kontakte[index]
        print(f"\n{'─' * 40}")
        print(f"  {k['vorname']} {k['nachname']}")
        print(f"{'─' * 40}")
        print(f"  Telefon:  {k['telefon'] or '---'}")
        print(f"  E-Mail:   {k['email'] or '---'}")
        print(f"  Adresse:  {k['adresse'] or '---'}")
        print(f"  Notizen:  {k['notizen'] or '---'}")
        print(f"{'─' * 40}")
    else:
        print("  Ungültiger Index!")

# UPDATE -- Kontakt bearbeiten
def kontakt_bearbeiten(index):
    """Bearbeitet einen bestehenden Kontakt."""
    if not (0 <= index < len(kontakte)):
        print("  Ungültiger Index!")
        return

    k = kontakte[index]
    print(f"\n--- Kontakt bearbeiten: {k['vorname']} {k['nachname']} ---")
    print("  (Leer lassen, um den Wert beizubehalten)")

    felder = ["vorname", "nachname", "telefon", "email", "adresse", "notizen"]
    bezeichnungen = ["Vorname", "Nachname", "Telefon", "E-Mail", "Adresse", "Notizen"]

    for feld, bezeichnung in zip(felder, bezeichnungen):
        aktuell = k[feld]
        neuer_wert = input(f"  {bezeichnung} [{aktuell}]: ").strip()
        if neuer_wert:
            k[feld] = neuer_wert

    print(f"\n  Kontakt wurde aktualisiert.")

# DELETE -- Kontakt löschen
def kontakt_loeschen(index):
    """Löscht einen Kontakt nach Bestätigung."""
    if not (0 <= index < len(kontakte)):
        print("  Ungültiger Index!")
        return

    k = kontakte[index]
    name = f"{k['vorname']} {k['nachname']}"

    bestaetigung = input(f"  '{name}' wirklich löschen? (j/n): ").strip().lower()
    if bestaetigung == "j":
        kontakte.pop(index)
        print(f"  '{name}' wurde gelöscht.")
    else:
        print("  Löschung abgebrochen.")

Jede CRUD-Operation ist eine eigene Funktion mit klarer Verantwortung.


Schritt 3: Menü-System

Das Menü verbindet alle Funktionen in einer benutzerfreundlichen Schleife:

def kontakt_auswahl(aktion_text="auswählen"):
    """Lässt den Benutzer einen Kontakt per Nummer auswählen."""
    if not kontakte:
        print("\n  Keine Kontakte vorhanden.")
        return -1

    alle_kontakte_anzeigen()

    try:
        nummer = int(input(f"\n  Kontakt-Nr. zum {aktion_text}: ")) - 1
        if 0 <= nummer < len(kontakte):
            return nummer
        print("  Ungültige Nummer!")
        return -1
    except ValueError:
        print("  Bitte gib eine Zahl ein!")
        return -1


def hauptmenue():
    """Zeigt das Hauptmenü und verarbeitet die Eingabe."""
    while True:
        print(f"\n{'=' * 40}")
        print(f"    KONTAKTBUCH")
        print(f"{'=' * 40}")
        print("  1. Neuen Kontakt erstellen")
        print("  2. Alle Kontakte anzeigen")
        print("  3. Kontakt-Details anzeigen")
        print("  4. Kontakt bearbeiten")
        print("  5. Kontakt löschen")
        print("  6. Kontakt suchen")
        print("  7. Speichern")
        print("  8. Laden")
        print("  9. Als CSV exportieren")
        print("  0. Beenden")
        print(f"{'=' * 40}")

        auswahl = input("Deine Wahl: ").strip()

        if auswahl == "1":
            neuer_kontakt = kontakt_erstellen()
            kontakt_hinzufuegen(neuer_kontakt)
        elif auswahl == "2":
            alle_kontakte_anzeigen()
        elif auswahl == "3":
            idx = kontakt_auswahl("anzeigen")
            if idx >= 0:
                kontakt_details(idx)
        elif auswahl == "4":
            idx = kontakt_auswahl("bearbeiten")
            if idx >= 0:
                kontakt_bearbeiten(idx)
        elif auswahl == "5":
            idx = kontakt_auswahl("löschen")
            if idx >= 0:
                kontakt_loeschen(idx)
        elif auswahl == "6":
            kontakt_suchen()
        elif auswahl == "7":
            kontakte_speichern()
        elif auswahl == "8":
            kontakte_laden()
        elif auswahl == "9":
            als_csv_exportieren()
        elif auswahl == "0":
            print("\nAuf Wiedersehen!")
            break
        else:
            print("  Ungültige Auswahl!")

Schritt 4: Suche und Filterung

Benutzer sollen Kontakte schnell finden können:

def kontakt_suchen():
    """Sucht Kontakte nach einem Suchbegriff."""
    suchbegriff = input("\n  Suchbegriff eingeben: ").strip().lower()

    if not suchbegriff:
        print("  Bitte gib einen Suchbegriff ein.")
        return

    treffer = []
    for i, k in enumerate(kontakte):
        # In allen Feldern suchen
        durchsuchbar = f"{k['vorname']} {k['nachname']} {k['telefon']} " \
                       f"{k['email']} {k['adresse']} {k['notizen']}"
        if suchbegriff in durchsuchbar.lower():
            treffer.append((i, k))

    if not treffer:
        print(f"  Keine Kontakte für '{suchbegriff}' gefunden.")
        return

    print(f"\n  {len(treffer)} Treffer für '{suchbegriff}':")
    print(f"  {'─' * 45}")
    for idx, k in treffer:
        print(f"  [{idx + 1}] {k['vorname']} {k['nachname']} - "
              f"{k['telefon']} - {k['email']}")

Die Suche ist case-insensitive und durchsucht alle Felder des Kontakts. So findet man einen Kontakt sowohl über den Namen als auch über die E-Mail oder Telefonnummer.


Schritt 5: Daten in JSON-Datei speichern und laden

Damit die Kontakte nach dem Programmende erhalten bleiben, speichern wir sie als JSON:

import json
import os

DATEINAME = "kontakte.json"

def kontakte_speichern(dateipfad=DATEINAME):
    """Speichert alle Kontakte in eine JSON-Datei."""
    try:
        with open(dateipfad, "w", encoding="utf-8") as datei:
            json.dump(kontakte, datei, indent=2, ensure_ascii=False)
        print(f"\n  {len(kontakte)} Kontakte in '{dateipfad}' gespeichert.")
    except IOError as e:
        print(f"\n  Fehler beim Speichern: {e}")

def kontakte_laden(dateipfad=DATEINAME):
    """Lädt Kontakte aus einer JSON-Datei."""
    global kontakte

    if not os.path.exists(dateipfad):
        print(f"\n  Datei '{dateipfad}' nicht gefunden.")
        return

    try:
        with open(dateipfad, "r", encoding="utf-8") as datei:
            geladene = json.load(datei)

        if not isinstance(geladene, list):
            print("  Ungültiges Dateiformat!")
            return

        kontakte = geladene
        print(f"\n  {len(kontakte)} Kontakte aus '{dateipfad}' geladen.")
    except json.JSONDecodeError:
        print("  Fehler: Datei enthält ungültiges JSON!")
    except IOError as e:
        print(f"  Fehler beim Laden: {e}")

So sieht die JSON-Datei aus:

[
  {
    "vorname": "Max",
    "nachname": "Mustermann",
    "telefon": "0171-1234567",
    "email": "max@example.com",
    "adresse": "Musterstraße 1, 12345 Berlin",
    "notizen": "Kollege"
  },
  {
    "vorname": "Anna",
    "nachname": "Schmidt",
    "telefon": "0172-9876543",
    "email": "anna.schmidt@example.com",
    "adresse": "Hauptstraße 42, 80331 München",
    "notizen": ""
  }
]

Schritt 6: OOP-Refactoring

Jetzt organisieren wir den Code mit Klassen. Eine Contact-Klasse für einzelne Kontakte und eine AddressBook-Klasse für die Verwaltung:

import json
import os
import csv
import re


class Contact:
    """Repräsentiert einen einzelnen Kontakt."""

    def __init__(self, vorname, nachname, telefon="", email="",
                 adresse="", notizen=""):
        self.vorname = vorname
        self.nachname = nachname
        self.telefon = telefon
        self.email = email
        self.adresse = adresse
        self.notizen = notizen

    @property
    def voller_name(self):
        """Gibt den vollen Namen zurück."""
        return f"{self.vorname} {self.nachname}"

    def to_dict(self):
        """Konvertiert den Kontakt in ein Dictionary."""
        return {
            "vorname": self.vorname,
            "nachname": self.nachname,
            "telefon": self.telefon,
            "email": self.email,
            "adresse": self.adresse,
            "notizen": self.notizen
        }

    @classmethod
    def from_dict(cls, daten):
        """Erstellt einen Kontakt aus einem Dictionary."""
        return cls(
            vorname=daten.get("vorname", ""),
            nachname=daten.get("nachname", ""),
            telefon=daten.get("telefon", ""),
            email=daten.get("email", ""),
            adresse=daten.get("adresse", ""),
            notizen=daten.get("notizen", "")
        )

    def __str__(self):
        return f"{self.voller_name} | {self.telefon} | {self.email}"

    def __repr__(self):
        return f"Contact('{self.vorname}', '{self.nachname}')"


class AddressBook:
    """Verwaltet eine Sammlung von Kontakten."""

    def __init__(self, dateipfad="kontakte.json"):
        self.kontakte = []
        self.dateipfad = dateipfad

    # --- CRUD-Operationen ---

    def hinzufuegen(self, kontakt):
        """Fügt einen neuen Kontakt hinzu."""
        self.kontakte.append(kontakt)
        print(f"  Kontakt '{kontakt.voller_name}' hinzugefügt.")

    def alle_anzeigen(self):
        """Zeigt alle Kontakte an."""
        if not self.kontakte:
            print("\n  Keine Kontakte vorhanden.")
            return

        print(f"\n{'=' * 60}")
        print(f"  KONTAKTBUCH ({len(self.kontakte)} Kontakte)")
        print(f"{'=' * 60}")

        for i, k in enumerate(self.kontakte, 1):
            print(f"  [{i}] {k.voller_name}")
            if k.telefon:
                print(f"      Tel:     {k.telefon}")
            if k.email:
                print(f"      E-Mail:  {k.email}")
            if k.adresse:
                print(f"      Adresse: {k.adresse}")
            if k.notizen:
                print(f"      Notizen: {k.notizen}")

        print(f"{'=' * 60}")

    def details(self, index):
        """Zeigt Details eines Kontakts."""
        k = self._kontakt_holen(index)
        if k is None:
            return

        print(f"\n{'─' * 40}")
        print(f"  {k.voller_name}")
        print(f"{'─' * 40}")
        print(f"  Telefon:  {k.telefon or '---'}")
        print(f"  E-Mail:   {k.email or '---'}")
        print(f"  Adresse:  {k.adresse or '---'}")
        print(f"  Notizen:  {k.notizen or '---'}")
        print(f"{'─' * 40}")

    def bearbeiten(self, index):
        """Bearbeitet einen Kontakt."""
        k = self._kontakt_holen(index)
        if k is None:
            return

        print(f"\n--- Bearbeiten: {k.voller_name} ---")
        print("  (Leer lassen = Wert beibehalten)")

        felder = [
            ("vorname", "Vorname"),
            ("nachname", "Nachname"),
            ("telefon", "Telefon"),
            ("email", "E-Mail"),
            ("adresse", "Adresse"),
            ("notizen", "Notizen"),
        ]

        for attr, label in felder:
            aktuell = getattr(k, attr)
            neuer_wert = input(f"  {label} [{aktuell}]: ").strip()
            if neuer_wert:
                setattr(k, attr, neuer_wert)

        print(f"  Kontakt aktualisiert.")

    def loeschen(self, index):
        """Löscht einen Kontakt nach Bestätigung."""
        k = self._kontakt_holen(index)
        if k is None:
            return

        antwort = input(f"  '{k.voller_name}' löschen? (j/n): ").strip().lower()
        if antwort == "j":
            self.kontakte.pop(index)
            print(f"  '{k.voller_name}' gelöscht.")
        else:
            print("  Abgebrochen.")

    def _kontakt_holen(self, index):
        """Gibt den Kontakt am Index zurück oder None."""
        if 0 <= index < len(self.kontakte):
            return self.kontakte[index]
        print("  Ungültiger Index!")
        return None

    # --- Suche ---

    def suchen(self, suchbegriff):
        """Durchsucht alle Kontakte nach einem Begriff."""
        suchbegriff = suchbegriff.lower()
        treffer = []

        for i, k in enumerate(self.kontakte):
            text = f"{k.voller_name} {k.telefon} {k.email} " \
                   f"{k.adresse} {k.notizen}".lower()
            if suchbegriff in text:
                treffer.append((i, k))

        return treffer

    # --- Dateioperationen ---

    def speichern(self):
        """Speichert alle Kontakte als JSON."""
        daten = [k.to_dict() for k in self.kontakte]
        try:
            with open(self.dateipfad, "w", encoding="utf-8") as f:
                json.dump(daten, f, indent=2, ensure_ascii=False)
            print(f"  {len(self.kontakte)} Kontakte gespeichert.")
        except IOError as e:
            print(f"  Fehler beim Speichern: {e}")

    def laden(self):
        """Lädt Kontakte aus JSON-Datei."""
        if not os.path.exists(self.dateipfad):
            print(f"  Datei '{self.dateipfad}' nicht gefunden.")
            return

        try:
            with open(self.dateipfad, "r", encoding="utf-8") as f:
                daten = json.load(f)
            self.kontakte = [Contact.from_dict(d) for d in daten]
            print(f"  {len(self.kontakte)} Kontakte geladen.")
        except (json.JSONDecodeError, IOError) as e:
            print(f"  Fehler beim Laden: {e}")

    # --- Sortierung ---

    def sortieren(self, nach="nachname"):
        """Sortiert die Kontakte nach einem Feld."""
        self.kontakte.sort(key=lambda k: getattr(k, nach, "").lower())
        print(f"  Kontakte nach '{nach}' sortiert.")

Die OOP-Version hat klare Vorteile: Jeder Kontakt ist ein Objekt mit eigenen Methoden, und das AddressBook kapselt die gesamte Verwaltungslogik.


Schritt 7: Validierung

Bevor wir Kontakte speichern, sollten wir die Eingaben validieren:

import re

class Validator:
    """Validiert Kontakt-Eingaben."""

    @staticmethod
    def email_validieren(email):
        """Prüft, ob eine E-Mail-Adresse gültig ist."""
        if not email:
            return True  # Leere E-Mail ist ok (optional)

        # Einfaches E-Mail-Muster
        muster = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
        if re.match(muster, email):
            return True
        return False

    @staticmethod
    def telefon_validieren(telefon):
        """Prüft, ob eine Telefonnummer gültig ist."""
        if not telefon:
            return True  # Leere Nummer ist ok (optional)

        # Erlaubte Zeichen: Ziffern, +, -, Leerzeichen, Klammern
        bereinigt = re.sub(r'[\s\-\(\)\+/]', '', telefon)
        if bereinigt.isdigit() and len(bereinigt) >= 4:
            return True
        return False

    @staticmethod
    def name_validieren(name):
        """Prüft, ob ein Name gültig ist."""
        if not name or len(name.strip()) < 1:
            return False
        return True

    @classmethod
    def kontakt_validieren(cls, vorname, nachname, telefon="", email=""):
        """Validiert alle Kontaktfelder und gibt Fehlerliste zurück."""
        fehler = []

        if not cls.name_validieren(vorname):
            fehler.append("Vorname darf nicht leer sein.")
        if not cls.name_validieren(nachname):
            fehler.append("Nachname darf nicht leer sein.")
        if not cls.telefon_validieren(telefon):
            fehler.append("Ungültige Telefonnummer.")
        if not cls.email_validieren(email):
            fehler.append("Ungültige E-Mail-Adresse.")

        return fehler

Jetzt integrieren wir die Validierung in die Kontakterstellung:

def kontakt_erstellen_validiert():
    """Erstellt einen neuen Kontakt mit Validierung."""
    print("\n--- Neuen Kontakt erstellen ---")

    while True:
        vorname = input("Vorname*: ").strip()
        nachname = input("Nachname*: ").strip()
        telefon = input("Telefon: ").strip()
        email = input("E-Mail: ").strip()
        adresse = input("Adresse: ").strip()
        notizen = input("Notizen: ").strip()

        fehler = Validator.kontakt_validieren(vorname, nachname, telefon, email)

        if fehler:
            print("\n  Fehler bei der Eingabe:")
            for f in fehler:
                print(f"    - {f}")
            wiederholen = input("\n  Nochmal versuchen? (j/n): ").strip().lower()
            if wiederholen != "j":
                return None
        else:
            return Contact(vorname, nachname, telefon, email, adresse, notizen)

Schritt 8: Import/Export als CSV

CSV-Dateien können in Excel oder Google Sheets geöffnet werden — sehr praktisch für den Datenaustausch:

import csv

def als_csv_exportieren(kontakte_liste, dateipfad="kontakte.csv"):
    """Exportiert alle Kontakte als CSV-Datei."""
    if not kontakte_liste:
        print("  Keine Kontakte zum Exportieren.")
        return

    felder = ["vorname", "nachname", "telefon", "email", "adresse", "notizen"]

    try:
        with open(dateipfad, "w", newline="", encoding="utf-8-sig") as f:
            writer = csv.DictWriter(f, fieldnames=felder, delimiter=";")
            writer.writeheader()
            for k in kontakte_liste:
                if isinstance(k, Contact):
                    writer.writerow(k.to_dict())
                else:
                    writer.writerow(k)

        print(f"  {len(kontakte_liste)} Kontakte nach '{dateipfad}' exportiert.")
    except IOError as e:
        print(f"  Fehler beim Exportieren: {e}")


def aus_csv_importieren(dateipfad="kontakte.csv"):
    """Importiert Kontakte aus einer CSV-Datei."""
    if not os.path.exists(dateipfad):
        print(f"  Datei '{dateipfad}' nicht gefunden.")
        return []

    importierte = []
    try:
        with open(dateipfad, "r", encoding="utf-8-sig") as f:
            reader = csv.DictReader(f, delimiter=";")
            for zeile in reader:
                kontakt = Contact.from_dict(zeile)
                importierte.append(kontakt)

        print(f"  {len(importierte)} Kontakte aus '{dateipfad}' importiert.")
        return importierte
    except (IOError, csv.Error) as e:
        print(f"  Fehler beim Importieren: {e}")
        return []

Warum utf-8-sig? Das Byte Order Mark (BOM) sorgt dafür, dass Excel die Datei korrekt mit Umlauten öffnet. Und das Semikolon als Trennzeichen ist in deutschsprachigen Excel-Versionen Standard.


Vollständiger Code

Hier ist das komplette Programm mit der OOP-Struktur:

import json
import os
import csv
import re


class Contact:
    """Repräsentiert einen einzelnen Kontakt."""

    def __init__(self, vorname, nachname, telefon="", email="",
                 adresse="", notizen=""):
        self.vorname = vorname
        self.nachname = nachname
        self.telefon = telefon
        self.email = email
        self.adresse = adresse
        self.notizen = notizen

    @property
    def voller_name(self):
        return f"{self.vorname} {self.nachname}"

    def to_dict(self):
        return {
            "vorname": self.vorname,
            "nachname": self.nachname,
            "telefon": self.telefon,
            "email": self.email,
            "adresse": self.adresse,
            "notizen": self.notizen
        }

    @classmethod
    def from_dict(cls, daten):
        return cls(**{k: daten.get(k, "") for k in
                      ["vorname", "nachname", "telefon", "email",
                       "adresse", "notizen"]})

    def __str__(self):
        return f"{self.voller_name} | {self.telefon} | {self.email}"


class Validator:
    """Validiert Kontakt-Eingaben."""

    @staticmethod
    def email_validieren(email):
        if not email:
            return True
        return bool(re.match(
            r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', email
        ))

    @staticmethod
    def telefon_validieren(telefon):
        if not telefon:
            return True
        bereinigt = re.sub(r'[\s\-\(\)\+/]', '', telefon)
        return bereinigt.isdigit() and len(bereinigt) >= 4

    @staticmethod
    def name_validieren(name):
        return bool(name and name.strip())

    @classmethod
    def kontakt_validieren(cls, vorname, nachname, telefon="", email=""):
        fehler = []
        if not cls.name_validieren(vorname):
            fehler.append("Vorname darf nicht leer sein.")
        if not cls.name_validieren(nachname):
            fehler.append("Nachname darf nicht leer sein.")
        if not cls.telefon_validieren(telefon):
            fehler.append("Ungültige Telefonnummer.")
        if not cls.email_validieren(email):
            fehler.append("Ungültige E-Mail-Adresse.")
        return fehler


class AddressBook:
    """Verwaltet eine Sammlung von Kontakten."""

    def __init__(self, dateipfad="kontakte.json"):
        self.kontakte = []
        self.dateipfad = dateipfad

    def hinzufuegen(self, kontakt):
        self.kontakte.append(kontakt)

    def bearbeiten(self, index):
        k = self._kontakt_holen(index)
        if not k:
            return
        print(f"\n--- Bearbeiten: {k.voller_name} ---")
        print("  (Leer lassen = beibehalten)")
        for attr, label in [("vorname", "Vorname"), ("nachname", "Nachname"),
                            ("telefon", "Telefon"), ("email", "E-Mail"),
                            ("adresse", "Adresse"), ("notizen", "Notizen")]:
            aktuell = getattr(k, attr)
            neu = input(f"  {label} [{aktuell}]: ").strip()
            if neu:
                setattr(k, attr, neu)
        print("  Kontakt aktualisiert.")

    def loeschen(self, index):
        k = self._kontakt_holen(index)
        if not k:
            return
        if input(f"  '{k.voller_name}' löschen? (j/n): ").lower() == "j":
            self.kontakte.pop(index)
            print(f"  Gelöscht.")

    def suchen(self, begriff):
        begriff = begriff.lower()
        return [(i, k) for i, k in enumerate(self.kontakte)
                if begriff in str(k).lower() or
                begriff in k.adresse.lower() or
                begriff in k.notizen.lower()]

    def sortieren(self, nach="nachname"):
        self.kontakte.sort(key=lambda k: getattr(k, nach, "").lower())

    def speichern(self):
        daten = [k.to_dict() for k in self.kontakte]
        with open(self.dateipfad, "w", encoding="utf-8") as f:
            json.dump(daten, f, indent=2, ensure_ascii=False)
        print(f"  {len(self.kontakte)} Kontakte gespeichert.")

    def laden(self):
        if not os.path.exists(self.dateipfad):
            return
        with open(self.dateipfad, "r", encoding="utf-8") as f:
            daten = json.load(f)
        self.kontakte = [Contact.from_dict(d) for d in daten]
        print(f"  {len(self.kontakte)} Kontakte geladen.")

    def als_csv_exportieren(self, dateipfad="kontakte.csv"):
        felder = ["vorname", "nachname", "telefon", "email",
                  "adresse", "notizen"]
        with open(dateipfad, "w", newline="", encoding="utf-8-sig") as f:
            writer = csv.DictWriter(f, fieldnames=felder, delimiter=";")
            writer.writeheader()
            for k in self.kontakte:
                writer.writerow(k.to_dict())
        print(f"  {len(self.kontakte)} Kontakte als CSV exportiert.")

    def aus_csv_importieren(self, dateipfad="kontakte.csv"):
        if not os.path.exists(dateipfad):
            print(f"  Datei nicht gefunden.")
            return
        with open(dateipfad, "r", encoding="utf-8-sig") as f:
            reader = csv.DictReader(f, delimiter=";")
            for zeile in reader:
                self.kontakte.append(Contact.from_dict(zeile))
        print(f"  Kontakte importiert. Gesamt: {len(self.kontakte)}")

    def alle_anzeigen(self):
        if not self.kontakte:
            print("\n  Keine Kontakte.")
            return
        print(f"\n{'=' * 60}")
        print(f"  KONTAKTBUCH ({len(self.kontakte)} Kontakte)")
        print(f"{'=' * 60}")
        for i, k in enumerate(self.kontakte, 1):
            print(f"  [{i}] {k}")
        print(f"{'=' * 60}")

    def _kontakt_holen(self, index):
        if 0 <= index < len(self.kontakte):
            return self.kontakte[index]
        print("  Ungültiger Index!")
        return None


# ============================================
# HAUPTPROGRAMM
# ============================================

def main():
    buch = AddressBook()

    # Automatisch laden, wenn Datei existiert
    if os.path.exists(buch.dateipfad):
        buch.laden()

    while True:
        print(f"\n{'=' * 40}")
        print(f"    KONTAKTBUCH")
        print(f"{'=' * 40}")
        print("  1. Neuer Kontakt")
        print("  2. Alle anzeigen")
        print("  3. Kontakt bearbeiten")
        print("  4. Kontakt löschen")
        print("  5. Suchen")
        print("  6. Sortieren")
        print("  7. Speichern")
        print("  8. CSV exportieren")
        print("  9. CSV importieren")
        print("  0. Beenden")
        print(f"{'=' * 40}")

        wahl = input("Wahl: ").strip()

        if wahl == "1":
            print("\n--- Neuer Kontakt ---")
            vn = input("  Vorname*: ").strip()
            nn = input("  Nachname*: ").strip()
            tel = input("  Telefon: ").strip()
            em = input("  E-Mail: ").strip()
            adr = input("  Adresse: ").strip()
            not_ = input("  Notizen: ").strip()

            fehler = Validator.kontakt_validieren(vn, nn, tel, em)
            if fehler:
                for f in fehler:
                    print(f"    Fehler: {f}")
            else:
                buch.hinzufuegen(Contact(vn, nn, tel, em, adr, not_))
                print(f"  '{vn} {nn}' hinzugefügt!")

        elif wahl == "2":
            buch.alle_anzeigen()

        elif wahl == "3":
            buch.alle_anzeigen()
            try:
                idx = int(input("  Nr. zum Bearbeiten: ")) - 1
                buch.bearbeiten(idx)
            except ValueError:
                print("  Bitte eine Zahl eingeben!")

        elif wahl == "4":
            buch.alle_anzeigen()
            try:
                idx = int(input("  Nr. zum Löschen: ")) - 1
                buch.loeschen(idx)
            except ValueError:
                print("  Bitte eine Zahl eingeben!")

        elif wahl == "5":
            begriff = input("  Suchbegriff: ").strip()
            treffer = buch.suchen(begriff)
            if treffer:
                print(f"\n  {len(treffer)} Treffer:")
                for idx, k in treffer:
                    print(f"  [{idx+1}] {k}")
            else:
                print("  Keine Treffer.")

        elif wahl == "6":
            buch.sortieren()
            print("  Nach Nachname sortiert.")

        elif wahl == "7":
            buch.speichern()

        elif wahl == "8":
            buch.als_csv_exportieren()

        elif wahl == "9":
            buch.aus_csv_importieren()

        elif wahl == "0":
            speichern = input("  Vor dem Beenden speichern? (j/n): ").lower()
            if speichern == "j":
                buch.speichern()
            print("\n  Auf Wiedersehen!")
            break


if __name__ == "__main__":
    main()

Erweiterungsideen

  • Geburtstag-Erinnerungen: Ein Datumsfeld hinzufügen und mit datetime an bevorstehende Geburtstage erinnern
  • Gruppen/Labels: Kontakte in Gruppen organisieren (Familie, Arbeit, Freunde)
  • Duplikat-Erkennung: Warnung, wenn ein ähnlicher Kontakt bereits existiert
  • Favoriten: Kontakte als Favorit markieren und schnell darauf zugreifen
  • GUI mit tkinter: Eine grafische Oberfläche mit Suchfeld und Tabelle
  • SQLite-Datenbank: Statt JSON eine richtige Datenbank verwenden
  • vCard-Export: Kontakte im vCard-Format (.vcf) exportieren, das von Smartphones gelesen werden kann

Was du gelernt hast

In diesem Projekt hast du folgende Konzepte praktisch angewendet:

  • Klassen und ObjekteContact und AddressBook als eigenständige Klassen
  • Properties und Classmethodsvoller_name als Property, from_dict als Classmethod
  • JSON-Dateioperationen — Kontakte persistent speichern und laden
  • CSV-Import/Export — Datenaustausch mit Tabellenkalkulationen
  • Reguläre Ausdrücke — E-Mail- und Telefonnummer-Validierung mit re
  • CRUD-Muster — Das universelle Muster für Datenverwaltung
  • Fehlerbehandlung — Try/Except für robuste Dateioperationen
  • Benutzermenü — Interaktive Konsolensteuerung mit While-Schleife

Pro-Tipp: Das CRUD-Muster, das du hier gelernt hast, ist eines der wichtigsten Konzepte in der Softwareentwicklung. Fast jede Anwendung — ob Webshop, Social Media oder Datenbank — basiert auf Create, Read, Update und Delete. Wenn du dieses Kontaktbuch-Projekt beherrschst, kannst du das gleiche Muster auf beliebige Daten anwenden: Rezepte, Bücher, Aufgaben, Produkte und vieles mehr!

Zurück zum Python Kurs