HTTP Status Codes
HTTP status codes matter. Learn correct usage for Python web development with practical steps, edge cases, and what to study next.
Focus: use http status codes correctly
Ever built a Python web API and wondered why your frontend shows a confusing 500 Internal Server Error when a user submits invalid data? Or maybe you've returned a 404 when a resource was actually forbidden? Inconsistent or wrong HTTP status codes are a silent killer of API usability — they break client logic, make debugging a nightmare, and frustrate users. This lesson will teach you the exact rules for using HTTP status codes correctly, so your Python web applications communicate clearly and behave predictably under every circumstance.
The problem this lesson solves
Incorrect HTTP status codes are more than a style issue — they actively break API clients. A frontend that sees a 200 OK for a failed login might display “Welcome” instead of an error message. A monitoring system that treats every 4xx as a 5xx could page you at 3 AM. When your API returns 500 for a missing record, the client can't distinguish between “your server is broken” and “you asked for something that doesn't exist.”
Without a shared understanding of status code semantics, every integration becomes a debugging session. Your API consumers have to guess what your endpoint means when it returns 200 with an error body vs. 201 with an empty response. The pain is real: misused codes cause silent failures, broken retries, and brittle client-side logic. This lesson gives you a mental template to eliminate that guesswork for good.
Core concept / mental model
Think of HTTP status codes as a three-digit language your server speaks to every client — browsers, mobile apps, curl scripts, and other services. Each code has a precise meaning, and getting it right is like using the correct verb in a sentence. Just as you wouldn't say “I'm fine” when you're hurt, you shouldn't return 200 OK when your API failed.
Here's the simple classification, the five families of status codes:
- 1xx (Informational): The request is received and processing continues. Rarely used in typical web apps.
- 2xx (Success): The request was received, understood, and accepted. Your API did exactly what was asked.
- 3xx (Redirection): The client must take additional action to complete the request, like following a redirect.
- 4xx (Client Error): The request contains bad syntax or cannot be fulfilled. The fault is on the client's side.
- 5xx (Server Error): The server failed to fulfill an apparently valid request. The fault is on your side.
Pro tip: A great way to build the mental model is to ask yourself “Whose fault is it?” every time you choose a status. If the client made a mistake — missing data, wrong URL, not authenticated — use a 4xx. If your server hit an exception or a database outage — use a 5xx.
The next level is remembering the most common codes in each family:
| Code | Name | Typical meaning |
|---|---|---|
| 200 | OK | Successful GET, PUT, DELETE (if no body returned) |
| 201 | Created | New resource created (POST) |
| 204 | No Content | Success, but no body to return |
| 301 | Moved Permanently | Permanent redirect |
| 302 | Found | Temporary redirect |
| 400 | Bad Request | Malformed syntax or missing fields |
| 401 | Unauthorized | Authentication missing or invalid |
| 403 | Forbidden | Authenticated but not allowed |
| 404 | Not Found | Resource doesn't exist |
| 409 | Conflict | Resource state conflicts (e.g., duplicate) |
| 422 | Unprocessable Entity | Validation errors (semantically invalid) |
| 429 | Too Many Requests | Rate limit exceeded |
| 500 | Internal Server Error | Unhandled exception |
| 503 | Service Unavailable | Server temporarily down/overloaded |
This map is your starting point. As you build more, you'll internalize which code fits each scenario — and that's the core skill of this lesson.
How it works step by step
Choosing the right status code isn't random; it follows a logical process. Here's a step-by-step decision framework you can apply to any endpoint:
- Identify the request type: Is it a POST (create), GET (read), PUT/PATCH (update), DELETE (remove)?
- Check if the request is well-formed: Did the client send valid JSON? Are all required fields present? If not, return
400 Bad Request. - Authenticate and authorize: Is the client logged in? If not, return
401. If logged in but lacks permission, return403. - Check resource existence: Does the URL point to an existing resource? If not, return
404. - Validate business rules: Even if the data is syntactically valid, does it violate your business logic (e.g., duplicate email)? Return
409or422. - Perform the action: If everything passes, execute the operation. Return the appropriate 2xx code.
- Handle unexpected exceptions: Wrap your code in a handler that returns
500for unknown errors — but you should never leak stack traces.
Blockquote callout: For a POST that creates a resource, the correct success code is 201 Created, not
200 OK. It tells the client a new resource exists at theLocationheader. Many beginners default to200and break client expectations.
For example, compare two endpoints:
POST /api/users→ 201 with the new user JSON (plus aLocationheader).POST /api/userswith a missing email field → 400 Bad Request.POST /api/userswith a duplicate email → 409 Conflict.GET /api/users/999where user 999 doesn't exist → 404 Not Found.
This systematic approach prevents lazy 500 responses and vague 200s. Your API becomes self-documenting, because the status code itself tells the client what happened.
Hands-on walkthrough
Let's put this into practice with a minimal but complete Python web service using Flask. First, install Flask if you don't have it:
pip install flask
Now create a file app.py with a RESTful API for managing tasks:
from flask import Flask, request, jsonify
app = Flask(__name__)
# In-memory storage
tasks = {}
next_id = 1
@app.route('/tasks', methods=['POST'])
def create_task():
global next_id
data = request.get_json(silent=True)
if data is None:
return jsonify({'error': 'Invalid JSON'}), 400
if not data.get('title'):
return jsonify({'error': 'Title is required'}), 400
task_id = next_id
next_id += 1
tasks[task_id] = {'id': task_id, 'title': data['title'], 'done': False}
return jsonify(tasks[task_id]), 201
@app.route('/tasks/<int:task_id>', methods=['GET'])
def get_task(task_id):
task = tasks.get(task_id)
if task is None:
return jsonify({'error': 'Task not found'}), 404
return jsonify(task), 200
@app.route('/tasks/<int:task_id>', methods=['PUT'])
def update_task(task_id):
task = tasks.get(task_id)
if task is None:
return jsonify({'error': 'Task not found'}), 404
data = request.get_json(silent=True)
if data is None:
return jsonify({'error': 'Invalid JSON'}), 400
task['title'] = data.get('title', task['title'])
task['done'] = data.get('done', task['done'])
return jsonify(task), 200
@app.route('/tasks/<int:task_id>', methods=['DELETE'])
def delete_task(task_id):
task = tasks.pop(task_id, None)
if task is None:
return jsonify({'error': 'Task not found'}), 404
return '', 204
if __name__ == '__main__':
app.run(debug=True)
Run it with python app.py, then test it using curl:
# Create a task — expect 201
curl -i -X POST http://localhost:5000/tasks -H "Content-Type: application/json" -d '{"title": "Build API"}'
# Output (abridged):
# HTTP/1.1 201 Created
# ...
# Get a non-existent task — expect 404
curl -i http://localhost:5000/tasks/99
# Output (abridged):
# HTTP/1.1 404 Not Found
# {"error": "Task not found"}
# Delete a task — expect 204
curl -i -X DELETE http://localhost:5000/tasks/1
# Output: HTTP/1.1 204 No Content (no body)
This example demonstrates the key mapping: 400 for bad input, 404 for missing resources, 201 for creation, 200 for reads, and 204 for delete without a body. Every status code you return is now meaningful to the client.
Compare options / when to choose what
When you're unsure between two similar codes, use the following comparisons:
| Scenario | Code A | Code B | Why choose which? |
|---|---|---|---|
| Missing authentication | 401 Unauthorized | 403 Forbidden | Use 401 when the client hasn't provided credentials; use 403 when credentials are provided but lack permission. |
| Invalid data sent | 400 Bad Request | 422 Unprocessable Entity | 400 for malformed JSON; 422 for syntactically valid but semantically invalid fields (e.g., negative age). Some teams use only 400. |
| Resource not found | 404 Not Found | 410 Gone | 404 when the resource never existed or is currently unavailable; 410 when it was intentionally removed and will not return. |
| Duplicate resource on create | 409 Conflict | 400 Bad Request | 409 is more semantic — the request is valid but conflicts with current state (duplicate email). Use 400 only for syntax errors. |
| Server overload | 503 Service Unavailable | 500 Internal Server Error | 503 indicates a temporary condition (e.g., maintenance); 500 is for unexpected errors that are likely permanent bugs. |
Pro tip: Some APIs deliberately return
404instead of403on unauthorized requests to avoid leaking that a resource exists. This is a design choice, but be consistent across your entire API to keep clients simple.
There are also variations in how you can implement status code handling:
- Use Flask's
abort()helper for quick responses:abort(404, description="Task not found")— but it's less flexible for custom JSON bodies. - Use a custom error handler to globally format all error responses in a consistent shape:
@app.errorhandler(404). - Use an API framework like FastAPI which comes with automatic validation error responses (422) and OpenAPI documentation that maps status codes to responses.
These are valid alternatives, but the core principle remains: choose the code that best communicates the outcome.
Troubleshooting & edge cases
A few situations trip up even experienced developers. Here are common mistakes and how to fix them:
- Returning 200 for a failed creation: If your database write fails, you must return a 5xx (e.g., 500 or 503) or a 4xx if it's a client data issue. Never mask failure as success.
- Using 404 for unauthorized access: It's a valid security pattern, but only if your API consistently hides existence. If you return 404 for one protected endpoint and 403 for another, clients get confusing responses.
- Forgetting the
Locationheader with 201: The spec says a 201 response should include aLocationheader pointing to the new resource. Some clients rely on this. - Returning a body with 204: A
204 No Contentmust have an empty body; sending a JSON body with 204 violates the spec and can break HTTP libraries. - Uncaught exceptions giving 500 with stack trace: In production, never send exception details to the client. Use a global error handler that logs and returns a generic 500 message.
- Redirect loops or wrong 3xx codes: Make sure 301 vs 302 are used correctly; a 301 is cached by browsers and changes the request method on redirect, while 302 does not.
If your curl tests show unexpected status codes, start by adding a custom error handler to log the exception.
from werkzeug.exceptions import HTTPException
@app.errorhandler(Exception)
def handle_error(e):
if isinstance(e, HTTPException):
return jsonify({'error': e.description}), e.code
app.logger.error('Exception occurred', exc_info=True)
return jsonify({'error': 'Internal server error'}), 500
This handler ensures clean JSON errors and prevents stack traces leaking.
What you learned & what's next
You've mastered the core skill of using HTTP status codes correctly. You can now:
- Explain the five families of status codes and their meaning.
- Apply a step-by-step decision process to map any API action to the correct response code.
- Build a complete Flask endpoint that returns
201,204,400,404, and500appropriately. - Avoid common pitfalls like returning bodies on 204 or mixing up 401/403.
This knowledge is the foundation for building resilient APIs. Next in the path, you'll dive into error handling and validation — learning how to structure error responses consistently and validate incoming data with Pydantic or Flask-JWT. That will make your APIs even more robust and client-friendly.
Keep this status code cheat sheet handy, and always ask yourself: “Does this response tell the client exactly what happened?” If yes, you're on the right track.
Practice recap
Now that you know the rules, harden your skills: extend the task API to support authentication and return 401 for missing tokens, and 403 for unauthorized access to other users' tasks. Also, add a validation for task titles longer than 100 characters and return 422 for that. Test each endpoint with curl and observe the status codes.
Common mistakes
- Returning 200 OK for a POST that creates a resource — should be 201 Created.
- Sending a JSON body with a 204 No Content response — the body must be empty.
- Using 500 for client errors like missing fields — use 400 or 422 instead.
- Confusing 401 Unauthorized with 403 Forbidden — use 401 for missing credentials, 403 for lacking permission.
Variations
- Use Flask's abort() helper for simple error responses.
- Implement custom error handlers for consistent JSON error formatting.
- Use FastAPI which automatically generates 422 validation responses and OpenAPI docs.
Real-world use cases
- A SaaS API returns 429 Too Many Requests when a user exceeds their rate limit, letting clients implement retry logic.
- An e-commerce API returns 409 Conflict when a user tries to place an order with an outdated product price, prompting a re-fetch.
- A mobile app backend returns 201 Created with a Location header for new user registrations, enabling automatic login flows.
Key takeaways
- HTTP status codes are a universal language: 4xx for client errors, 5xx for server errors, 2xx for success.
- Map each API action to the correct code: 201 for create, 200 for read/update, 204 for delete.
- Always ask 'whose fault is it?' before choosing a status code.
- Never leak stack traces in 500 responses — use a global error handler.
- Respect HTTP semantics: 204 has no body, 201 should include Location header.
- Consistency is key: choose a pattern (e.g., always 400 vs 422) and stick to it across your API.
Keep learning
Related tutorials, quizzes, and articles for this topic.
Discussion
Questions, corrections, and tips help everyone reading this page.
0 comments
Add a comment
No comments yet — start the thread.