Typorithm · medium

Raise an HTTP failure rather than returning a status tuple

pythonfastapiindependent-practice
Saved on this device
Full lesson · 4 learning steps Open any exercise for focused practice
1

Step 1 of 4 · intro card

Raise an HTTP failure rather than returning a status tuple

0%

Concept and contract

Keep missing resources and invalid input distinct from successful JSON.

An HTTP failure needs both an appropriate status and a useful public representation. FastAPI does not interpret every framework convention from other Python libraries. In particular, returning a body/status tuple is not the standard way to set a FastAPI error status.

The fixture accepts a small positive role ID, returns a known role and raises HTTPException when a valid identifier has no matching record. A malformed identifier fails request validation earlier. The test observes all three paths, so not-found behavior cannot hide behind a successful JSON response.

Mechanism and worked example

HTTPException interrupts normal endpoint execution and produces its declared status with a detail body.

HTTPException interrupts normal endpoint execution and produces its declared status with a detail body. Request validation creates a different response before the route function runs. The inspection helper reduces validation errors to their types, avoiding brittle comparison of framework prose.

The missing-role branch is intentionally short and contains no side effect. That makes the control flow visible: after the exception, there is no normal success return for the missing object. Real services should likewise avoid performing protected writes before checking required preconditions.

Complete worked example

python

from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient

def inspect_example(role_id):
    app = FastAPI()
    @app.get("/roles/{role_id}")
    def read(role_id: int):
        if role_id != 1:
            raise HTTPException(status_code=404, detail="Role not found")
        return {"id": 1, "title": "QA"}
    with TestClient(app) as client:
        response = client.get(f"/roles/{role_id}")
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}
Reasoning
HTTPException interrupts normal endpoint execution and produces its declared status with a detail body. Request validation creates a different response before the route function runs. The inspection helper reduces validation errors to their types, avoiding brittle comparison of framework prose. The missing-role branch is intentionally short and contains no side effect. That makes the control flow visible: after the exception, there is no normal success return for the missing object. Real services should likewise avoid performing protected writes before checking required preconditions.

Focused decision: use the complete example for context

python

@app.get("/roles/{role_id}")
def read(role_id: int):
    if role_id != 1:
        raise HTTPException(status_code=404, detail="Role not found")
    return {"id": 1, "title": "QA"}
Focused decision
Reconstruct this focused decision after the warm-up. The complete imports, model or application, and test client remain in the worked example. Typing records practice; only the downloadable computer lab executes Python.

Counterexamples and common mistakes

The counterexample returns ({detail: ...}, 404).

The counterexample returns ({detail: ...}, 404). FastAPI serializes that Python value as JSON while the ordinary success status remains in effect. A client that checks only whether the body mentions not found can therefore misclassify the operation.

Do not catch every Exception and turn it into a 200 with an error field. Unexpected service failures need separate logging and operational handling; client-facing messages should avoid internal stack traces or credentials. The lesson handles expected absence, not every possible production incident.

Controlled mistake: observe the actual wrong behavior

python

from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient

def inspect_example(role_id):
    app = FastAPI()
    @app.get("/roles/{role_id}")
    def read(role_id: int):
        if role_id != 1:
            return {"detail": "Role not found"}, 404
        return {"id": 1, "title": "QA"}
    with TestClient(app) as client:
        response = client.get(f"/roles/{role_id}")
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}
Reasoning
The counterexample returns ({detail: ...}, 404). FastAPI serializes that Python value as JSON while the ordinary success status remains in effect. A client that checks only whether the body mentions not found can therefore misclassify the operation. Do not catch every Exception and turn it into a 200 with an error field. Unexpected service failures need separate logging and operational handling; client-facing messages should avoid internal stack traces or credentials. The lesson handles expected absence, not every possible production incident.

Independent practice: one bounded change

Change the public missing-role detail to Role unavailable while keeping status 404.

Change the public missing-role detail to Role unavailable while keeping status 404. Preserve successful roles and validation failures. This is a bounded response-contract change that encourages testing the status independently from the wording.

Run a known role, an absent role and malformed input. Do not update the test helper to rewrite the old detail into the new one. Observe the actual HTTP body. If changing a public message affects a client, prefer a stable structured error code in a real versioned contract.

Step 1: Change the missing-role detail while keeping its failure status and unrelated parsing behavior. Step 2: Retain the working imports and application structure. Change the stated policy, run the supplied practice cases, then explain the newly accepted or rejected boundary.

Practice starter: solve before reading the solution

python

from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient

def inspect_example(role_id):
    app = FastAPI()
    @app.get("/roles/{role_id}")
    def read(role_id: int):
        if role_id != 1:
            raise HTTPException(status_code=404, detail="Role not found")
        return {"id": 1, "title": "QA"}
    with TestClient(app) as client:
        response = client.get(f"/roles/{role_id}")
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}

# Independent task: Change the missing-role detail while keeping its failure status and unrelated parsing behavior.
Reasoning
Change the missing-role detail while keeping its failure status and unrelated parsing behavior.
  • Hint 1: Find the one field or branch that owns this policy.
  • Hint 2: Compare the first changed acceptance case with the retained baseline.

Solution, tests and trade-offs

The solution changes only the declared detail.

The solution changes only the declared detail. It keeps HTTPException in the failure branch, so the response remains a failure and endpoint success cannot continue accidentally. The existing input validation case proves the wording change did not weaken request parsing.

Some products deliberately use 404 instead of 403 to avoid disclosing object existence. That is an access-control design choice, not a universal rule. Establish a consistent policy and test it with authorized and unauthorized principals rather than copying a status mechanically from an example.

Practice solution and acceptance checks

python

from fastapi import FastAPI, HTTPException
from fastapi.testclient import TestClient

def inspect_example(role_id):
    app = FastAPI()
    @app.get("/roles/{role_id}")
    def read(role_id: int):
        if role_id != 1:
            raise HTTPException(status_code=404, detail="Role unavailable")
        return {"id": 1, "title": "QA"}
    with TestClient(app) as client:
        response = client.get(f"/roles/{role_id}")
        body = response.json()
        if response.status_code == 422:
            body = sorted({e["type"] for e in body["detail"]})
        return {"status": response.status_code, "body": body}
Reasoning
The solution changes only the declared detail. It keeps HTTPException in the failure branch, so the response remains a failure and endpoint success cannot continue accidentally. The existing input validation case proves the wording change did not weaken request parsing. Some products deliberately use 404 instead of 403 to avoid disclosing object existence. That is an access-control design choice, not a universal rule. Establish a consistent policy and test it with authorized and unauthorized principals rather than copying a status mechanically from an example.

Interview reasoning and next step

In an interview, explain what happens to a tuple returned by this framework and why a detail string alone cannot establish failure semantics.

In an interview, explain what happens to a tuple returned by this framework and why a detail string alone cannot establish failure semantics. Show separate expectations for malformed input, missing data and a successful response.

The next lesson explores routing order. A well-designed error branch is irrelevant if a static path is first consumed by an earlier dynamic route. Debug the selected operation before changing its validation or error handling.

Optional stretch — after the core task works: Add one independently reasoned boundary case and explain its expected value before running it.

Computer practice: https://applydjinn.com/typorithm/python-api-lab-20261105.zip. Use an isolated Python environment and the pinned dependencies. Run verify.py --track fastapi, then edit the working exercise baseline and run --practice fa-errors. The reference suite rejects meaningful counterexamples and checks values, types and unchanged inputs. All data is owned and all HTTP clients are in-process. No production database, feed, account or paid service is used.

Keep missing resources and invalid input distinct from successful JSON.