Nobody has started this yet — be first.
Business impact
When a bulk CSV import partially fails, the API tells the user which "row" had a problem so they can go fix it in their spreadsheet and re-upload. Right now every reported row number is one less than the actual line in the file the user uploaded. A user who trusts the error message ("row 3 has a bad email") will open their spreadsheet, "fix" the wrong line, re-upload, and get the exact same error again -- a frustrating, support-ticket-generating loop for what should be a simple self-service correction. For a bulk-import feature, accurate row reporting isn't cosmetic -- it's the entire value of returning per-row errors instead of one opaque failure.
Problem
import_contacts() numbers rows via enumerate(reader, start=1). Since the CSV header occupies line 1 of the file, the first data row is line 2 -- but the loop reports it as "row 1", the second data row as "row 2", and so on: every reported row number is off by exactly one relative to the actual line number in the uploaded file.
Current behavior
A CSV import result reports the first data row as "row 1" instead of "row 2", shifting every subsequent row number down by one relative to the real line in the uploaded file.
Expected behavior
The "row" field in each result of POST /api/contacts/import should match the actual 1-indexed line number in the uploaded CSV file (header = line 1, so the first data row is line 2).
Steps to reproduce
printf 'name,phone,email,groups\nGood Row,555-1000,good@example.com,\n,555-2000,bad@example.com,\n' > /tmp/contacts.csv curl -s -X POST http://127.0.0.1:5000/api/contacts/import -F "file=@/tmp/contacts.csv;type=text/csv"
Why this matters
enumerate(iterable, start=N) is a one-argument decision that silently shifts every downstream index. Here the comment right above the loop (# header is row 1) documents the intended numbering, but the start= value contradicts its own comment -- the kind of drift that happens when someone "simplifies" a loop and doesn't re-derive the correct offset from first principles.
Suggested approach
Look at the for row_number, row in enumerate(reader, ...) line in import_contacts(). Re-derive the offset from the comment already sitting right above it: if the header is line 1, what should start= be so the first data row is numbered correctly?
Acceptance criteria
Verification
SECRET_KEY=dev-secret pytest practice_tickets/tests/test_ticket_04_csv_row_number_off_by_one.py -v && SECRET_KEY=dev-secret pytest tests/test_csv.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 fix/csv-import-row-number-off-by-oneFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/csv-import-row-number-off-by-oneSubmit 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 inBy default DevelopmentConfig uses a local SQLite file, no Postgres server required -- point DATABASE_URL at a real Postgres instance instead if you want one, or use Docker Compose instead (cp .env.example .env then docker compose up --build, which starts a healthchecked Postgres 16 container plus the app served by gunicorn behind entrypoint.sh, which waits for Postgres, runs flask db upgrade, then starts gunicorn -- migrations are always applied on boot). The API is then available at http://localhost:5000 either way.
The existing suite runs under TestingConfig (an in-memory SQLite database created fresh per test, so it never touches your dev database):
source .venv/bin/activate
export SECRET_KEY=dev-secret
pytest -v
Work the tickets in practice_tickets/tickets/ (TICKET-01 through TICKET-07); each names one dedicated test in practice_tickets/tests/ -- a separate pytest package from the project's normal tests/, which plain pytest runs by default:
SECRET_KEY=dev-secret pytest practice_tickets/tests/test_ticket_01_name_length_boundary.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 7, clean pass/fail summary
All 7 dedicated tests fail out of the box -- that is the starting point, not a setup mistake. Fixing TICKET-02 (search operator flip) and TICKET-04 (CSV row-number off-by-one) correctly also turns 4 currently-failing tests in the project's own tests/ suite back to green.
Level 1
Fix a bug
Read existing behaviour, correct it.