Challenge 3: Fix a Task That Passes a Model Instance — Possible Solution ==================================================================== # BEFORE — broken # catalog/tasks.py @shared_task def process_order(order): order.status = "processed" order.save() # catalog/views.py def checkout(request): order = Order.objects.create(...) process_order.delay(order) # ⚠️ passing the actual model instance return redirect("order-confirmation") # AFTER — fixed # catalog/tasks.py @shared_task def process_order(order_id): from .models import Order order = Order.objects.get(pk=order_id) # re-fetched fresh, inside the task order.status = "processed" order.save() # catalog/views.py def checkout(request): order = Order.objects.create(...) process_order.delay(order.pk) # pass just the ID return redirect("order-confirmation") WHAT WOULD ACTUALLY GO WRONG WITH THE ORIGINAL VERSION ----------------------------------------------------------- Celery needs to serialize every task's arguments into a message format (by default, JSON) to send them across the broker to a worker process, which might not even be running on the same machine. A Django model instance is a complex Python object — it isn't JSON-serializable by default at all. Depending on the configured serializer: 1. With Celery's default JSON serializer, calling process_order.delay(order) would raise a serialization error immediately (something like "Object of type Order is not JSON serializable") — the task would never even get queued. 2. If a different serializer (like pickle) were configured instead, the call might succeed, but it would serialize a SNAPSHOT of the order object exactly as it existed at the moment .delay() was called. If anything about that order changes between when the task is queued and when a worker actually picks it up and runs it — another process updates its status, the order is cancelled, or (worse) the order is deleted entirely — the task operates on stale, disconnected data rather than the order's real current state, and calling order.save() on that stale instance could silently overwrite legitimate changes made by something else in the meantime. Passing order.pk (a plain integer) sidesteps both problems: integers serialize trivially to JSON, and re-fetching Order.objects.get(pk=order_id) inside the task guarantees the task always operates on the order's actual current database state at the moment it runs, not whatever it looked like when it was originally queued. WHY THIS WORKS -------------- - This is the exact pattern demonstrated in Challenge 1's generate_book_report task and reinforced by this chapter's gotcha — passing an ID and re-fetching inside the task isn't just a style preference, it's what makes the task's behavior correct and serializable at all.