Final Project: `contact_book` Mini Library

Final project goals

By completing this project you will be able to:

  • structure a reusable mini library,
  • model data with dataclass,
  • normalize and validate inputs,
  • save and load data as JSON,
  • cover the core behavior with automated tests.
Project convention

The package is called contact_book in both languages to keep a realistic Python package naming convention.

The goal

You are going to build a small library to manage contacts. It will not be a web app or a one-off script, but a reusable package that another project could import.

The idea is to integrate in one exercise what you have already practiced separately:

  • data structures to store contacts and tags,
  • functions for search, filtering, and sorting,
  • light OOP with dataclass,
  • files for JSON persistence,
  • testing to make sure the core works.

Functional brief

The library should allow you to:

  • create contacts with name, email, phone number, and tags,
  • normalize input before saving,
  • validate basic email and phone formats,
  • save an address book to JSON and load it back,
  • search by text,
  • filter by tag,
  • sort by name.

Target package structure

 1contact_book/
 2β”œβ”€β”€ src/
 3β”‚   └── contact_book/
 4β”‚       β”œβ”€β”€ __init__.py
 5β”‚       β”œβ”€β”€ models.py
 6β”‚       β”œβ”€β”€ validators.py
 7β”‚       β”œβ”€β”€ storage.py
 8β”‚       └── service.py
 9β”œβ”€β”€ tests/
10β”‚   β”œβ”€β”€ test_validators.py
11β”‚   β”œβ”€β”€ test_storage.py
12β”‚   └── test_service.py
13β”œβ”€β”€ pyproject.toml
14└── README.md
What to validate first

Do not try to write everything at once. Finish each milestone and add tests before moving to the next one.


Milestone 1: Model and validation

Start by defining the data model and the cleanup/validation helpers.

1# src/contact_book/models.py
2from dataclasses import dataclass, field
3
4@dataclass(slots=True)
5class Contact:
6    name: str
7    email: str
8    phone: str = ""
9    tags: list[str] = field(default_factory=list)
 1# src/contact_book/validators.py
 2import re
 3
 4EMAIL_RE = re.compile(r"^[^@\\s]+@[^@\\s]+\\.[^@\\s]+$")
 5PHONE_RE = re.compile(r"^[0-9+()\\-\\s]{7,}$")
 6
 7def normalize_name(name: str) -> str:
 8    return " ".join(name.strip().split())
 9
10def normalize_tags(tags: list[str]) -> list[str]:
11    return sorted({tag.strip().lower() for tag in tags if tag.strip()})
12
13def validate_email(email: str) -> str:
14    email = email.strip().lower()
15    if not EMAIL_RE.match(email):
16        raise ValueError("Invalid email")
17    return email
18
19def validate_phone(phone: str) -> str:
20    phone = phone.strip()
21    if phone and not PHONE_RE.match(phone):
22        raise ValueError("Invalid phone")
23    return phone

Milestone 2: JSON persistence

Once the model is clear, create a simple and predictable storage layer.

 1# src/contact_book/storage.py
 2import json
 3from dataclasses import asdict
 4from pathlib import Path
 5
 6from .models import Contact
 7
 8def save_contacts(path: str | Path, contacts: list[Contact]) -> None:
 9    data = [asdict(contact) for contact in contacts]
10    Path(path).write_text(json.dumps(data, indent=2), encoding="utf-8")
11
12def load_contacts(path: str | Path) -> list[Contact]:
13    raw = Path(path).read_text(encoding="utf-8")
14    items = json.loads(raw)
15    return [Contact(**item) for item in items]

Milestone 3: Address book operations

This is where the useful core of the library lives: add, search, filter, and sort.

 1# src/contact_book/service.py
 2from .models import Contact
 3from .validators import normalize_name, normalize_tags, validate_email, validate_phone
 4
 5def build_contact(name: str, email: str, phone: str = "", tags: list[str] | None = None) -> Contact:
 6    return Contact(
 7        name=normalize_name(name),
 8        email=validate_email(email),
 9        phone=validate_phone(phone),
10        tags=normalize_tags(tags or []),
11    )
12
13def search_contacts(contacts: list[Contact], text: str) -> list[Contact]:
14    needle = text.strip().lower()
15    return [
16        contact
17        for contact in contacts
18        if needle in contact.name.lower() or needle in contact.email.lower()
19    ]
20
21def filter_by_tag(contacts: list[Contact], tag: str) -> list[Contact]:
22    wanted = tag.strip().lower()
23    return [contact for contact in contacts if wanted in contact.tags]
24
25def sort_contacts(contacts: list[Contact]) -> list[Contact]:
26    return sorted(contacts, key=lambda contact: contact.name.lower())

Milestone 4: Core tests

You do not need to test every line, but you do need to test the important behaviors.

 1# tests/test_validators.py
 2import pytest
 3
 4from contact_book.validators import normalize_name, normalize_tags, validate_email
 5
 6def test_normalize_name_removes_extra_spaces():
 7    assert normalize_name("  Ana   Perez ") == "Ana Perez"
 8
 9def test_normalize_tags_unique_and_lowercase():
10    assert normalize_tags(["Work", " work ", "", "Family"]) == ["family", "work"]
11
12def test_validate_email_rejects_invalid_values():
13    with pytest.raises(ValueError):
14        validate_email("mail-without-at")
 1# tests/test_service.py
 2from contact_book.service import build_contact, filter_by_tag, search_contacts
 3
 4def test_build_contact_normalizes_values():
 5    contact = build_contact("  Ana Perez ", "[email protected]", tags=["Work", "work"])
 6    assert contact.name == "Ana Perez"
 7    assert contact.email == "[email protected]"
 8    assert contact.tags == ["work"]
 9
10def test_search_contacts_finds_by_name_and_email():
11    contacts = [
12        build_contact("Ana", "[email protected]"),
13        build_contact("Luis", "[email protected]"),
14    ]
15    assert len(search_contacts(contacts, "ana")) == 1
16
17def test_filter_by_tag_returns_matching_contacts():
18    contacts = [
19        build_contact("Ana", "[email protected]", tags=["work"]),
20        build_contact("Luis", "[email protected]", tags=["family"]),
21    ]
22    assert len(filter_by_tag(contacts, "work")) == 1

Acceptance checklist

  • The package uses a src/ layout.
  • There is a Contact dataclass.
  • Names and tags are normalized before saving.
  • Invalid emails raise ValueError.
  • The library can save and load JSON.
  • Search, filtering, and sorting are separate functions.
  • The core package has automated tests.
  • The project includes pyproject.toml and README.md.

Optional extensions

If you want to stretch the project, add one of these improvements:

  • CSV import/export support,
  • updating existing contacts,
  • deleting by email,
  • a small __main__.py example entry point,
  • more parametrized validation tests.

Congratulations!

You have completed the Python course. From your first print("Hello world") to a reusable mini library, you now have a coherent path across syntax, data, functions, files, testing, and project organization.

What you integrated in this ending
  • Fundamentals to model data and control flow
  • Data structures for lists, tags, and filters
  • Functions to separate responsibilities
  • OOP with dataclass
  • Files with JSON persistence
  • Testing to validate the core
  • Projects with professional structure and basic packaging

Next steps

Now it makes sense to explore a branch depending on what you enjoyed most:

  • FastAPI for clean, modern APIs
  • Flask for smaller flexible apps
  • Django if you want a more complete framework
  • pandas for table-shaped data
  • NumPy for numerical computing
  • matplotlib/seaborn for visualizations
  • requests/httpx for API integrations
  • pathlib and shutil for useful scripts
  • schedule or system schedulers for recurring automation
  • refactor your mini library,
  • add more tests,
  • publish it as a personal project,
  • compare your solution with real libraries.

Previous: Projects Next: Cheat Sheet