How to Upload Files with Python: requests Examples
Uploading a file in Python takes about five lines of code with the requests library. This guide covers the complete pattern: opening files correctly, setting authentication headers, handling the response, uploading from a URL without writing to disk, and avoiding the most common mistakes that cause 400 errors.
Basic File Upload with requests
The files parameter in requests.post handles multipart encoding for you:
import requests
with open("photo.png", "rb") as f:
response = requests.post(
"https://httpbin.org/post",
files={"file": f}
)
print(response.json())
Opening the file in binary mode ("rb") is required. Text mode ("r") will corrupt binary files like images, PDFs, and archives. The files parameter tells requests to encode the body as multipart/form-data and set the correct Content-Type header automatically, including the boundary string that separates parts.
Adding Authentication Headers
Most file upload APIs require an API key or Bearer token. Pass headers separately from the files parameter:
import requests
API_KEY = "your_api_key_here"
with open("report.pdf", "rb") as f:
response = requests.post(
"https://api.example.com/upload",
headers={"X-API-Key": API_KEY},
files={"file": f}
)
response.raise_for_status()
print(response.json())
Important: do not put Content-Type in your headers dictionary. When you use files=, requests generates the multipart boundary and includes it in Content-Type. If you override that header, the boundary disappears and the server rejects the request with a 400 error.
For Bearer tokens, the pattern is the same:
headers = {"Authorization": "Bearer eyJhbGci..."}
with open("invoice.pdf", "rb") as f:
response = requests.post(url, headers=headers, files={"file": f})
Uploading to the FilePost API
FilePost accepts a single multipart upload and returns a permanent public CDN URL. No buckets, no SDK, no configuration:
import requests
API_KEY = "your_api_key_here"
with open("screenshot.png", "rb") as f:
response = requests.post(
"https://filepost.dev/v1/upload",
headers={"X-API-Key": API_KEY},
files={"file": f}
)
data = response.json()
print(data["url"])
# https://cdn.filepost.dev/file/filepost/uploads/a1/a1b2c3.png
The url field is a permanent, publicly accessible link served through Cloudflare's CDN. You can store it in a database, embed it in HTML, or return it from your own API.
To control the filename and MIME type explicitly:
with open("report.pdf", "rb") as f:
response = requests.post(
"https://filepost.dev/v1/upload",
headers={"X-API-Key": API_KEY},
files={"file": ("quarterly-report.pdf", f, "application/pdf")}
)
The tuple format is (filename, file_object, content_type). This is useful when the filename on disk differs from what you want the server to see.
Uploading Multiple Files in a Loop
import requests
from pathlib import Path
API_KEY = "your_api_key_here"
UPLOAD_URL = "https://filepost.dev/v1/upload"
results = []
for path in Path("./images").glob("*.png"):
with open(path, "rb") as f:
resp = requests.post(
UPLOAD_URL,
headers={"X-API-Key": API_KEY},
files={"file": f}
)
resp.raise_for_status()
results.append(resp.json()["url"])
print(f"Uploaded {len(results)} files")
for url in results:
print(url)
For parallel uploads that finish faster, use concurrent.futures:
import requests
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
API_KEY = "your_api_key_here"
UPLOAD_URL = "https://filepost.dev/v1/upload"
def upload_file(path):
with open(path, "rb") as f:
resp = requests.post(
UPLOAD_URL,
headers={"X-API-Key": API_KEY},
files={"file": f}
)
resp.raise_for_status()
return str(path), resp.json()["url"]
paths = list(Path("./images").glob("*.png"))
with ThreadPoolExecutor(max_workers=4) as executor:
futures = {executor.submit(upload_file, p): p for p in paths}
for future in as_completed(futures):
filename, url = future.result()
print(f"{filename} → {url}")
Uploading a File from a URL (No Disk Write)
If your source is a URL rather than a local file, stream the download directly into the upload request:
import requests
API_KEY = "your_api_key_here"
SOURCE_URL = "https://example.com/image.jpg"
# Stream the download without writing to disk
with requests.get(SOURCE_URL, stream=True) as download:
download.raise_for_status()
response = requests.post(
"https://filepost.dev/v1/upload",
headers={"X-API-Key": API_KEY},
files={"file": ("image.jpg", download.raw)}
)
print(response.json()["url"])
The stream=True flag keeps the connection open and exposes response.raw as a file-like object. You pass it directly to files=, no BytesIO buffer needed, no temporary files, minimal memory usage.
Uploading In-Memory Data
When the file content is already in memory as bytes (from a PIL image, a PDF generator, or any other in-memory operation), use io.BytesIO:
import io
import requests
API_KEY = "your_api_key_here"
# Example: generate content in memory
content = b"col1,col2\nval1,val2\n"
buffer = io.BytesIO(content)
response = requests.post(
"https://filepost.dev/v1/upload",
headers={"X-API-Key": API_KEY},
files={"file": ("export.csv", buffer, "text/csv")}
)
print(response.json()["url"])
Error Handling
Always call raise_for_status() so HTTP errors surface as exceptions rather than silent failures. For production scripts, wrap in a try-except:
import requests
from requests.exceptions import HTTPError, Timeout, RequestException
API_KEY = "your_api_key_here"
try:
with open("document.pdf", "rb") as f:
response = requests.post(
"https://filepost.dev/v1/upload",
headers={"X-API-Key": API_KEY},
files={"file": f},
timeout=60
)
response.raise_for_status()
url = response.json()["url"]
print(f"Uploaded: {url}")
except HTTPError as e:
print(f"HTTP error {e.response.status_code}: {e.response.text}")
except Timeout:
print("Upload timed out, file may be too large or connection is slow")
except RequestException as e:
print(f"Request failed: {e}")
Always set a timeout. Without it, a dropped connection will hang your script indefinitely. For large files, 60–120 seconds is a reasonable value.
Frequently Asked Questions
How do I upload a file with Python requests?
Open the file in binary mode and pass it to the files parameter of requests.post:
with open("photo.png", "rb") as f:
response = requests.post(url, headers={"X-API-Key": key}, files={"file": f})
The requests library handles multipart encoding and the correct Content-Type header automatically.
What is the files parameter in requests.post?
It tells requests to encode the body as multipart/form-data. The key is the form field name the server expects (usually "file"), and the value is a file object opened in binary mode.
How do I upload a file from a URL without saving it first?
Use requests.get with stream=True and pass response.raw to the files parameter. This streams data without writing a temporary file to disk.
Why does my upload return a 400 error?
The most common cause is setting Content-Type manually in headers. When you use files=, requests generates the multipart boundary automatically. Override it and the server cannot parse the body. Remove any Content-Type key from your headers dictionary.
Try It With FilePost
One API call, one URL back. 30 free uploads per month. No buckets, no SDK, no setup.
Get Your Free API Key