Use File IDs
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:
- knowing whether a file has already been uploaded,
- confirming that the stored ID still works, and
- 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
- 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.
- 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. - A missing or new file is uploaded, and its digest and ID are saved, so the next run finds it.
- Both questions use the same ID, so the file is referenced rather than resent.
- 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
- Run the example twice, then change one byte in the PDF and run it again. What happens on each run?
- Add a
--forgetoption that removes the cache entry for a given file, and forces a new upload. - Explain why
file_exists()catchesNotFoundErrorinstead of letting it propagate. - Modify
file_id_for()to store the owner's user ID alongside each entry, and refuse to return an entry for a different user.
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.