How to Write API PUT & PATCH Tests in Python | Verify Data Updates with pytest × requests

test-automation

📌 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()

👨‍💻 About the Author

Working as a QA engineer handling API test automation with Python, pytest, and requests in real-world projects. All code used in this article is publicly available on GitHub and has been verified to work as described. View code on GitHub →

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

Both PUT and PATCH are “update” operations, but they differ in the scope of the update.

PUTPATCH
Update ScopeReplaces the entire resourceUpdates only the specified fields
Omitted FieldsBecome null or default valuesRemain unchanged at their original values
IdempotencyYes (same result no matter how many times sent)Implementation-dependent
Use CaseUpdating an entire user profileChanging 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-html

We’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 -s

Example 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 CaseContent
TC01Status code 200 should be returned on full update with PUT
TC02Detect 4xx/5xx with raise_for_status()
TC03Updated data should be correctly reflected in the PUT response
TC04Content-Type should be JSON
TC05Status code 200 should be returned on partial update with PATCH
TC06Specified field should be updated by PATCH
TC07Multiple fields should be updatable simultaneously with PATCH
TC08Response schema (types) should be correct
TC09Appropriate status should return for PUT to non-existent resource

The next article covers DELETE request API testing — verifying data deletion.

Copied title and URL