How to Convert curl to Python Requests (and fetch)

By Ramanathan Oct 13, 2026 5 min read cURL Formatter

API docs and browser dev tools hand you curl commands, but your code is in Python. Converting one to the requests library is mostly mechanical: each curl option has a matching argument. This guide shows that mapping, a worked example, the differences that trip people up, and the same request in JavaScript fetch.

Get the curl command

If the request comes from a website, open your browser's dev tools, go to the Network tab, right-click the request and choose Copy → Copy as cURL. Chrome offers two versions: bash (single quotes, $'…' for special characters) and, on Windows, cmd (with ^ escapes). Either one is fine as a starting point. It will include every header the browser sent, most of which (sec-ch-ua, accept-language and so on) you can delete.

A worked example

This curl command creates an order with a JSON body and a 10-second timeout:

curl -X POST 'https://api.example.com/v1/orders?dry_run=true' \
  -H 'Authorization: Bearer TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"sku":"A-100","qty":2}' \
  -m 10

In Python with requests (install it with pip install requests):

import requests

headers = {
    "Authorization": "Bearer TOKEN",
}

json_data = {
    "sku": "A-100",
    "qty": 2,
}

response = requests.post(
    "https://api.example.com/v1/orders?dry_run=true",
    headers=headers,
    json=json_data,
    timeout=10,
)
print(response.status_code)
print(response.text)

Three things changed shape. -X POST became the function name (requests.post). The JSON string became a Python dict passed as json=, which serialises it and sets Content-Type: application/json for you, so that header is no longer needed. And -m 10 became timeout=10.

How each curl option maps to requests

curlrequestsNotes
-X POST, -X PUT, …requests.post(), requests.put(), …Other methods: requests.request("PURGE", url)
-H 'Name: value'headers={"Name": "value"}One dict for all headers
-d 'a=1&b=2'data={"a": "1", "b": "2"}A dict is form-encoded, like curl's -d
-d '{"a":1}' with a JSON Content-Type, or --jsonjson={"a": 1}Sets the JSON Content-Type itself
--data-binary @filedata=open("file", "rb")Sends the file's bytes as the body
-F 'name=value'files={"name": (None, "value")}Multipart form field
-F '[email protected]'files={"file": open("photo.jpg", "rb")}Multipart file upload
-u user:passauth=("user", "pass")HTTP Basic auth
-b 'a=1; b=2'cookies={"a": "1", "b": "2"}Or keep it as a Cookie header
-G -d 'q=x'params={"q": "x"}Data goes in the query string
-m 10timeout=10Seconds
-kverify=FalseSkips TLS checks; for testing only
-Irequests.head()Headers only
-L(default)requests follows redirects already
--compressed(default)gzip and deflate are decoded automatically
-s, -v, -i—Output options with no request effect

Forms, query strings and uploads

A login form posted with -d is form-encoded. Pass a dict and requests encodes it the same way:

curl https://api.example.com/login -d username=ada -d 'password=s3cret!'
data = {
    "username": "ada",
    "password": "s3cret!",
}

response = requests.post("https://api.example.com/login", data=data)

With -G, curl moves the -d data into the URL and sends a GET. In requests, either write the query string into the URL or pass params={"q": "blue shoes", "page": "2"} and let it encode the values.

A multipart upload with -F becomes files=. Fields with plain values use a (None, value) tuple so they are sent as form fields rather than files:

files = {
    "file": open("report.pdf", "rb"),
    "title": (None, "Q3 report"),
}

response = requests.post(
    "https://api.example.com/upload",
    files=files,
    auth=("ada", "s3cret"),
)

Gotchas when converting

  • A string body has no Content-Type. curl's -d always sends Content-Type: application/x-www-form-urlencoded. In requests, data= with a dict does the same, but data= with a plain string sends no Content-Type at all. If the API expects one, add it to headers, or use json= for JSON.
  • Don't copy a multipart Content-Type header. Content-Type: multipart/form-data needs a boundary that only the library knows. Leave the header out when you use files=, or the server can't parse the body.
  • Redirects differ. curl doesn't follow redirects unless you pass -L; requests follows them by default (only requests.head() doesn't). Pass allow_redirects=False to match curl without -L.
  • There is no default timeout. Without timeout=, requests can wait forever on a server that never answers. Set one even if the curl command didn't.
  • Errors don't raise. A 404 or 500 is a normal response in both tools. Call response.raise_for_status() to turn 4xx and 5xx into an exception, much like curl's -f.
  • verify=False is for testing. It turns off certificate checks, just like -k. Fix the certificate, or pass verify="ca.pem", before shipping.

The same request with fetch

In JavaScript (browsers, Node 18+, Deno, Bun), the order example looks like this:

const response = await fetch("https://api.example.com/v1/orders?dry_run=true", {
  method: "POST",
  headers: {
    "Authorization": "Bearer TOKEN",
    "Content-Type": "application/json"
  },
  body: JSON.stringify({
    "sku": "A-100",
    "qty": 2
  }),
  signal: AbortSignal.timeout(10000)
});

console.log(response.status);
console.log(await response.text());

The differences from requests: the method goes in the options object, the JSON body must be stringified and its Content-Type set by hand, and a timeout is an AbortSignal in milliseconds. A plain string body is sent as text/plain, so form data should be a URLSearchParams and uploads a FormData. fetch has no switch to skip certificate checks, and like requests it doesn't throw on 4xx or 5xx: check response.ok.

Related

Part of The Complete Guide to HTTP. To make a long command readable first, see How to Format a Long cURL Command. For the methods and headers involved, read HTTP Request Methods and How to Read HTTP Headers.

Try it

Paste a curl command into the cURL Formatter and switch to Python requests or JavaScript fetch mode to get the code. It handles headers, JSON and form bodies, uploads, auth, cookies, -G and timeouts, lists any option it can't translate as a comment, and runs entirely in your browser, so tokens in the command are never uploaded.

About the author

Ramanathan · Software Engineer & Solutions Architect

I'm a Software Engineer and Solutions Architect with 20+ years of experience building enterprise applications across BFSI, Healthcare, Retail, Manufacturing, and Industrial Automation. I've spent those two decades living in JSON, tokens, regexes, and config files — so I built the fast, private, no-login developer tools I always wanted to reach for myself.

Last updated: Oct 13, 2026