Use File IDs

Su Suriya Ravichandran Updated 10 Nov 2026
6 min read ·Lesson 116 of 200

Referencing Files Instead of Resending Them

Lesson 115 uploaded a file and got back an ID. This lesson makes that ID useful in a real application. The goal is simple: a file should be uploaded once, and every later request should reuse its ID. Doing this well requires three things:

  1. knowing whether a file has already been uploaded,
  2. confirming that the stored ID still works, and
  3. re-uploading only when necessary.

The first requirement is the hardest. Two uploads of the same bytes produce two different IDs, so the application needs its own way to recognize a file it has seen before.


Recognizing a File by Its Content

A content hash is a fingerprint of the bytes. The SHA-256 algorithm produces the same value for identical content, and a different value for any change, so it identifies files reliably regardless of their names:

import hashlib

digest = hashlib.sha256(data).hexdigest()

Store the mapping from digest to file ID in your own database or in a local file. Then the lookup becomes:

digest seen before? ──yes──► still valid? ──yes──► use file_id
        │                         │
        no                        no
        ▼                         ▼
   upload, store mapping    re-upload, update mapping

Using the content hash rather than the file name prevents two problems: a renamed copy of a file would be uploaded again, and two different files that share a name would be confused with each other.


Confirming a Stored ID Still Works

Stored IDs can become invalid. A file may have been deleted by a cleanup job, by another team member, or by a retention rule. Before reusing an ID, confirm it exists. The SDK raises anthropic.NotFoundError for a missing file (Lesson 9), which is the signal to upload again.

import anthropic


def file_exists(client, file_id: str) -> bool:
    try:
        client.files.retrieve_metadata(file_id)
    except anthropic.NotFoundError:
        return False
    return True

retrieve_metadata is the method name in current SDK versions. As with the other Files API methods in Lesson 115, confirm it against the reference for your installed version.

Note: a check costs a request, so it is worth doing before a batch of work, not before every single question. A failed message that reports a missing file is another valid signal to re-upload.


A Reusable File Cache

Putting the pieces together gives a small cache. It stores digests and file IDs in a JSON file, checks each stored ID before use, and re-uploads when needed:

import json
from pathlib import Path

CACHE_PATH = Path("file_cache.json")


def load_cache() -> dict:
    return json.loads(CACHE_PATH.read_text(encoding="utf-8")) if CACHE_PATH.exists() else {}


def save_cache(cache: dict) -> None:
    CACHE_PATH.write_text(json.dumps(cache, indent=2), encoding="utf-8")


def file_id_for(client, name: str, data: bytes, media_type: str) -> str:
    cache = load_cache()
    digest = hashlib.sha256(data).hexdigest()
    entry = cache.get(digest)
    if entry and file_exists(client, entry["file_id"]):
        return entry["file_id"]

    uploaded = client.files.upload(file=(name, data, media_type))
    cache[digest] = {"file_id": uploaded.id, "name": name}
    save_cache(cache)
    return uploaded.id

The cache file contains only digests, names, and IDs, not the file contents. Still, file names can reveal information, so protect the cache file like the data it describes.


Practical Example: Reusing a Document Across Sessions

This example uses the cache to upload a PDF only when it is new or missing, then asks two questions. Running it twice shows the difference: the second run reuses the stored ID.

"""file_cache.py: upload each document once, and reuse its file ID afterwards."""

import hashlib
import json
import sys
from pathlib import Path

import anthropic
from anthropic import Anthropic

client = Anthropic()
CACHE_PATH = Path("file_cache.json")


def load_cache() -> dict:
    return json.loads(CACHE_PATH.read_text(encoding="utf-8")) if CACHE_PATH.exists() else {}


def save_cache(cache: dict) -> None:
    CACHE_PATH.write_text(json.dumps(cache, indent=2), encoding="utf-8")


def file_exists(file_id: str) -> bool:
    try:
        client.files.retrieve_metadata(file_id)
    except anthropic.NotFoundError:
        return False
    return True


def file_id_for(name: str, data: bytes) -> str:
    if not data.startswith(b"%PDF-"):
        raise ValueError(f"{name} is not a PDF")
    cache = load_cache()
    digest = hashlib.sha256(data).hexdigest()
    entry = cache.get(digest)
    if entry and file_exists(entry["file_id"]):
        print(f"reusing {entry['file_id']} for {name}")
        return entry["file_id"]

    uploaded = client.files.upload(file=(name, data, "application/pdf"))
    cache[digest] = {"file_id": uploaded.id, "name": name}
    save_cache(cache)
    print(f"uploaded {name} as {uploaded.id}")
    return uploaded.id


def ask(file_id: str, question: str) -> str:
    message = client.messages.create(
        model="claude-opus-5-5",
        max_tokens=1024,
        messages=[{"role": "user", "content": [
            {"type": "document", "source": {"type": "file", "file_id": file_id}},
            {"type": "text", "text": question},
        ]}],
    )
    return "".join(block.text for block in message.content if block.type == "text").strip()


if __name__ == "__main__":
    path = Path(sys.argv[1])
    file_id = file_id_for(path.name, path.read_bytes())
    print("Q1:", ask(file_id, "What is this document about, in one sentence?"))
    print("Q2:", ask(file_id, "Who is the intended audience?"))

Example output:

$ python file_cache.py handbook.pdf      # first run
uploaded handbook.pdf as file_01XYZ...
Q1: The handbook explains how new employees set up their accounts and tools.
Q2: New employees in their first month.

$ python file_cache.py handbook.pdf      # second run
reusing file_01XYZ... for handbook.pdf
Q1: ...

How It Works

  1. The digest is computed from the bytes, so the same document is recognized whatever its name, and a changed document produces a new digest and a new upload.
  2. The stored ID is checked with file_exists() before it is used. A file deleted elsewhere is detected here, rather than failing in the middle of a conversation.
  3. A missing or new file is uploaded, and its digest and ID are saved, so the next run finds it.
  4. Both questions use the same ID, so the file is referenced rather than resent.
  5. The cache stores only identifiers and names, never the document itself.

Common Mistakes

Identifying Files by Name

What happens: a different file with the same name is sent the cached ID of another document.

Fix: identify files by their content hash.

Trusting Stored IDs Forever

What happens: requests fail with not-found errors after cleanup jobs delete files.

Fix: verify the ID before use, and re-upload when it is missing.

Checking Existence for Every Question

What happens: each question costs an extra request, and a conversation becomes slower.

Fix: check once per session or per batch of work.

Storing Sensitive Information in the Cache

What happens: file names or cache entries reveal confidential document titles to anyone who can read the cache file.

Fix: protect the cache with the same controls as the data, or store it in your database with access rules.

Sharing File IDs Across Users Without Checks

What happens: one user can reference another user's file if they learn its ID.

Fix: keep file IDs tied to their owners, and check ownership before use, just as with chat sessions (Lesson 63).


Best Practices

  • Identify files by content hash, never by name alone.
  • Verify stored IDs before reusing them.
  • Keep the mapping in a database when there are many users or files.
  • Tie file IDs to owners, and check ownership on every use.
  • Delete unused files according to a retention policy.
  • Protect the cache like the data it describes.

Exercise

  1. Run the example twice, then change one byte in the PDF and run it again. What happens on each run?
  2. Add a --forget option that removes the cache entry for a given file, and forces a new upload.
  3. Explain why file_exists() catches NotFoundError instead of letting it propagate.
  4. Modify file_id_for() to store the owner's user ID alongside each entry, and refuse to return an entry for a different user.
Show answers for steps 1 and 3

Step 1: the first run after the change computes a new digest, finds no entry for it, and uploads the modified file under a new ID. The original entry stays in the cache, and the original file still works if it still exists. Digests make changes visible automatically, without comparing names or dates.

Step 3: a missing file is an expected situation, not an error in the program. Catching NotFoundError turns it into a normal False answer, which the caller uses to decide to re-upload. Other errors, such as a network failure, should still propagate, because they mean something else is wrong and retrying the upload would not help.

0 Comments

Reviewed before they appear

No comments yet.

Anthropic SDK Using Python
Anthropic SDK Intro Claude API Intro SDK Requirements Install Anthropic SDK Upgrade the SDK Create an API Key Set API Keys Use Environment Variables Import Anthropic Create a Client Send Your First Message Choose a Claude Model Set Max Tokens Read a Response Print Response Text Understand Message Blocks Use System Prompts Send User Messages Set Temperature Handle Stop Reasons Messages API Intro Create Messages Use Message Roles Send Conversation History Continue a Conversation Build a Chat Loop Reset Chat History Use Multiple Turns Inspect Message IDs Read Usage Data Write Clear Prompts Set a System Prompt Add Context to Prompts Use Prompt Templates Add Examples to Prompts Request JSON Output Use XML Tags Control Response Length Improve Prompt Quality Avoid Prompt Injection Read Text Blocks Read Content Blocks Check Stop Reasons Count Input Tokens Count Output Tokens Count Tokens Before Sending Limit Output Tokens Use Stop Sequences Read Request IDs Parse API Responses Streaming Intro Stream Text Output Use Stream Events Use Stream Helpers Get Final Messages Stream Async Responses Handle Stream Errors Build a Live Chat Stream Long Responses Close Streams Safely Build a Chatbot Keep Chat History Manage Chat Sessions Add Chat Memory Build a CLI Chatbot Build a Web Chatbot Use Flask with Claude Use FastAPI with Claude Return JSON from an API Add Chat Error Messages Async SDK Intro Create an Async Client Send Async Messages Use asyncio Run Parallel Requests Stream Async Text Handle Async Errors Set Async Timeouts Close Async Clients Choose Sync or Async Tool Use Intro Define a Tool Create Tool Schemas Send Tools to Claude Read Tool Calls Run Python Tools Return Tool Results Continue After Tool Use Control Tool Choice Handle Tool Errors Use the beta_tool Decorator Create a Tool Runner Add Tool Docstrings Validate Tool Inputs Use Multiple Tools Limit Tool Calls Log Tool Activity Build a Weather Tool Build a Search Tool Build a Calculator Tool Structured Output Intro Request JSON Data Validate JSON Output Use Pydantic Models Handle Invalid JSON Extract Fields from Text Generate Typed Data Use Image URLs Send Base64 Images Read PDFs with Claude Use Output Schemas Build Data Extraction Test Structured Responses Send Image Inputs Upload Files Use File IDs Process Text Files Summarize Documents Extract Table Data Handle File Errors Read Cache Usage Reduce Repeated Input Costs Debug Cache Misses Prompt Caching Intro Enable Prompt Caching Cache System Prompts Cache Long Context Test Cache Performance Use Cache Breakpoints Cache Best Practices Message Batches Intro Create a Message Batch Set Batch IDs Check Batch Status List Message Batches Get Batch Results Process Batch Outputs Handle Failed Batch Items Use Batches for Bulk Tasks Compare Batches and Requests Handle API Errors Handle Rate Limits Retry Failed Requests Set Request Timeouts Handle Network Errors Use Exponential Backoff Build Reliable API Calls Use Extended Thinking Log API Errors Track Request IDs Paginate API Results Set Thinking Budgets Read Thinking Blocks Use Tool Search Use Web Search Tools Use Computer Tools Use Citations Use Token Counting Read Raw Responses Set Custom Headers Use Claude on AWS Bedrock Use Claude on Vertex AI Use Claude on Microsoft Foundry Configure Cloud Credentials Choose a Cloud Provider Compare API Integrations Configure Provider Clients Handle Cloud Errors Use Cloud SDK Extras Deploy Cloud Apps Protect API Keys Use a .env File Ignore Secrets in Git Validate User Input Reduce Prompt Injection Limit Tool Permissions Avoid Unsafe Tool Calls Sanitize Tool Results Set Usage Limits Review Security Logs Test SDK Functions Mock API Responses Test Error Handling Test Streaming Code Measure Token Usage Track API Costs Configure Logging Deploy with Docker Deploy a FastAPI App Monitor Production Apps Build a Claude Chatbot Build a Text Summarizer Build a Code Explainer Build a JSON Extractor Build a PDF Assistant Build a Streaming Chat App Build a Tool-Calling Agent Build a Batch Processor Build a Research Assistant Build a Claude API Dashboard
Ask about this post
AI Ask about this post

Ask questions about Use File IDs and get answers drawn from it.

Signed-in readers only.