📌 Who This Article Is For
- Those who want to write PUT and PATCH request tests with Python and requests
- QA engineers who want to learn how to verify data updates in API testing
- Those who want to understand the difference between PUT and PATCH and apply it in tests
- Those who want to complete CRUD API testing with pytest
✅ What You Will Learn
- The difference between PUT (full update) and PATCH (partial update) and when to use each
- The basic pattern for writing PUT and PATCH API tests with pytest and requests
- How to verify responses after updates (status, body, headers)
- How to write production-level test code using raise_for_status()
When writing API tests with Python and pytest, both PUT and PATCH are requests that “update data.” Using pytest and requests, we can automatically verify “Does a full update work correctly?” and “Does a partial update change only the specified fields?”
This article covers everything from the difference between PUT and PATCH to production-ready test code.
- 00. The Difference Between PUT and PATCH in API Testing
- 01. API Test Environment with pytest + requests
- 02. How to Write API PUT Tests in Python
- 03. How to Write API PATCH Tests in Python
- 04. API Test Schema Validation for PUT and PATCH
- 05. Negative Test Patterns for API PUT and PATCH
- 06. Complete PUT / PATCH Test Code
- 07. Pitfalls & Lessons Learned
- 08. Frequently Asked Questions (FAQ)
- 09. Summary
00. The Difference Between PUT and PATCH in API Testing
Both PUT and PATCH are “update” operations, but they differ in the scope of the update.
| PUT | PATCH | |
|---|---|---|
| Update Scope | Replaces the entire resource | Updates only the specified fields |
| Omitted Fields | Become null or default values | Remain unchanged at their original values |
| Idempotency | Yes (same result no matter how many times sent) | Implementation-dependent |
| Use Case | Updating an entire user profile | Changing only an email address |
💡 Key Takeaway:In real-world projects, PATCH is used more frequently. It avoids having to send all fields just to change one.
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.
# PUT request (full update)
PUT https://jsonplaceholder.typicode.com/users/1
# PATCH request (partial update)
PATCH https://jsonplaceholder.typicode.com/users/1💡 Key Takeaway:JSONPlaceholder is a mock API so data is not actually updated. However, responses are returned in the same format as a real API, making it ideal for learning.
02. How to Write API PUT Tests in Python
Basic PUT Test (200 Verification)
Send a PUT request for a full update and confirm that 200 is returned.
import requests
BASE_URL = "https://jsonplaceholder.typicode.com"
def test_put_user_status_code():
"""TC01: Status code 200 should be returned on full update with PUT"""
updated_user = {
"id": 1,
"name": "Updated Yoshitsugu",
"username": "yoshitsugu728",
"email": "updated@example.com",
"phone": "090-9999-9999",
"website": "qa-auto-lab.com"
}
response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
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_put_user_raise_for_status():
"""TC02: Exception should be raised on 4xx/5xx errors"""
updated_user = {"id": 1, "name": "Updated User", "email": "updated@example.com"}
response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
response.raise_for_status()
assert response.status_code == 200
print(f"\n✅ TC02 PASS | raise_for_status() passed normally")💡 Key Takeaway:response.raise_for_status() is an essential one-liner in real-world API testing. Always add it to PUT and PATCH tests as well.
Verifying the PUT Response Body
Confirm that the updated data is correctly reflected in the response.
def test_put_user_response_body():
"""TC03: Updated data should be correctly reflected in the PUT response"""
updated_user = {
"id": 1,
"name": "Updated Yoshitsugu",
"email": "updated@example.com"
}
response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
response.raise_for_status()
body = response.json()
assert body["name"] == updated_user["name"], f"name mismatch: {body['name']}"
assert body["email"] == updated_user["email"], f"email mismatch: {body['email']}"
print(f"\n✅ TC03 PASS | Updated name: {body['name']}")Verifying Response Headers
def test_put_user_header():
"""TC04: Content-Type should be JSON"""
updated_user = {"id": 1, "name": "Test", "email": "test@example.com"}
response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
response.raise_for_status()
content_type = response.headers.get("Content-Type", "")
assert "application/json" in content_type, \
f"Unexpected Content-Type: {content_type}"
print(f"\n✅ TC04 PASS | Content-Type: {content_type}")💡 Key Takeaway:The Content-Type returns as application/json; charset=utf-8, so use in for partial matching instead of ==.
03. How to Write API PATCH Tests in Python
Basic PATCH Test (Partial Update Verification)
Send a PATCH request to update only some fields and confirm that 200 is returned.
def test_patch_user_status_code():
"""TC05: Status code 200 should be returned on partial update with PATCH"""
patch_data = {
"email": "patched@example.com" # Update email only
}
response = requests.patch(f"{BASE_URL}/users/1", json=patch_data)
assert response.status_code == 200, \
f"Expected: 200, Got: {response.status_code}"
print(f"\n✅ TC05 PASS | Status code: {response.status_code}")Verifying the PATCH Response Body
Confirm that the specified field has been updated.
def test_patch_user_response_body():
"""TC06: Specified field should be updated by PATCH"""
patch_data = {"email": "patched@example.com"}
response = requests.patch(f"{BASE_URL}/users/1", json=patch_data)
response.raise_for_status()
body = response.json()
assert body["email"] == patch_data["email"], \
f"email was not updated: {body['email']}"
print(f"\n✅ TC06 PASS | Updated email: {body['email']}")💡 Key Takeaway:The key feature of PATCH is that it changes only the specified fields. For more thorough testing, also verify that unspecified fields have not changed.
Updating Multiple Fields with PATCH
def test_patch_user_multiple_fields():
"""TC07: Multiple fields should be updatable simultaneously with PATCH"""
patch_data = {
"name": "Patched Name",
"email": "patched@example.com"
}
response = requests.patch(f"{BASE_URL}/users/1", json=patch_data)
response.raise_for_status()
body = response.json()
assert body["name"] == patch_data["name"], f"name mismatch: {body['name']}"
assert body["email"] == patch_data["email"], f"email mismatch: {body['email']}"
print(f"\n✅ TC07 PASS | Updated: {body['name']} / {body['email']}")04. API Test Schema Validation for PUT and PATCH
Also verify that the updated resource’s response has the correct types.
def test_put_user_schema():
"""TC08: PUT response schema (types) should be correct"""
updated_user = {"id": 1, "name": "Updated User", "email": "updated@example.com"}
response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
response.raise_for_status()
body = response.json()
assert isinstance(body["id"], int), f"id should be int: {type(body['id'])}"
assert isinstance(body["name"], str), f"name should be str: {type(body['name'])}"
assert isinstance(body["email"], str), f"email should be str: {type(body['email'])}"
print(f"\n✅ TC08 PASS | Schema validation complete")05. Negative Test Patterns for API PUT and PATCH
PUT to a Non-Existent Resource (404 Verification)
def test_put_nonexistent_user():
"""TC09: Appropriate status should return when PUT-ing a non-existent resource"""
updated_user = {"id": 99999, "name": "Ghost User", "email": "ghost@example.com"}
response = requests.put(f"{BASE_URL}/users/99999", json=updated_user)
# JSONPlaceholder may return 200 as a mock
# Real APIs would return 404
assert response.status_code in [200, 404], \
f"Unexpected status code: {response.status_code}"
print(f"\n✅ TC09 PASS | PUT to 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.
06. Complete PUT / PATCH Test Code
"""
PUT / PATCH API Test
Target: JSONPlaceholder (https://jsonplaceholder.typicode.com)
Framework: Python + requests + pytest
"""
import requests
BASE_URL = "https://jsonplaceholder.typicode.com"
def test_put_user_status_code():
"""TC01: Status code 200 should be returned on full update with PUT"""
updated_user = {"id": 1, "name": "Updated Yoshitsugu", "email": "updated@example.com"}
response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
assert response.status_code == 200, \
f"Expected: 200, Got: {response.status_code}"
print(f"\n✅ TC01 PASS | status: {response.status_code}")
def test_put_user_raise_for_status():
"""TC02: Exception should be raised on 4xx/5xx errors"""
updated_user = {"id": 1, "name": "Updated User", "email": "updated@example.com"}
response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
response.raise_for_status()
assert response.status_code == 200
print(f"\n✅ TC02 PASS | raise_for_status() passed normally")
def test_put_user_response_body():
"""TC03: Updated data should be correctly reflected in the PUT response"""
updated_user = {"id": 1, "name": "Updated Yoshitsugu", "email": "updated@example.com"}
response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
response.raise_for_status()
body = response.json()
assert body["name"] == updated_user["name"]
assert body["email"] == updated_user["email"]
print(f"\n✅ TC03 PASS | name: {body['name']}")
def test_put_user_header():
"""TC04: Content-Type should be JSON"""
updated_user = {"id": 1, "name": "Test", "email": "test@example.com"}
response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
response.raise_for_status()
content_type = response.headers.get("Content-Type", "")
assert "application/json" in content_type
print(f"\n✅ TC04 PASS | Content-Type: {content_type}")
def test_patch_user_status_code():
"""TC05: Status code 200 should be returned on partial update with PATCH"""
patch_data = {"email": "patched@example.com"}
response = requests.patch(f"{BASE_URL}/users/1", json=patch_data)
assert response.status_code == 200, \
f"Expected: 200, Got: {response.status_code}"
print(f"\n✅ TC05 PASS | status: {response.status_code}")
def test_patch_user_response_body():
"""TC06: Specified field should be updated by PATCH"""
patch_data = {"email": "patched@example.com"}
response = requests.patch(f"{BASE_URL}/users/1", json=patch_data)
response.raise_for_status()
body = response.json()
assert body["email"] == patch_data["email"]
print(f"\n✅ TC06 PASS | email: {body['email']}")
def test_patch_user_multiple_fields():
"""TC07: Multiple fields should be updatable simultaneously with PATCH"""
patch_data = {"name": "Patched Name", "email": "patched@example.com"}
response = requests.patch(f"{BASE_URL}/users/1", json=patch_data)
response.raise_for_status()
body = response.json()
assert body["name"] == patch_data["name"]
assert body["email"] == patch_data["email"]
print(f"\n✅ TC07 PASS | {body['name']} / {body['email']}")
def test_put_user_schema():
"""TC08: PUT response schema (types) should be correct"""
updated_user = {"id": 1, "name": "Updated User", "email": "updated@example.com"}
response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
response.raise_for_status()
body = response.json()
assert isinstance(body["id"], int)
assert isinstance(body["name"], str)
assert isinstance(body["email"], str)
print(f"\n✅ TC08 PASS | Schema validation complete")
def test_put_nonexistent_user():
"""TC09: Appropriate status should return for PUT to non-existent resource"""
updated_user = {"id": 99999, "name": "Ghost User", "email": "ghost@example.com"}
response = requests.put(f"{BASE_URL}/users/99999", json=updated_user)
assert response.status_code in [200, 404]
print(f"\n✅ TC09 PASS | status: {response.status_code}")Run Command
pytest test_put_patch_api.py -v -sExample Output
test_put_patch_api.py::test_put_user_status_code PASSED
test_put_patch_api.py::test_put_user_raise_for_status PASSED
test_put_patch_api.py::test_put_user_response_body PASSED
test_put_patch_api.py::test_put_user_header PASSED
test_put_patch_api.py::test_patch_user_status_code PASSED
test_put_patch_api.py::test_patch_user_response_body PASSED
test_put_patch_api.py::test_patch_user_multiple_fields PASSED
test_put_patch_api.py::test_put_user_schema PASSED
test_put_patch_api.py::test_put_nonexistent_user PASSED
9 passed in 4.23s ✅07. Pitfalls & Lessons Learned
Here are the key issues I encountered during implementation. I hope this helps others who run into the same problems.
① Omitted fields become null with PUT
When I sent only some fields with PUT, the fields I didn’t send became null or empty. Because PUT replaces the entire resource, not sending all fields causes data to be lost.
# ❌ Sending only some fields with PUT can wipe other fields
response = requests.put(url, json={"name": "Updated"})
# → phone, website etc. may become null!
# ✅ Always include all fields with PUT
updated_user = {
"id": 1,
"name": "Updated",
"email": "original@example.com", # Include unchanged fields too
"phone": "090-1234-5678",
"website": "qa-auto-lab.com"
}
response = requests.put(url, json=updated_user)💡 Key Takeaway:If you only want to change some fields, PATCH is the correct choice, not PUT.
② Forgetting to check that non-updated fields haven’t changed after PATCH
When updating a single field with PATCH, I forgot to verify that the other fields had not been unintentionally changed.
# ❌ Only verifying the updated field is insufficient
patch_data = {"email": "new@example.com"}
response = requests.patch(url, json=patch_data)
body = response.json()
assert body["email"] == "new@example.com" # Not enough
# ✅ Also verify that unchanged fields remain the same
assert body["email"] == "new@example.com" # Should be updated
assert body["name"] == "Leanne Graham" # Should NOT have changed💡 Key Takeaway:A complete PATCH test verifies not just “the changed field was changed” but also “the unchanged fields were not changed.”
③ Confusing PUT and PATCH endpoints
Since PUT and PATCH use the same endpoint URL, I sometimes mixed up the methods when writing test code.
# ❌ Using wrong method
response = requests.put(url, json=patch_data) # Meant to use PATCH
# ✅ Use methods explicitly and deliberately
# Full update → PUT
response = requests.put(f"{BASE_URL}/users/1", json=full_data)
# Partial update → PATCH
response = requests.patch(f"{BASE_URL}/users/1", json=partial_data)⚠️ Note:The URL is the same but the HTTP method is different. Include put/patch clearly in function names to avoid confusion.
④ Forgetting to verify that the ID hasn’t changed after update
After a PUT update, I forgot to verify that the ID had not been accidentally changed.
# ✅ Verify the ID remains the same after update
updated_user = {"id": 1, "name": "Updated", "email": "updated@example.com"}
response = requests.put(f"{BASE_URL}/users/1", json=updated_user)
body = response.json()
assert body["id"] == 1, "ID should not change"
assert body["name"] == "Updated", "name should be updated"
assert body["email"] == "updated@example.com", "email should be updated"💡 Key Takeaway:Verifying that the ID hasn’t changed after an update is a test that is often overlooked in real projects. Always add it.
⑤ Cannot verify idempotency with a mock API
When trying to verify PUT’s idempotency characteristic (“same result no matter how many times sent”) with JSONPlaceholder, the mock returned the same response every time, making it impossible to actually verify.
# Idempotency verification (for use with a real API)
updated_user = {"id": 1, "name": "Idempotent User", "email": "idem@example.com"}
response1 = requests.put(url, json=updated_user) # First request
response2 = requests.put(url, json=updated_user) # Second request (same)
# Verify the same result is returned
assert response1.json() == response2.json(), "PUT should be idempotent"⚠️ Note:Idempotency verification requires testing against an API where data actually changes. JSONPlaceholder doesn’t persist data, so this can’t be verified there.
08. Frequently Asked Questions (FAQ)
Q. Which should I use — PUT or PATCH?
A. Use PATCH if you only want to change some fields, and PUT if you want to replace the entire resource. In real-world projects, PATCH is used more frequently because it avoids having to send all fields just to change one.
Q. Should the status code for a successful PUT be 200 or 204?
A. It depends on the API design. Some APIs return 200 with the updated resource in the body, and some return 204 with no body. Always check the API documentation before writing tests. JSONPlaceholder returns 200.
Q. How do I verify that other fields haven’t changed after a PATCH?
A. The most reliable approach is to GET the data before the PATCH, then GET again after and compare. This lets you verify that only the specified field changed and all others remain at their original values.
Q. What is idempotency?
A. It’s the property where sending the same request multiple times always produces the same result. PUT is idempotent, but POST is not because each call creates a new resource. It’s an important concept in API quality verification.
Q. Should I write PUT/PATCH tests after GET and POST tests?
A. Yes. Implementing in CRUD order — GET → POST → PUT/PATCH → DELETE — creates a natural test flow. Update tests assume existing data, so confirm GET and POST work first.
09. Summary
When implementing PUT and PATCH API tests in Python, verifying the status code, response body, and schema using pytest and requests is the production standard. The key is designing tests with awareness of the difference between PUT (full update) and PATCH (partial update).
This article covered how to implement API PUT and PATCH tests using pytest and requests.
| Test Case | Content |
|---|---|
| TC01 | Status code 200 should be returned on full update with PUT |
| TC02 | Detect 4xx/5xx with raise_for_status() |
| TC03 | Updated data should be correctly reflected in the PUT response |
| TC04 | Content-Type should be JSON |
| TC05 | Status code 200 should be returned on partial update with PATCH |
| TC06 | Specified field should be updated by PATCH |
| TC07 | Multiple fields should be updatable simultaneously with PATCH |
| TC08 | Response schema (types) should be correct |
| TC09 | Appropriate status should return for PUT to non-existent resource |
The next article covers DELETE request API testing — verifying data deletion.

