📌 Who This Article Is For
- Those who want to write DELETE request tests with Python and requests
- QA engineers who want to learn how to verify data deletion in API testing
- Those who want to complete CRUD API testing with pytest
- Those who want to implement robust DELETE tests including post-deletion 404 verification
✅ What You Will Learn
- The basic pattern for writing DELETE request API tests with pytest and requests
- The difference between status codes 200 and 204 on successful deletion and when to use each
- How to verify that 404 is returned after deletion
- How to write production-level test code using raise_for_status()
When writing API tests with Python, the final piece of CRUD testing is the DELETE request test. Using pytest and requests, you can automatically verify whether an API’s deletion logic works correctly.
This article covers everything from the basics of DELETE testing to post-deletion 404 verification with production-ready code.
- 00. API Testing and the Basics of DELETE Requests
- 01. API Test Environment with pytest + requests
- 02. How to Write API DELETE Tests in Python
- 03. API Test Post-Deletion State Verification
- 04. Negative Test Patterns for API DELETE
- 05. API Test DELETE Header Verification
- 06. Complete DELETE Test Code
- 07. Pitfalls & Lessons Learned
- 08. Best Practices for DELETE API Testing
- 09. Frequently Asked Questions (FAQ)
- 10. Summary
00. API Testing and the Basics of DELETE Requests
A DELETE request is used to “remove a resource.” It’s simpler than GET or POST, but verifying the state of the resource after deletion is critical.
| Verification Item | Content | Example |
|---|---|---|
| Status Code | Does 200 or 204 return on successful deletion? | 200 OK / 204 No Content |
| Post-Deletion Check | Does GET return 404 after deletion? | 404 Not Found |
| Non-Existent Resource | Does an appropriate error return when deleting a non-existent ID? | 404 Not Found |
| Response Body | Is the body empty for 204? | {} or empty |
💡 Key Takeaway:A DELETE test only becomes meaningful when you verify not just “the deletion succeeded” but also “the resource is no longer accessible after deletion.”
01. API Test Environment with pytest + requests
If you haven’t set up your environment yet, install the required libraries first.
pip install requests pytest pytest-htmlWe’ll use the free mock API JSONPlaceholder as our test target.
DELETE https://jsonplaceholder.typicode.com/users/1💡 Key Takeaway:JSONPlaceholder is a mock API, so executing DELETE does not actually remove any data. Because of this, GET requests after deletion may still return 200. In real APIs, it is common to write tests that expect 404 after a successful deletion.
02. How to Write API DELETE Tests in Python
Basic DELETE Test (200 Verification)
Send a DELETE request and confirm that 200 is returned.
import requests
BASE_URL = "https://jsonplaceholder.typicode.com"
def test_delete_user_status_code():
"""TC01: Status code 200 should be returned on DELETE"""
response = requests.delete(f"{BASE_URL}/users/1")
assert response.status_code == 200, \
f"Expected: 200, Got: {response.status_code}"
print(f"\n✅ TC01 PASS | Status code: {response.status_code}")Detecting 4xx/5xx Errors with raise_for_status()
def test_delete_user_raise_for_status():
"""TC02: Exception should be raised on 4xx/5xx errors"""
response = requests.delete(f"{BASE_URL}/users/1")
response.raise_for_status()
assert response.status_code == 200
print(f"\n✅ TC02 PASS | raise_for_status() passed normally")💡 Key Takeaway:Using raise_for_status() lets you detect 4xx/5xx errors as exceptions. However, since API testing sometimes intentionally verifies 4xx/5xx responses, it’s important to choose between raise_for_status() and status_code assertions depending on the situation.
Verifying the Response Body
Confirm that the DELETE response body is empty or an empty JSON object.
def test_delete_user_response_body():
"""TC03: DELETE response body should be empty or an empty JSON object"""
response = requests.delete(f"{BASE_URL}/users/1")
response.raise_for_status()
# When 204 No Content, calling json() raises JSONDecodeError — branch accordingly
if response.status_code == 204:
body = None
else:
body = response.json()
assert body in [None, {}], \
f"Expected empty body after deletion, got: {body}"
print(f"\n✅ TC03 PASS | Response body: {body}")💡 Key Takeaway:The DELETE response body varies by API design. The patterns include empty JSON `{}`、empty string、and 204 No Content (no body). Always branch on the status code before calling json().
03. API Test Post-Deletion State Verification
The most important part of DELETE testing is verifying that “the resource is no longer accessible after deletion.”
Verify That GET Returns 404 After Deletion
def test_delete_then_get_returns_404():
"""TC04: GET should return 404 after deletion"""
# Step 1: Delete the resource
delete_response = requests.delete(f"{BASE_URL}/users/1")
assert delete_response.status_code == 200, "Deletion failed"
# Step 2: Try GET after deletion
get_response = requests.get(f"{BASE_URL}/users/1")
# JSONPlaceholder may return 200 as a mock
# Real APIs should return 404
assert get_response.status_code in [200, 404], \
f"Expected 404 after deletion, got: {get_response.status_code}"
print(f"\n✅ TC04 PASS | GET status after deletion: {get_response.status_code}")💡 Key Takeaway:Post-deletion 404 verification is implemented as a two-step test (DELETE → GET). This is the most important verification in DELETE testing.
04. Negative Test Patterns for API DELETE
Deleting a Non-Existent Resource (404 Verification)
def test_delete_nonexistent_user():
"""TC05: Deleting a non-existent resource should return 404"""
response = requests.delete(f"{BASE_URL}/users/99999")
# JSONPlaceholder may return 200 as a mock
# Real APIs should return 404
assert response.status_code in [200, 404], \
f"Unexpected status code: {response.status_code}"
print(f"\n✅ TC05 PASS | Delete non-existent resource: {response.status_code}")⚠️ Note:JSONPlaceholder may return 200 even for non-existent IDs. In real APIs, verify that 404 is returned for non-existent resources.
Test That Accepts Both 200 and 204
def test_delete_user_accepts_200_or_204():
"""TC06: Successful deletion should return 200 or 204"""
response = requests.delete(f"{BASE_URL}/users/1")
# REST convention: deletion success is 200 or 204
assert response.status_code in [200, 204], \
f"Expected 200 or 204 on deletion, got: {response.status_code}"
print(f"\n✅ TC06 PASS | Status code: {response.status_code}")💡 Key Takeaway:The status code on successful deletion is 200 or 204 depending on the API design. Writing tests that accept both improves versatility.
05. API Test DELETE Header Verification
def test_delete_user_header():
"""TC07: Content-Type should be JSON (except for 204)"""
response = requests.delete(f"{BASE_URL}/users/1")
response.raise_for_status()
# 204 No Content may not include a Content-Type header
if response.status_code != 204:
content_type = response.headers.get("Content-Type", "")
assert "application/json" in content_type, \
f"Unexpected Content-Type: {content_type}"
print(f"\n✅ TC07 PASS | Content-Type: {content_type}")
else:
print(f"\n✅ TC07 PASS | 204 No Content — Content-Type check skipped")06. Complete DELETE Test Code
"""
DELETE API Test
Target: JSONPlaceholder (https://jsonplaceholder.typicode.com)
Framework: Python + requests + pytest
"""
import requests
BASE_URL = "https://jsonplaceholder.typicode.com"
def test_delete_user_status_code():
"""TC01: Status code 200 should be returned on DELETE"""
response = requests.delete(f"{BASE_URL}/users/1")
assert response.status_code == 200, \
f"Expected: 200, Got: {response.status_code}"
print(f"\n✅ TC01 PASS | status: {response.status_code}")
def test_delete_user_raise_for_status():
"""TC02: Exception should be raised on 4xx/5xx errors"""
response = requests.delete(f"{BASE_URL}/users/1")
response.raise_for_status()
assert response.status_code == 200
print(f"\n✅ TC02 PASS | raise_for_status() passed normally")
def test_delete_user_response_body():
"""TC03: DELETE response body should be empty or an empty JSON object"""
response = requests.delete(f"{BASE_URL}/users/1")
response.raise_for_status()
if response.status_code == 204:
body = None
else:
body = response.json()
assert body in [None, {}]
print(f"\n✅ TC03 PASS | body: {body}")
def test_delete_then_get_returns_404():
"""TC04: GET should return 404 after deletion"""
delete_response = requests.delete(f"{BASE_URL}/users/1")
assert delete_response.status_code == 200
get_response = requests.get(f"{BASE_URL}/users/1")
assert get_response.status_code in [200, 404]
print(f"\n✅ TC04 PASS | GET status after deletion: {get_response.status_code}")
def test_delete_nonexistent_user():
"""TC05: Deleting a non-existent resource should return 404"""
response = requests.delete(f"{BASE_URL}/users/99999")
assert response.status_code in [200, 404]
print(f"\n✅ TC05 PASS | status: {response.status_code}")
def test_delete_user_accepts_200_or_204():
"""TC06: Successful deletion should return 200 or 204"""
response = requests.delete(f"{BASE_URL}/users/1")
assert response.status_code in [200, 204]
print(f"\n✅ TC06 PASS | status: {response.status_code}")
def test_delete_user_header():
"""TC07: Content-Type should be JSON (except for 204)"""
response = requests.delete(f"{BASE_URL}/users/1")
response.raise_for_status()
if response.status_code != 204:
content_type = response.headers.get("Content-Type", "")
assert "application/json" in content_type
print(f"\n✅ TC07 PASS | Content-Type: {content_type}")
else:
print(f"\n✅ TC07 PASS | 204 — Content-Type check skipped")Run Command
pytest test_delete_api.py -v -sExample Output
test_delete_api.py::test_delete_user_status_code PASSED
test_delete_api.py::test_delete_user_raise_for_status PASSED
test_delete_api.py::test_delete_user_response_body PASSED
test_delete_api.py::test_delete_then_get_returns_404 PASSED
test_delete_api.py::test_delete_nonexistent_user PASSED
test_delete_api.py::test_delete_user_accepts_200_or_204 PASSED
test_delete_api.py::test_delete_user_header PASSED
7 passed in 3.56s ✅07. Pitfalls & Lessons Learned
Here are the key issues I encountered during implementation. I hope this helps others who run into the same problems.
① Unsure whether successful DELETE should return 200 or 204
I was confused about whether a successful DELETE response should return 200 or 204. It depends on the API design.
# ❌ Only expecting 200
assert response.status_code == 200
# ✅ Accept both 200 and 204
assert response.status_code in [200, 204]
# 200 OK → Has response body (e.g., info about the deleted resource)
# 204 No Content → No response body (simple deletion confirmation)💡 Key Takeaway:Always check the API documentation to confirm the expected status code before writing tests. JSONPlaceholder returns 200.
② Getting a JSONDecodeError with response.json() on 204
Calling response.json() on a 204 No Content response raised a JSONDecodeError because the body was empty.
# ❌ Calling json() on 204 causes an error
body = response.json() # JSONDecodeError!
# ✅ Branch on status code first
if response.status_code == 204:
print("No response body (204 No Content)")
else:
body = response.json()
print(f"Response body: {body}")⚠️ Note:204 No Content has no response body. Always check the status code before calling json().
③ Can’t verify post-deletion 404 with a mock API
When testing against JSONPlaceholder, GET requests after DELETE still returned 200, making it impossible to verify the post-deletion 404.
# JSONPlaceholder is a mock so GET after DELETE still returns 200
requests.delete(f"{BASE_URL}/users/1")
response = requests.get(f"{BASE_URL}/users/1")
print(response.status_code) # → 200 (404 expected in a real API)💡 Key Takeaway:Post-deletion 404 verification requires an API where data is actually deleted. Test against Restful Booker or your own API for this.
④ Including a request body in a DELETE request
I tried to include a request body in a DELETE request, but most REST APIs identify the deletion target via the URL, so a body is generally not needed.
# ❌ Including a body in DELETE is uncommon in most REST APIs
response = requests.delete(url, json={"id": 1})
# ✅ Specify the target via URL only
response = requests.delete(f"{BASE_URL}/users/1")💡 Key Takeaway:DELETE typically specifies the target via URL path parameters, so a request body is not needed in most REST APIs. However, some APIs (e.g., Elasticsearch) do use a body for deletion conditions. Always check the API documentation.
⑤ Forgetting to verify behavior when re-deleting an already deleted resource
I forgot to check what response is returned when trying to delete a resource that had already been deleted.
# Re-deleting an already deleted resource
response1 = requests.delete(f"{BASE_URL}/users/1") # 1st: success (200)
response2 = requests.delete(f"{BASE_URL}/users/1") # 2nd: already gone
assert response1.status_code == 200
# Real APIs typically return 404 on the second attempt
assert response2.status_code in [200, 404]💡 Key Takeaway:Re-deletion testing requires a real API where data is actually removed. This cannot be verified with a mock API.
08. Best Practices for DELETE API Testing
There are three key things to verify in DELETE API testing.
| Verification Item | Content | Priority |
|---|---|---|
| ① Status Code | 200 or 204 returns on successful deletion | ⭐⭐⭐ Required |
| ② Post-Deletion 404 | GET returns 404 after deletion | ⭐⭐⭐ Important |
| ③ Error Handling | Appropriate error returns when deleting a non-existent resource | ⭐⭐ Recommended |
Verifying these three points ensures a high level of quality assurance for your DELETE API. In particular, verifying that GET returns 404 after deletion goes beyond just “the deletion succeeded” to confirm “the resource is truly gone.” This should always be included in real-world testing.
💡 Key Takeaway:Implementing all four test types — GET, POST, PUT/PATCH, and DELETE — produces a complete API test suite covering all CRUD operations. This is a very powerful portfolio piece that clearly demonstrates your API testing skills.
09. Frequently Asked Questions (FAQ)
Q. What is the minimum I need to verify in a DELETE test?
A. At minimum, verify “status code is 200 or 204.” Adding “GET returns 404 after deletion” will bring it to a production-level standard.
Q. Should the DELETE status code be 200 or 204?
A. Both are valid — it depends on the API design. 200 is used when the response returns information about the deleted resource, and 204 is used when nothing is returned. Always check the API documentation before writing tests.
Q. Does a DELETE request need a request body?
A. Generally not. DELETE identifies the target via URL path parameters (e.g., /users/1). However, some APIs include deletion reasons or conditions in the body. Always check the API documentation.
Q. Which API can I use to practice post-deletion 404 verification?
A. JSONPlaceholder is a mock API so post-deletion 404 can’t be verified. Try reqres.in, your own API, or the booking deletion endpoint of Restful Booker.
Q. How many test cases does a full CRUD test suite have?
A. The test cases implemented in this series are: GET (9), POST (9), PUT/PATCH (9), DELETE (7), totaling 34 cases. This is more than enough to demonstrate “I can handle all CRUD operations” in a portfolio.
10. Summary
When implementing DELETE API tests in Python, verifying the status code and post-deletion state using pytest and requests is the production standard. DELETE tests are simple, but verifying that 404 is returned after deletion completes a full CRUD test suite.
This article covered how to implement API DELETE tests using pytest and requests.
| Test Case | Content |
|---|---|
| TC01 | Status code 200 should be returned on DELETE |
| TC02 | Detect 4xx/5xx with raise_for_status() |
| TC03 | DELETE response body should be empty or an empty JSON object |
| TC04 | GET should return 404 after deletion |
| TC05 | Deleting a non-existent resource should return 404 |
| TC06 | Successful deletion should return 200 or 204 |
| TC07 | Content-Type should be JSON (except for 204) |
With GET, POST, PUT/PATCH, and DELETE all implemented, CRUD testing for all API operations is now complete. The next article covers the series wrap-up: API test design principles for real-world projects.

