Editor

Contact Book in Python

Intermediate · Useful tools · about 60 minutes to build · 148 lines

Store names, phone numbers and email addresses, then find, change or remove any contact by searching for part of it.

The program

Press Run and answer its questions in the box that appears under the code.

contact_book.py
"""A contact book: add, list, search, edit and delete contacts."""
MIN_PHONE_DIGITS = 7
PHONE_CHARACTERS = "0123456789 +-()"
MENU = "1) Add 2) List 3) Search 4) Edit 5) Delete 6) Quit"
class Contact:
"""One person's name, phone number and email address."""
def __init__(self, name, phone, email):
self.name = name
self.phone = phone
self.email = email
def matches(self, text):
"""Return True if text appears in the name, phone or email."""
text = text.lower()
return (text in self.name.lower() or text in self.phone
or text in self.email.lower())
def row(self):
"""Return the contact as one line of the table."""
return f"{self.name:<16}{self.phone:<20}{self.email or '-'}"
# Checking what the user types
def is_valid_name(name):
"""A name needs at least one letter."""
return any(ch.isalpha() for ch in name)
def is_valid_phone(phone):
"""Digits, spaces, + - ( ) only, with at least MIN_PHONE_DIGITS digits."""
digits = sum(ch.isdigit() for ch in phone)
return digits >= MIN_PHONE_DIGITS and all(ch in PHONE_CHARACTERS for ch in phone)
def is_valid_email(email):
"""Empty is fine (no email); otherwise it needs name@domain.ending."""
if email == "":
return True
user, _, domain = email.partition("@")
return (user != "" and "." in domain.strip(".")
and "@" not in domain and " " not in email)
def ask_field(label, is_valid, error, current=None):
"""Ask for one field; when editing, pressing Enter keeps the current value."""
hint = "" if current is None else f" [{current or 'none'}]"
while True:
text = input(f"{label}{hint}: ").strip()
if text == "" and current is not None:
return current
if is_valid(text):
return text
print(f" {error}")
# Showing and finding contacts
def search(contacts, text):
"""Return the contacts that match text, sorted by name."""
found = [contact for contact in contacts if contact.matches(text)]
return sorted(found, key=lambda contact: contact.name.lower())
def show_contacts(contacts):
"""Print contacts as a numbered table, or say there are none."""
if not contacts:
print(" No contacts found.")
return
print(f" {'#':>2} {'Name':<16}{'Phone':<20}Email")
for number, contact in enumerate(contacts, start=1):
print(f" {number:>2} {contact.row()}")
def find_one(contacts, action):
"""Search, let the user pick from the matches, and return one contact."""
text = input(f"Search for the contact to {action}: ").strip()
found = search(contacts, text)
if not found:
print(" No contact matches that.")
return None
if len(found) == 1:
return found[0]
show_contacts(found)
while True:
choice = input(f"Which one (1-{len(found)})? ").strip()
if choice.isdecimal() and 1 <= int(choice) <= len(found):
return found[int(choice) - 1]
print(f" Please type a number from 1 to {len(found)}.")
def fill_in(name=None, phone=None, email=None):
"""Ask for every field and return (name, phone, email)."""
name = ask_field("Name", is_valid_name,
"A name needs at least one letter.", name)
phone = ask_field("Phone", is_valid_phone,
f"Use digits, spaces, + - ( ) and at least "
f"{MIN_PHONE_DIGITS} digits.", phone)
email = ask_field("Email (optional)", is_valid_email,
"That does not look like an email address.", email)
return name, phone, email
def main():
"""Run the menu until the user quits."""
contacts = []
print("Contact book")
while True:
print()
print(MENU)
choice = input("Choose 1-6: ").strip()
if choice == "1":
contact = Contact(*fill_in())
contacts.append(contact)
print(f" Added {contact.name}.")
elif choice == "2":
show_contacts(search(contacts, "")) # An empty search matches everyone
elif choice == "3":
show_contacts(search(contacts, input("Search for: ").strip()))
elif choice == "4":
contact = find_one(contacts, "edit")
if contact:
print(f" Editing {contact.name}. Press Enter to keep a value.")
contact.name, contact.phone, contact.email = fill_in(
contact.name, contact.phone, contact.email)
print(f" Updated: {contact.row()}")
elif choice == "5":
contact = find_one(contacts, "delete")
if contact:
answer = input(f"Delete {contact.name}? (y/n): ").strip().lower()
if answer == "y":
contacts.remove(contact)
print(f" Deleted {contact.name}.")
else:
print(" Nothing deleted.")
elif choice == "6":
print("Goodbye.")
break
else:
print(" Please choose a number from 1 to 6.")
if __name__ == "__main__":
main()

Open it in the full editor to save it, share it and keep building.

Sample run

What the program printed, and what was typed (in yellow).

Contact book

1) Add  2) List  3) Search  4) Edit  5) Delete  6) Quit
Choose 1-6: 1
Name: Asha Rao
Phone: 98765 43210
Email (optional): asha@example.com
  Added Asha Rao.

1) Add  2) List  3) Search  4) Edit  5) Delete  6) Quit
Choose 1-6: 1
Name: Ben Carter
Phone: 12ab
  Use digits, spaces, + - ( ) and at least 7 digits.
Phone: +44 20 7946 0958
Email (optional): 
  Added Ben Carter.

1) Add  2) List  3) Search  4) Edit  5) Delete  6) Quit
Choose 1-6: 1
Name: Chitra Nair
Phone: 91234 56789
Email (optional): chitra@mail
  That does not look like an email address.
Email (optional): chitra@example.org
  Added Chitra Nair.

1) Add  2) List  3) Search  4) Edit  5) Delete  6) Quit
Choose 1-6: 3
Search for: ra
   #  Name            Phone               Email
   1  Asha Rao        98765 43210         asha@example.com
   2  Chitra Nair     91234 56789         chitra@example.org

1) Add  2) List  3) Search  4) Edit  5) Delete  6) Quit
Choose 1-6: 4
Search for the contact to edit: asha
  Editing Asha Rao. Press Enter to keep a value.
Name [Asha Rao]: 
Phone [98765 43210]: 98765 00000
Email (optional) [asha@example.com]: 
  Updated: Asha Rao        98765 00000         asha@example.com

1) Add  2) List  3) Search  4) Edit  5) Delete  6) Quit
Choose 1-6: 5
Search for the contact to delete: ben
Delete Ben Carter? (y/n): y
  Deleted Ben Carter.

1) Add  2) List  3) Search  4) Edit  5) Delete  6) Quit
Choose 1-6: 2
   #  Name            Phone               Email
   1  Asha Rao        98765 00000         asha@example.com
   2  Chitra Nair     91234 56789         chitra@example.org

1) Add  2) List  3) Search  4) Edit  5) Delete  6) Quit
Choose 1-6: 6
Goodbye.

How it is built

1. The Contact class

Each contact is an object with three attributes. matches lets one search look in the name, the phone and the email, and row formats the contact as a line of the table.

class Contact:
    """One person's name, phone number and email address."""

    def __init__(self, name, phone, email):
        self.name = name
        self.phone = phone
        self.email = email

    def matches(self, text):
        """Return True if text appears in the name, phone or email."""
        text = text.lower()
        return (text in self.name.lower() or text in self.phone
                or text in self.email.lower())

    def row(self):
        """Return the contact as one line of the table."""
        return f"{self.name:<16}{self.phone:<20}{self.email or '-'}"

2. Checking a phone number

A phone number may hold digits, spaces and + - ( ), and needs at least seven digits. Each check returns True or False, so it can be handed to ask_field as an argument.

def is_valid_phone(phone):
    """Digits, spaces, + - ( ) only, with at least MIN_PHONE_DIGITS digits."""
    digits = sum(ch.isdigit() for ch in phone)
    return digits >= MIN_PHONE_DIGITS and all(ch in PHONE_CHARACTERS for ch in phone)

3. One question function for every field

The check arrives as the is_valid argument, so the same loop asks for a name, a phone or an email. When editing, current is shown in brackets and Enter keeps it.

def ask_field(label, is_valid, error, current=None):
    """Ask for one field; when editing, pressing Enter keeps the current value."""
    hint = "" if current is None else f" [{current or 'none'}]"
    while True:
        text = input(f"{label}{hint}: ").strip()
        if text == "" and current is not None:
            return current
        if is_valid(text):
            return text
        print(f"  {error}")

4. Searching and picking one

A search can match several people, so the function lists them and asks which one. With a single match it returns that contact straight away.

def find_one(contacts, action):
    """Search, let the user pick from the matches, and return one contact."""
    text = input(f"Search for the contact to {action}: ").strip()
    found = search(contacts, text)
    if not found:
        print("  No contact matches that.")
        return None
    if len(found) == 1:
        return found[0]
    show_contacts(found)
    while True:
        choice = input(f"Which one (1-{len(found)})? ").strip()
        if choice.isdecimal() and 1 <= int(choice) <= len(found):
            return found[int(choice) - 1]
        print(f"  Please type a number from 1 to {len(found)}.")

5. The menu

Editing passes the current values into fill_in, which hands back all three fields. Deleting asks for y before anyone is removed.

def main():
    """Run the menu until the user quits."""
    contacts = []
    print("Contact book")
    while True:
        print()
        print(MENU)
        choice = input("Choose 1-6: ").strip()
        if choice == "1":
            contact = Contact(*fill_in())
            contacts.append(contact)
            print(f"  Added {contact.name}.")
        elif choice == "2":
            show_contacts(search(contacts, ""))  # An empty search matches everyone
        elif choice == "3":
            show_contacts(search(contacts, input("Search for: ").strip()))
        elif choice == "4":
            contact = find_one(contacts, "edit")
            if contact:
                print(f"  Editing {contact.name}. Press Enter to keep a value.")
                contact.name, contact.phone, contact.email = fill_in(
                    contact.name, contact.phone, contact.email)
                print(f"  Updated: {contact.row()}")
        elif choice == "5":
            contact = find_one(contacts, "delete")
            if contact:
                answer = input(f"Delete {contact.name}? (y/n): ").strip().lower()
                if answer == "y":
                    contacts.remove(contact)
                    print(f"  Deleted {contact.name}.")
                else:
                    print("  Nothing deleted.")
        elif choice == "6":
            print("Goodbye.")
            break
        else:
            print("  Please choose a number from 1 to 6.")

Make it bigger

  • Save the contacts to a CSV file so the book survives a restart.
  • Add a birthday to each contact and list the birthdays coming up this month.
  • Refuse a second contact with the same phone number.
  • Group the list by the first letter of each name.

What it practises