What We're Building
In this project walkthrough, you'll build a practical command-line tool using Python and Typer — a modern library for building CLI apps based on type hints. By the end, you'll have a working notes CLI that can add, list, and delete short notes, stored in a local JSON file.
This project teaches you: Typer basics, file I/O, JSON handling, and packaging a script as a runnable command.
Why Typer?
Typer is built on top of Click, but uses Python type annotations to automatically generate argument parsing, help text, and error messages. You write less boilerplate and get a polished CLI with almost no extra effort.
Setup
mkdir notes-cli && cd notes-cli
python -m venv .venv
source .venv/bin/activate
pip install typer[all]
Project Structure
notes-cli/
├── notes_cli/
│ ├── __init__.py
│ ├── main.py
│ └── storage.py
├── pyproject.toml
└── README.md
Step 1: Storage Layer
Create notes_cli/storage.py to handle reading and writing notes to a JSON file:
import json
from pathlib import Path
NOTES_FILE = Path.home() / ".notes.json"
def load_notes() -> list[dict]:
if not NOTES_FILE.exists():
return []
return json.loads(NOTES_FILE.read_text())
def save_notes(notes: list[dict]) -> None:
NOTES_FILE.write_text(json.dumps(notes, indent=2))
def add_note(text: str) -> dict:
notes = load_notes()
note = {"id": len(notes) + 1, "text": text}
notes.append(note)
save_notes(notes)
return note
def delete_note(note_id: int) -> bool:
notes = load_notes()
new_notes = [n for n in notes if n["id"] != note_id]
if len(new_notes) == len(notes):
return False
save_notes(new_notes)
return True
Step 2: The CLI Commands
Create notes_cli/main.py:
import typer
from . import storage
app = typer.Typer(help="A simple notes manager.")
@app.command()
def add(text: str = typer.Argument(..., help="The note text to add")):
"""Add a new note."""
note = storage.add_note(text)
typer.echo(f"✅ Note added with ID {note['id']}: {note['text']}")
@app.command()
def list():
"""List all notes."""
notes = storage.load_notes()
if not notes:
typer.echo("No notes yet. Add one with: notes add 'Your note'")
return
for note in notes:
typer.echo(f"[{note['id']}] {note['text']}")
@app.command()
def delete(note_id: int = typer.Argument(..., help="ID of the note to delete")):
"""Delete a note by ID."""
if storage.delete_note(note_id):
typer.echo(f"🗑️ Note {note_id} deleted.")
else:
typer.echo(f"Note with ID {note_id} not found.", err=True)
raise typer.Exit(code=1)
if __name__ == "__main__":
app()
Step 3: Make It Installable
In pyproject.toml, define the entry point so users can run notes directly:
[project]
name = "notes-cli"
version = "0.1.0"
requires-python = ">=3.10"
dependencies = ["typer[all]"]
[project.scripts]
notes = "notes_cli.main:app"
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
Then install in development mode:
pip install -e .
Step 4: Try It Out
notes add "Buy groceries"
notes add "Read Python docs"
notes list
# [1] Buy groceries
# [2] Read Python docs
notes delete 1
notes list
# [2] Read Python docs
What to Build Next
- Add timestamps to notes using
datetime - Add a
searchcommand that filters notes by keyword - Add colored output using
typer.style() - Export notes to a Markdown file
This project is a great foundation. CLIs are incredibly useful for automating your own workflows — and Typer makes building them a genuine pleasure.