
Python can make the same HTTP requests as a cURL command in three practical ways: translate the command into the Requests library, call libcurl through PycURL, or execute the installed curl program with subprocess.run(). The best method depends on whether you want a Python-native API, libcurl features, or exact compatibility with an existing cURL command.
Quick Answer
Use Requests for most Python HTTP and API code because its request, response, JSON, authentication, file, timeout, and error interfaces are straightforward. Choose PycURL when you specifically need libcurl behavior or detailed transfer controls. Use subprocess.run() when an existing cURL command must run unchanged—but pass arguments as a list, keep shell=False, enforce timeouts, and check the exit status.
Key Takeaways
- Requests is usually the clearest choice for a new Python HTTP client.
- PycURL is a Python interface to libcurl, not a wrapper around the curl command-line program.
- subprocess.run() launches an external cURL process, so the cURL binary must exist on the system.
- Pass subprocess arguments as a list and avoid shell=True for ordinary cURL calls.
- Always set timeouts, check HTTP status or process exit codes, and keep TLS verification enabled.
- Proxy configuration changes the request route; it does not replace authentication, parsing, retries, or permission to access a target.
Requests vs PycURL vs subprocess
The phrase “use cURL in Python” can mean two different things. You might want to reproduce what a cURL command does using Python code, or you might want Python to launch the cURL executable itself.
| Method | What actually runs | Best for | Main advantage | Main limitation |
|---|---|---|---|---|
| Requests | A Python HTTP client library | New API clients, scripts, downloads, and integrations | Readable Python interface and broad ecosystem | Does not expose every libcurl capability |
| PycURL | libcurl through a Python extension | Transfer-heavy code or projects needing libcurl options | Direct access to libcurl behavior and transfer metrics | Lower-level API and a compiled dependency |
| `subprocess.run()` | The external `curl` executable | Reusing a known command or a CLI-based workflow | Closest match to a tested shell command | Separate process, cURL dependency, and more careful secret handling |
If the goal is simply “convert this cURL request to Python,” start with Requests. If the application already standardizes on libcurl options, consider PycURL. If you must preserve a command exactly, use subprocess safely.
Install the Python Packages
Create a virtual environment so the tutorial does not change system packages:
On Windows PowerShell, activate it with:
Requests is a Python library. PycURL also depends on libcurl and may require operating-system development packages when a compatible wheel is unavailable. The subprocess example needs the curl program on PATH; verify it with:
Complete Python Requests Example
This is the most useful starting point for a normal JSON endpoint:
The timeout tuple means five seconds to establish the connection and 20 seconds between received bytes. It is not a guaranteed limit on the entire download. Requests has no default timeout, so production code should supply one.
raise_for_status() raises an HTTPError for unsuccessful HTTP responses. Network failures, TLS errors, proxy errors, timeouts, and redirect failures appear through the Requests exception hierarchy.
Common Requests Examples
GET parameters and headers
Let Requests encode query parameters instead of manually joining strings:
Headers should describe the real client. Changing a User-Agent value does not turn a Python HTTP client into a browser, execute JavaScript, or reproduce a browser fingerprint. The dedicated cURL headers guide covers header syntax in more depth.
POST JSON
Use json= for a JSON request body. Requests serializes the dictionary and sets an appropriate content type:
Use data= instead when the endpoint expects form-encoded fields rather than JSON.
Basic authentication
Load real credentials from environment variables or a secret manager. Do not paste them into an article, repository, screenshot, or exception message.
Upload a file
Opening the file inside a with block ensures that Python closes it after the request.
Download a file without loading it all into memory
Streaming matters when a response may be too large to hold comfortably in memory. If the server supports checksums or content lengths, validate them before treating the download as complete.
Complete PycURL Example
PycURL exposes libcurl options through setopt(). It does not create response storage automatically, so this example supplies a BytesIO buffer:
The PycURL quick start explains that a transfer normally follows three steps: create a Curl object, set its options, and call perform(). Response metadata must be read with getinfo() before closing the object.
PycURL option names generally mirror libcurl's CURLOPT_* names without the CURLOPT_ prefix. For example, CURLOPT_FOLLOWLOCATION becomes pycurl.FOLLOWLOCATION.
POST JSON with PycURL
PycURL offers many lower-level transfer controls. That flexibility is useful only when the project needs it; it is not automatically better than Requests for a normal JSON API.
Safe subprocess.run() Usage
Use subprocess when an existing cURL command is already the source of truth or when the Python program must invoke the command-line tool itself:
Important details:
- Each argument is a separate list element.
- shell=False is the default and is intentionally retained.
- check=True turns a non-zero cURL exit status into an exception.
- --fail-with-body makes HTTP 400+ responses fail while preserving the body for diagnosis.
- cURL's --max-time and Python's subprocess timeout protect different layers.
Do not build one command string from untrusted input and pass it to shell=True. That would ask a shell to interpret metacharacters, substitutions, redirects, and pipelines. If a legitimate workflow needs a pipeline, construct the process chain explicitly or validate each fixed component.
Convert a cURL Command to Python Requests
Consider this command:
Map each option to a Requests argument:
| cURL element | Requests equivalent |
|---|---|
| `--request POST` | `requests.post(...)` |
| URL query string | `params={...}` |
| `--header` | `headers={...}` |
| `--user` | `auth=(username, password)` |
| JSON `--data` | `json={...}` |
| `--max-time` | `timeout=...` with different semantics |
The Python version is:
Do not translate mechanically without checking semantics. A cURL config can include cookies, client certificates, multipart parts, redirect rules, retry behavior, proxy settings, and binary bodies that require their own Python equivalents.
Use a Proxy in Python
A proxy changes the network route used by the HTTP request. It does not parse a page, execute browser JavaScript, or authorize collection.
Use non-working placeholders in shared code:
Configure an authenticated HTTP proxy in Requests like this:
The https dictionary key means “use this proxy for HTTPS destinations.” The proxy URL can still begin with http:// because an HTTP proxy can carry an HTTPS request through CONNECT. Use the actual scheme supplied by the provider.
For SOCKS, install requests[socks] and follow the library's documented socks5 or socks5h behavior. socks5h requests remote DNS resolution through the proxy, while socks5 can resolve locally.
With a Proxidize access point, replace the placeholders with the generated server, port, username, and password. Test the exit IP and expected location from the same Python client before trusting a collection result. The full cURL proxy guide covers HTTP, HTTPS, SOCKS5, authentication, and CLI diagnostics.
Timeouts, Redirects, HTTP Errors, and TLS
| Concern | Requests | PycURL | cURL through subprocess |
|---|---|---|---|
| Connection timeout | `timeout=(connect, read)` | `CONNECTTIMEOUT` | `--connect-timeout` |
| Overall transfer limit | Requires additional application logic; read timeout is not a total limit | `TIMEOUT` | `--max-time`, plus subprocess timeout |
| Redirects | Followed for GET by default; control with `allow_redirects` | Off by default; enable `FOLLOWLOCATION` | Enable with `--location` |
| HTTP 400+ handling | Call `raise_for_status()` | Set `FAILONERROR` or inspect `RESPONSE_CODE` | Use `--fail-with-body` and `check=True` |
| TLS verification | Enabled by default | Supply a trusted CA bundle when needed | Enabled by default; use a trusted CA path if required |
Do not “fix” a certificate error by permanently disabling TLS verification. Identify whether the failure comes from an outdated CA store, an intercepting corporate proxy, a hostname mismatch, or an invalid server certificate. The cURL TLS guide explains the limited cases where insecure local testing may be acceptable and why it should not become production configuration.
Retries also need deliberate policy. Retry transient connection failures and selected 429/5xx responses only when the operation is safe to repeat. A POST may have completed even if the client did not receive the response. For bounded retry logic in Requests, use the Python Requests retry guide.
Tested Versions and Results
We tested the primary patterns on October 2, 2026 with:
- Python 3.11.2.
- Requests 2.34.2.
- PycURL 7.48.0 linked to libcurl 8.22.0.
- cURL 7.88.1 linked to the system libcurl 7.88.1.
Requests and PycURL retrieved and parsed https://httpbin.org/json, and the subprocess example returned the same JSON through the installed cURL executable.
We also ran a controlled local test covering Requests GET, JSON POST, Basic authentication, multipart upload, streamed download, and an authenticated HTTP proxy. A PycURL GET and a list-based subprocess.run() cURL call were checked against the same fixture. All assertions passed.
These checks establish that the displayed patterns worked on the recorded versions. They do not prove compatibility with every API, proxy, TLS environment, or operating system. Validate the exact endpoint and dependency versions used in production.
Troubleshooting
| Symptom | Likely cause | What to check |
|---|---|---|
| `ModuleNotFoundError: requests` | Wrong environment or package not installed | Activate the virtual environment and run `python -m pip install requests` |
| `ImportError` while importing PycURL | Missing or incompatible libcurl/SSL build | Install a compatible wheel or required OS development packages, then rebuild in a clean environment |
| `FileNotFoundError: curl` | cURL is absent from `PATH` | Install cURL or call the verified executable path |
| Subprocess returns empty output | cURL wrote an error to stderr or request returned no body | Inspect `returncode` and `stderr`; use `check=True` and `--show-error` |
| Request hangs | No timeout or a stalled response | Add connection/read or process timeouts and log the phase that failed |
| `HTTPError` after `raise_for_status()` | Server returned an unsuccessful status | Record status, safe response excerpt, request ID, and retry only when appropriate |
| `ProxyError` or HTTP 407 | Wrong proxy endpoint or authentication | Recheck host, port, scheme, username, password, account state, and allowed source IP |
| Public IP does not change | Request did not use the intended proxy or the provider kept the same session | Inspect effective configuration and compare the exit from the same client |
| TLS certificate error | CA, hostname, interception, or server-certificate problem | Fix trust configuration; do not disable verification as a permanent workaround |
| Redirect loop | Endpoint or cookie/auth flow keeps redirecting | Disable automatic redirects temporarily and inspect the `Location` chain |
Conclusion
Requests is the best default for most Python HTTP code. PycURL is appropriate when libcurl controls matter, and subprocess.run() is useful when an established cURL command must remain the executable interface. Whichever method you choose, make timeouts, status checks, TLS verification, secret handling, and output validation explicit.
For API calls and ordinary downloads, custom code is usually enough. When a project instead needs maintained visual extraction workflows, a platform such as Octoparse may be worth evaluating alongside code-first tools. For proxy routing, start with the complete cURL proxy setup guide or compare cURL and Wget before choosing the command-line layer.
Frequently asked questions
Yes. subprocess.run() can launch the installed curl executable. Pass arguments as a list, retain the default shell=False, set both cURL and subprocess timeouts, capture stderr, and use check=True when a non-zero exit should fail the program.
No. Requests is a Python HTTP library. cURL is a command-line program built on libcurl, and PycURL is a Python binding to libcurl. They can send equivalent requests, but their defaults, timeout semantics, redirect behavior, protocol support, and error interfaces differ.
Use Requests for most new Python API clients and scripts. Choose PycURL when the project specifically benefits from libcurl options, transfer information, protocol behavior, or an existing libcurl-based design.
Map the method, URL, query parameters, headers, authentication, body, files, cookies, proxy, and timeout settings individually. Requests is usually the clearest target. Review redirects, TLS, retries, binary bodies, and multipart data rather than translating only the visible URL and method.
No. A normal cURL invocation should use a list of arguments with shell=False, which is the default. Shell mode is only needed for shell syntax such as pipelines or redirects and introduces additional injection risk when any component is variable.
Generate an access point in the Proxidize dashboard, load its host, port, username, and password from environment variables, build the proxy URL, and pass it in Requests' proxies mapping. Then verify the observed exit IP and location from the same client.