cURL to Python Requests Converter
Turn a cURL command into idiomatic Python `requests` code — headers, JSON bodies, form data, basic auth and query strings included.
cURL command
bash syntaxPython (requests)
request.pyPaste a command to see the generated code.
Parsing and code generation happen in your browser. Tokens and credentials inside the command are never transmitted.
About the cURL to Python Requests Converter
Paste a cURL command and this curl to Python converter returns the equivalent code using the requests library — headers, JSON bodies, form data, file uploads and basic auth all mapped to the right keyword argument. It is the quickest route from "this works in my terminal" to a script you can run.
The generated code is idiomatic rather than literal. A JSON body becomes a Python dict passed as json=payload, so requests sets the Content-Type and serialises for you. A urlencoded body becomes a dict passed as data=. Multipart -F fields become the files= mapping, using the (None, value) form that requests requires for plain text parts.
Two things are added that curl does not have but every production script needs: an explicit timeout, because requests will otherwise wait forever, and raise_for_status() so an HTTP error is not mistaken for success.
Known HTTP methods map onto requests' own convenience functions — requests.get(...), .post(...), .put(...), .patch(...), .delete(...) — and anything outside that set falls back to the explicit requests.request(method, url, ...) form, so a custom verb from an internal API still comes out correct. requests itself is not part of the standard library, which is the most common first failure: install it before anything else here will run.
- JSON bodies converted to real Python dicts, with true/false/null mapped to True/False/None
- Basic auth mapped to the auth= tuple, -F uploads to files=, form bodies to data=
- allow_redirects and verify set to match what curl actually did
- timeout=30 and raise_for_status() included by default
- Unsupported flags reported rather than silently dropped
- Known methods call requests.get/post/put/patch/delete/head/options directly; anything else falls back to requests.request()
- Multi-line bash commands with backslash continuations parsed the way a shell would
- Windows cmd ^ continuations detected and flagged rather than silently mis-parsed
How to use it
- Copy a cURL command — from your terminal history, from an API's documentation, or via Copy as cURL (bash) in browser DevTools.
- Paste it into the left pane.
- Read the generated Python on the right; it updates as you type.
- Install the dependency with pip install requests if you have not already.
- Adjust the timeout, and move any credentials into environment variables before committing the script.
Real-world use cases
Backend & API developers
Turn a cURL example from an API's documentation into a working Python script in one paste, to sanity-check what an endpoint actually returns before writing the real integration.
QA & test automation engineers
Reproduce a request captured from a bug report, a HAR export or a colleague's terminal history as a small pytest fixture or repro script, without hand-translating headers and a JSON body.
Data engineers
Convert a one-off API call — pulling a report, hitting a webhook, polling an endpoint — into a script that belongs in a scheduled job, complete with the timeout and error handling a cron job actually needs.
DevOps & SRE engineers
Turn a cURL health-check or diagnostic command from a runbook into a Python script for a monitoring tool or an automation pipeline, instead of shelling out to curl from inside Python.
Students & developers learning requests
See a real request rendered in requests' own idioms — json= versus data=, the auth tuple, raise_for_status() — as a working reference instead of a stripped-down tutorial snippet.
Examples
A JSON POST
curl -X POST https://api.example.com/users \
-H 'Content-Type: application/json' \
-d '{"name":"Ada","active":true}'import requests
url = "https://api.example.com/users"
headers = {
"Content-Type": "application/json",
}
payload = {
"name": "Ada",
"active": True,
}
response = requests.post(url, headers=headers, json=payload, allow_redirects=False, timeout=30)
response.raise_for_status()
print(response.json())JSON true becomes Python True, and null becomes None — a detail that trips up hand conversion constantly.
A urlencoded form body
curl -X POST https://api.example.com/login \ -d 'username=ada&password=s3cret'
import requests
url = "https://api.example.com/login"
headers = {
"Content-Type": "application/x-www-form-urlencoded",
}
payload = {
"username": "ada",
"password": "s3cret",
}
response = requests.post(url, headers=headers, data=payload, allow_redirects=False, timeout=30)
response.raise_for_status()
print(response.text)curl defaults -d to application/x-www-form-urlencoded when no Content-Type is given, so the body becomes data=payload rather than json=payload — passing this particular dict to json= would send the wrong Content-Type entirely.
Basic auth
curl -u demo:s3cret https://api.example.com/me
auth = ("demo", "s3cret")
response = requests.get(url, auth=auth, allow_redirects=False, timeout=30)requests builds the Authorization header for you. Read the credentials from os.environ rather than hard-coding them.
A file upload
curl -F "field=value" -F "avatar=@photo.png" https://api.example.com/upload
files = {
"field": (None, "value"),
"avatar": open("photo.png", "rb"),
}
response = requests.post(url, files=files, allow_redirects=False, timeout=30)The (None, value) tuple tells requests the part is a plain field rather than a file. Setting Content-Type manually would break the multipart boundary, so it is omitted.
DELETE with a bearer token
curl -X DELETE https://api.example.com/users/42 \ -H 'Authorization: Bearer eyJhbGciOi...'
import requests
url = "https://api.example.com/users/42"
headers = {
"Authorization": "Bearer eyJhbGciOi...",
}
response = requests.delete(url, headers=headers, allow_redirects=False, timeout=30)
response.raise_for_status()
print(response.text)DELETE is one of the methods requests exposes directly, so it calls requests.delete() rather than the more verbose requests.request("DELETE", ...) form. No body means no json= or data= argument at all.
Common errors
| Message | Cause | Fix |
|---|---|---|
| ModuleNotFoundError: No module named 'requests' | requests is a third-party library, not part of the standard library. | Run pip install requests, ideally inside a virtual environment. |
| SSLError: CERTIFICATE_VERIFY_FAILED | A self-signed or internal certificate — the same situation where you reached for curl -k. | Pass verify="/path/to/ca-bundle.pem". Use verify=False only for throwaway local debugging; it disables TLS verification entirely. |
| The script hangs indefinitely | requests has no default timeout, so a stalled connection blocks forever. | Always pass timeout=. The generated code includes timeout=30. |
| The API rejects the body as invalid JSON | Using data=payload with a dict form-encodes it instead of serialising to JSON. | Use json=payload, which serialises and sets Content-Type: application/json. |
| TypeError: Object of type datetime is not JSON serializable | A value in the dict that json= cannot encode. | Convert it first — for example date.isoformat() — or pass a pre-serialised string via data=. |
| requests.exceptions.HTTPError: 401 Client Error: Unauthorized for url | raise_for_status() turns any 4xx or 5xx response into an exception. This is intentional — a script that quietly continues past a failed request usually corrupts whatever runs next. | Wrap the call in try/except requests.exceptions.HTTPError if the script needs to handle the failure itself, or inspect response.status_code before calling raise_for_status(). |
| Printed response text looks garbled | response.text decodes the body using a charset requests guesses from the response headers, which is occasionally wrong. | Use response.content for the raw bytes, or set response.encoding explicitly before reading .text. |
Frequently asked questions
›Is the command uploaded anywhere?
No. Everything is parsed and generated in your browser. This matters because a cURL command copied from DevTools almost always contains a session cookie or bearer token.
›Why does the generated code use json= instead of data=?
When the request has a JSON Content-Type, json= is the correct argument: requests serialises the dict and sets the header for you. Passing a dict to data= would form-encode it instead, which most JSON APIs reject.
›Why is allow_redirects=False in the output?
curl does not follow redirects unless you pass -L, but requests follows them by default for most methods. Stating it explicitly keeps the Python behaviour identical to the command you pasted. If your original command had -L, the argument is omitted and requests' default applies.
›Should I really use verify=False?
Only for local debugging. It disables certificate verification completely, which removes the protection TLS exists to provide. For an internal CA, point verify= at the CA bundle instead.
›Can I get httpx or aiohttp output instead?
Not yet. requests is the target for now; the parser is shared, so additional generators are straightforward to add.
›Does it handle multi-line commands?
Yes. Backslash line continuations and both quoting styles are parsed the way bash would, so a copied multi-line command works without editing. Windows cmd commands using ^ continuations are detected and flagged.
›Does it handle DELETE, PUT and PATCH, not just GET and POST?
Yes. The HTTP method comes directly from curl's -X flag (or is inferred as POST when there's a body and GET otherwise). GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS call the matching requests.get/post/put/patch/delete/head/options function; anything else falls back to the explicit requests.request(method, url, ...) form so an unusual verb still works.
›Why does the generated code include timeout=30 when my curl command didn't set one?
requests has no default timeout at all — a stalled connection or an unresponsive server leaves the call blocked forever, which is a worse failure mode in a script than it is at an interactive terminal. 30 seconds is a conservative default; lower it for an endpoint you expect to answer quickly, or raise it for a slow report-generation call.
›The command has several -H flags for headers with different names — are they all included?
Yes. Each -H flag becomes one entry in the headers dict, keyed by header name, so distinct headers all survive the conversion. A header repeated with the same name twice is the one case worth checking by eye afterward, since a Python dict can only hold one value per key.
›Should I reuse this code for several requests to the same API?
The generated snippet is a single, self-contained request. If a script is about to make several calls to the same host, wrapping them in a requests.Session() instead of separate requests.get()/post() calls reuses the underlying TCP connection and lets you set headers or auth once for every request made through it — worth doing by hand once you're past the first working call.
Last updated