Final Project: `contact_book` Mini Library
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.
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.mdDo 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 phoneMilestone 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")) == 1Acceptance checklist
- The package uses a
src/layout. - There is a
Contactdataclass. - 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.tomlandREADME.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__.pyexample 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.
- 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
shutilfor 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.