To convert a curl command to Python, map each option to an argument of requests or httpx. -H becomes headers, -d becomes data, --json becomes json, -u becomes auth, and -x with -U becomes a proxy URL with the login inside. Then check the result, because neither library copies curl's defaults. In our side-by-side test on 11 October 2026, requests followed redirects that curl stopped at. A data= string went out with no Content-Type, and httpx gave up on a slow answer after 5 seconds.
We sent 19 curl commands from our own server with curl 8.5.0, requests 2.34.2 and httpx 0.28.1. We also ran the code that the converter curlconverter generated from each command. Every request went to an echo that shows what arrived. This page gives the mapping, the differences we measured and the fix for each.
Which Python argument does each curl option become?
| curl | requests | httpx |
|---|---|---|
-X POST, or -d alone | requests.post(url, ...) | httpx.post(url, ...) |
-H "Name: value" | headers={"Name": "value"} | headers={"Name": "value"} |
-d "a=1&b=2" | data={"a": "1", "b": "2"} | data={"a": "1", "b": "2"} |
--json '{"a": 1}' | json={"a": 1} | json={"a": 1} |
-F "file=@a.txt" | files={"file": f}, f opened with "rb" | files={"file": f} |
-G --data-urlencode "q=x y" | params={"q": "x y"} | params={"q": "x y"} |
-u USER:PASS | auth=("USER", "PASS") | auth=("USER", "PASS") |
-b "session=abc" | cookies={"session": "abc"} | cookies={"session": "abc"} |
-I | requests.head(url) | httpx.head(url) |
no -L | allow_redirects=False | the default |
-L | the default | follow_redirects=True |
-k | verify=False | verify=False |
--compressed | the default | the default |
-m 20 | timeout=20, per wait | timeout=20, per wait |
-x http://host:port -U USER:PASS | proxies={"http": P, "https": P} | proxy=P |
In the last row, P is http://USER:PASS@host:port. Two rows need care. A string in data= is not the same as -d, as the next sections show. And timeout= is not --max-time. curl's option limits "each transfer", while requests says its timeout "is not a time limit on the entire response download". It limits each wait for data, and httpx counts "network inactivity" the same way.
How do you convert a command step by step?
You need Python 3 with requests or httpx installed (pip install requests httpx), and the curl command you want to convert. Take this command. It logs in, asks for JSON, sends two form fields and stops after 20 seconds:
curl https://httpbin.org/anything \
-u USER:PASS \
-H "Accept: application/json" \
-d "name=alice&age=30" \
--max-time 20
- Pick the method. With
-d, curl sends a POST "in the same way that a browser does" with a form. So the call isrequests.post. - Copy the headers. Each
-Hbecomes one entry inheaders. - Move the body. Split the
-dstring into a dict. The library then sends a form with the right Content-Type. - Carry the login over.
-u USER:PASSbecomesauth=("USER", "PASS"). - Match curl's defaults. curl stops at a redirect without
-L, so requests needsallow_redirects=False. Pick a timeout as well.
With requests:
import requests
response = requests.post(
"https://httpbin.org/anything",
auth=("USER", "PASS"),
headers={"Accept": "application/json"},
data={"name": "alice", "age": "30"},
timeout=20,
allow_redirects=False, # curl does not follow redirects without -L
)
print(response.status_code, response.json()["form"])
With httpx, where redirects stay off by default:
import httpx
response = httpx.post(
"https://httpbin.org/anything",
auth=("USER", "PASS"),
headers={"Accept": "application/json"},
data={"name": "alice", "age": "30"},
timeout=20,
)
print(response.status_code, response.json()["form"])
Both printed 200 {'age': '30', 'name': 'alice'} in our run. That is the same form the echo received from curl, with the same login.
What changes when you keep the defaults?
These are the differences we measured with the same requests from all three clients:
| Case | curl 8.5.0 | requests 2.34.2 | httpx 0.28.1 |
|---|---|---|---|
| User-Agent | curl/8.5.0 | python-requests/2.34.2 | python-httpx/0.28.1 |
| Body given as a string | a form, with -d | no Content-Type (data=) | no Content-Type (content=) |
| Accept header for JSON | application/json (--json) | */* (json=) | */* (json=) |
| Page that redirects twice | stops at 302 | 200, after 2 redirects | stops at 302 |
| Self-signed certificate | exit code 60 | SSLError | ConnectError |
| gzip, nothing set | raw gzip bytes | decoded | decoded |
| Answer after 7 seconds | 200 after 8.1 s | 200 after 7.2 s | ReadTimeout after 5.4 s |

Redirects. curl follows a redirect only with -L. The requests documentation says it "will perform location redirection for all verbs except HEAD". HTTPX does the opposite: "Unlike requests, HTTPX does not follow redirects by default". Our guide to curl and redirects covers the curl side.
A body given as a string. curl's -d labels the body application/x-www-form-urlencoded. In requests, a dict is form-encoded, but "if you pass in a string instead of a dict, that data will be posted directly". Our echo received that string with no Content-Type and an empty form. A strict API may answer 415 Unsupported Media Type to it. In httpx, raw text belongs in content=, and data= with text is deprecated.
JSON. --json sets both Content-Type and Accept to application/json. In both libraries, json= sets only the Content-Type, and our echo saw Accept: */*. If an API checks Accept, add the header yourself. The curl POST guide shows the curl options for JSON.
Timeouts. In requests, calls "do not time out unless a timeout value is set explicitly". httpx raises a timeout "after 5 seconds of network inactivity". Against a page that answered after 7 seconds, curl and requests waited, and httpx gave up after 5.4 seconds. See Python requests timeout for the settings.
Compression and certificates. Without --compressed, curl printed the raw gzip bytes, while both libraries decoded them. All three refused a self-signed certificate until told otherwise: -k in curl, verify=False in the libraries, where requests adds an InsecureRequestWarning. Keep verification on outside tests; cURL ignore SSL shows the safer options.
User-Agent. The server saw three different programs. A site that treats clients by this header can answer each of them differently.
Can a converter do it for you?
curlconverter is an open-source converter that can "transpile curl commands into" Python and many other languages. We ran its requests code for all 19 commands, and it behaved like curl in 15.
Two of the differences are documented. The converted code follows redirects and decodes gzip, because "the generated code will do whatever the default is for that runtime". The project's advice is to set the redirect policy in the command. With --no-location, the code got allow_redirects=False.
The other two were the proxy cases, and the project's README says nothing about them. curlconverter 4.12.0 turned -U USER:PASS into a proxy setting without the login, and it gave no warning. Our logging proxy received the user name from curl, requests and httpx, and nothing from the converted code. Its parser keeps the value, but its Python generator never reads it.

A proxy that requires a login answers such code with 407. That status says the client "needs to authenticate itself in order to use a proxy". The fix is simple. Write the login into the proxy URL before you convert, as -x http://USER:PASS@host:port. curlconverter kept credentials written that way.
How do you send the request through a proxy?
curl takes the proxy with -x and the login with -U, "the username and password to use for proxy authentication". Python puts both into one URL. The requests documentation says to "use the http://user:password@host/ syntax". httpx takes the login "as the userinfo section of the proxy URL". With our gateway's address and placeholder credentials:
curl -x http://premium.hproxy.com:10000 -U USER:PASS https://httpbin.org/anything
import requests
proxy = "http://USER:PASS@premium.hproxy.com:10000"
response = requests.get(
"https://httpbin.org/anything",
proxies={"http": proxy, "https": proxy},
timeout=20,
)
import httpx
response = httpx.get(
"https://httpbin.org/anything",
proxy="http://USER:PASS@premium.hproxy.com:10000",
timeout=20,
)
We ran both blocks through our logging proxy instead of the gateway. Each answered 200, and the proxy received the user name from both.
For a SOCKS proxy, install requests[socks] or httpx[socks] first. The scheme decides where the host name is looked up. In requests, "the scheme socks5 causes the DNS resolution to happen on the client". To resolve names on the proxy, you "use socks5h as the scheme", in line with curl. Our guides to proxies in Python requests and proxies in httpx go further. Lines from our residential proxies use the same URL form as above.
When should you run curl itself from Python?
Run curl itself when the command must stay exactly as written, for example with an option neither library has. Use subprocess and pass the arguments as a list. The Python documentation explains that a list "allows the module to take care of any required escaping and quoting of arguments". With shell=True, the quoting is your job, and mistakes open "shell injection vulnerabilities".
import subprocess
result = subprocess.run(
["curl", "-sS", "--max-time", "20", "https://httpbin.org/anything"],
capture_output=True,
text=True,
timeout=30,
)
print(result.returncode, result.stdout[:80])
In our run it returned 0 and the echo's answer. Read result.returncode before you trust the output, since curl reports failures there, such as 60 for a certificate it does not trust. To use curl's engine inside Python instead, PycURL is "a Python interface to libcurl", for libcurl 7.19.0 or later.
How do you check that the conversion worked?
Send the curl command and the Python code to the same echo and compare what arrived. https://httpbin.org/anything returns the method, the URL, the headers and the body as it parsed it. Compare five things: the method, the Content-Type, the form or JSON fields, the Authorization header and where redirects ended.
This is how the string trap shows itself. The same body sent as data="name=alice&age=30" came back as an empty form with no Content-Type. curl's -d came back with both form fields. Every difference on this page turned up this way.
What this page could not check
- We tested on Linux with curl 8.5.0, the build Ubuntu 24.04 ships, not the newest release, 8.22.0, and not curl.exe on Windows.
- Every request went to httpbin.org, an echo service, or to our own stub proxy. We did not test a real API, an HTTPS proxy or a SOCKS proxy.
- Our stub proxy accepted every request, so the lost login showed in its log, not as a 407.
- We tested one converter, curlconverter 4.12.0, not uncurl or other online converters. A later release may handle
-U. - These were single runs, so the timings include ordinary network jitter. We did not measure how many sites treat the Python User-Agents differently from curl's.
- requests, httpx and curlconverter change between releases. We will run the test again by 11 January 2027.
Sources
- curl manual, curl project, current version, read 11 October 2026: curl.se.
- Requests documentation, Quickstart and Advanced Usage, Requests 2.34.2 (14 May 2026), read 11 October 2026: requests.readthedocs.io.
- HTTPX documentation, Requests Compatibility, Timeouts and Proxies, HTTPX 0.28.1 (6 December 2024), read 11 October 2026: python-httpx.org.
- Python documentation, subprocess, read 11 October 2026: docs.python.org.
- curlconverter, README and the package source of version 4.12.0 (7 February 2025), read 11 October 2026: github.com/curlconverter.
- PycURL, README, read 11 October 2026: github.com/pycurl.
- RFC 9110, HTTP Semantics, section 15.5.8, IETF, June 2022: rfc-editor.org.
- Our own test, 11 October 2026, 01:32 to 01:36 UTC, from our server: 19 curl commands against requests, httpx and curlconverter's code, and the page's code blocks.


