Premier League Predictor: FastAPI & PostgreSQL — Chapter 4, Exercise 1 ==================================================== TASK Explain why create_gameweek and create_fixture both check their own rule in Python (the 1-38 range, home_team_id == away_team_id) even though a database-level guarantee (a CheckConstraint or a UniqueConstraint) already exists for each one. SOLUTION The database-level constraints are real and genuinely enforced — a gameweek numbered 45, or a fixture where a team plays itself, could never actually be stored, with or without the Python-level checks. What the database constraints don't do is fail nicely. When PostgreSQL rejects an insert because a constraint was violated, SQLAlchemy raises an IntegrityError. Left uncaught, that exception propagates straight up through the route handler as an unhandled exception, and FastAPI turns it into a bare 500 Internal Server Error with no useful detail about what actually went wrong — genuinely unhelpful for whoever, or whatever frontend, is calling the API. Checking the same rule in Python first means the bad request never even reaches the database at all — it gets caught early and turned into a clean, specific HTTPException (a 400 with "Gameweek number must be between 1 and 38," or "A team cannot play itself") that actually explains the problem. For the case that can't be caught this early — the gameweek 1-38 route also wraps its own commit() in a try/except IntegrityError, since a duplicate gameweek number can only really be detected once the insert is attempted — the same principle applies one step later: catch the database's own real rejection and turn it into a readable 409 instead of letting the raw exception surface. Both checks exist for the same reason: the database constraint guarantees correctness no matter what; the Python-level check (or the try/except around the commit) makes a violation actually pleasant to handle, both for a human debugging the API and for a frontend trying to show a useful error message. WHY THIS WORKS AS AN ANSWER ---------------------------- It distinguishes what the database constraint actually guarantees (correctness) from what it doesn't provide (a friendly error), explains concretely what happens to an uncaught IntegrityError inside FastAPI, and covers both the pre-emptive Python check and the try/except-around- commit pattern as two forms of the same underlying reasoning.