Nobody has started this yet — be first.
Business impact
Every auto-generated short link's slug is base62.encode(row.id). Redirects still "work" today because GET /<slug> looks up ShortURL.query.filter_by(slug=slug) using the string slug directly, never decoding it back to an id. The real damage shows up anywhere the id needs to be recovered from the slug (a future admin tool, a migration script, a support engineer manually computing "what id does /3d7 correspond to?"), and in the fact that two different ids can currently encode to visually-confusable strings. This is the kind of bug that looks fine in a demo (single-digit ids: 0-61) and quietly produces garbage the moment the id counter crosses 62 -- exactly when a project has enough traffic to matter.
Problem
base62.encode() builds the base-62 digits of n one at a time using divmod, appending each digit to a list as it goes -- a loop that naturally produces digits least-significant first. The final line joins that list directly, without reversing it first.
Current behavior
For any n < 62 this is invisible (only one digit, so order does not matter). The moment n >= 62 and encoding needs two or more digits, the string comes out backwards: base62.encode(62) returns "01" instead of "10", and base62.decode(base62.encode(1000)) returns 512, not 1000.
Expected behavior
base62.decode(base62.encode(n)) == n for every non-negative integer n, and base62.encode(62) == "10" (not "01").
Steps to reproduce
Why this matters
divmod(n, BASE) on each loop iteration yields the digit representing the current least-significant place of what is left of n. Converting an integer to a positional-notation string always requires reversing that sequence before joining, because "most significant digit first" is the opposite order from "the order in which division peels digits off." This is the single most common way to get base-N conversion wrong.
Suggested approach
Look at the loop in encode() and compare the order digits are appended in against the order they need to appear in the final string. You do not need to change the loop itself -- only how the collected digits are assembled into the return value.
Acceptance criteria
Verification
pytest practice_tickets/tests/test_ticket_01_base62_roundtrip.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
Sign in to create it automatically, or do it yourself:
$git checkout -b fix/base62-round-tripFix it and commit
Meet every acceptance criterion, and add a test that would have caught this.
Push the branch
$git push -u origin fix/base62-round-tripSubmit 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-entrypoint.shcurl -X POST http://localhost:5000/api/urls \
-H "Content-Type: application/json" \
-d '{"target_url": "https://anthropic.com", "custom_slug": "claude"}'
# -> {"slug": "claude", "short_url": "http://localhost:5000/claude", "expires_at": null}
curl -i http://localhost:5000/claude # 302 redirect, records a click
make seed # or: docker compose exec web flask seed-db
If something is already listening on 5432/6379 locally, uncomment POSTGRES_HOST_PORT / REDIS_HOST_PORT in .env to remap the host side — the app always talks to db/redis over the compose network regardless. Run production-style (gunicorn, no dev server, no bind mount) with docker compose -f docker-compose.prod.yml up --build. GET /healthz checks both Postgres and Redis connectivity.
You do not need Docker running at all to work the tickets — the test suite uses TestingConfig (SQLite in-memory via StaticPool) and fakeredis instead of a real Postgres/Redis, so everything runs against a plain virtualenv:
python3 -m venv .venv && .venv/bin/pip install -r requirements-dev.txt
.venv/bin/pytest practice_tickets/tests/test_ticket_01_base62_roundtrip.py -v # a single ticket
./practice_tickets/run_tickets.sh # all 20, clean pass/fail summary
./practice_tickets/run_tickets.sh 05 13 16 # just a few, by number
All 20 tests fail at the start; as you fix each ticket in app/, its test (and only its test) should flip to passing. .venv/bin/pytest tests/ (via make test) runs the project's own main suite — some ticket bugs (TICKET-05, 07, 08, 09, 14 among others) also break assertions in that suite, since they are real bugs in shared code paths; fixing the ticket fixes those too, not a separate task.
Level 1
Fix a bug
Read existing behaviour, correct it.