Nobody has started this yet — be first.
Business impact
Typos happen ("Maditate"), people move timezones, and habit names change as life changes ("Gym 3x/week" becomes "Gym daily"). Today the only way to change a habit's name or timezone is DELETE + re-POST -- which destroys every check-in and the whole streak history along with it. That's a disproportionate cost for fixing a typo, and it's the kind of gap that generates "why did my streak reset, I didn't miss a day!" support tickets that are actually self-inflicted by the workaround, not a real bug.
Problem
app/schemas.py::HabitUpdate and the PATCH /habits/{habit_id} route in app/main.py already exist and call crud.update_habit -- but crud.update_habit is a stub that unconditionally raises NotImplementedError, so every PATCH currently 500s.
Current behavior
Every PATCH /habits/{id} request currently returns a 500 Internal Server Error because crud.update_habit unconditionally raises NotImplementedError, regardless of what fields are sent.
Expected behavior
PATCH /habits/{id} accepts a partial HabitUpdate body ({"name": ...}, {"timezone": ...}, or both) and updates only the fields that were actually provided, leaving the rest of the habit -- critically, all of its check-ins -- untouched. timezone, when provided, goes through the same IANA validation HabitCreate already uses.
Steps to reproduce
uvicorn app.main:app --reload
curl -s -X PATCH http://127.0.0.1:8000/habits/<id> -H "Authorization: Bearer <token>"
-H 'Content-Type: application/json' -d '{"name": "Meditate daily"}'
Why this matters
The interesting part isn't the SQLAlchemy update itself -- it's correctly distinguishing "the caller didn't send this field" from "the caller sent this field back to its current value" from Pydantic's partial-update model. HabitUpdate gives every field a default of None, which means you also need to decide: does None ever mean "clear this field," or does it always mean "field omitted, don't touch it"? For this ticket, every field is required/non-nullable on Habit itself, so None can only sensibly mean "not provided."
Suggested approach
Look at crud.create_habit for how a Habit ORM instance gets its fields set, and at Pydantic's model_dump(exclude_unset=True) (or equivalent) for how to get only the fields the caller actually included in the request body, as opposed to every field's current/default value.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket04_update_habit.py -v
Hints (0/2)
Try it without hints first — the reading is the exercise.
Working on this ticket
Work on a branch named for the ticket — that's what you'll submit.
Branch off your fork
$git checkout -b feat/update-habit-endpointFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin feat/update-habit-endpointSubmit it below
Paste your fork URL and the branch name, with a short write-up of the root cause.
Questions
Ask about anything unclear in the ticket — the maintainer and anyone who has solved it can answer. Please don't post full solutions.
Sign in to ask a question or reply.
Sign indocker compose up --buildfastapi/habit_trackerRun the project's own suite with pytest -v (or pytest tests/ -q for a quick pass/fail count) from fastapi/habit_tracker -- a fresh clone should show a handful of failures until TICKET-03 and TICKET-06 are fixed.
Work the tickets in practice_tickets/ (TICKET-01 through TICKET-07); each names one pytest file under practice_tickets/tests/:
pytest practice_tickets/tests/test_ticket01_stats_window.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 7, clean pass/fail summary
./practice_tickets/run_tickets.sh 03 07 # just a subset
practice_tickets/tests/ is intentionally outside pyproject.toml's testpaths = ["tests"], so a plain pytest run from the repo root never picks these up -- they're learning exercises, not part of the project's CI-gating regression suite.
Level 2
Implement a feature
Extend the system within its own patterns.