Debugging wrong data: trace the value, not the screen
Wrong-data bugs are quiet. Nothing crashes: a total is a cent off, a day's revenue lands on the wrong day, a record is simply gone. By the time someone notices, the damage is already stored. The skill here is to stop staring at the screen and follow one wrong value backwards, from where it is displayed to where it was produced.
Follow one value end to end
Pick one concrete wrong example (order 4812, invoice INV-2025-00031, the 50.00 subtotal) and walk it through the system: what the database holds, what the code reads, what each function returns, what is finally rendered. The first place the value is wrong is where the bug lives, and it is often earlier than you expect. If the stored value is already wrong, the bug is in the write path; if the stored value is right, it is in the read path.
Keep the example small enough to check by hand. A wrong number you can recompute on paper is worth more than a hundred you can only see in aggregate.
Relationships decide what deletion does
Every foreign key answers a question the model author may not have asked: what should happen to me when the row I point at is deleted? CASCADE deletes me too, PROTECT refuses the deletion, SET_NULL keeps me but detaches me. Cascades follow chains: delete a customer and the orders go, and so does everything hanging off the orders. Django even shows you this list on the admin delete confirmation page.
For business records (orders, invoices, payments) the right answer is almost never to lose them. The bug is often a single word in a model, and it can lie dormant for months until someone deletes the wrong row.
Money is exact and time has a zone
Two families of data bugs come from using the wrong kind of number. Binary floating point cannot represent most decimal fractions exactly, so money computed through floats drifts by a cent in a few percent of cases, and round() on a float does not round halves the way accounting expects. Keep money in Decimal from input to output and choose the rounding mode on purpose.
Time has the mirror-image problem. Django stores instants in UTC, but a business day (or a report row, or an invoice date) belongs to a time zone, and the offset changes with daylight saving. "Which day was this order placed?" has no answer until you say which zone you mean. Whenever a number sits next to a date, ask what zone the date is in and where the conversion happens.
Each lab in this section gives you one such lie to run to ground. The fix is only done when a test with a carefully chosen input (the 50.00 subtotal, the order just before midnight, the customer with orders) fails on the old code.