The Story That Explains APIs 🍽️APIs समझाने वाली कहानी 🍽️一个讲清 API 的故事 🍽️
An API (Application Programming Interface) works the same way. A server somewhere has data, or can do some work. It publishes a "menu" of URLs you can call. You send a request in a standard format. The server does the kitchen work and sends back a plate — usually a JSON document, sometimes XML.
Your Python program is the customer.
requests is the waiter. The API is the menu. That is the whole idea.API (Application Programming Interface) भी ऐसे ही काम करता है। कहीं एक server के पास data है, या वह कोई काम कर सकता है। वह URLs का एक "menu" प्रकाशित करता है जिन्हें आप call कर सकते हैं। आप तय format में request भेजते हैं। Server kitchen का काम करता है और plate वापस भेजता है — आम तौर पर एक JSON document, कभी-कभी XML।
आपका Python program ग्राहक है।
requests waiter है। API menu है। बस इतनी-सी बात है।API(应用程序编程接口)也是这样。某处的服务器有数据,或者能完成某项工作。它公布一份可以调用的 URL"菜单"。你按标准格式发送请求。服务器在"厨房"里干活,然后把"菜"送回来 — 通常是一个 JSON 文档,有时是 XML。
你的 Python 程序是顾客,
requests 是服务员,API 是菜单。整个思路就是这样。Every modern app talks to APIs — weather, payments, maps, AI models, stock prices, social feeds. In Python the standard tool is the requests library: one line to send an HTTP call, one line to parse the JSON. This tutorial goes from "what is an API" to "I can build a reliable client for a real service".
हर आधुनिक app APIs से बात करता है — मौसम, payments, maps, AI models, शेयर के भाव, social feeds। Python में मानक औज़ार requests library है: HTTP call भेजने के लिए एक line, JSON parse करने के लिए एक line। यह tutorial "API क्या है" से "मैं असली service के लिए भरोसेमंद client बना सकता हूँ" तक ले जाता है।
每个现代应用都在调用 API — 天气、支付、地图、AI 模型、股价、社交动态。在 Python 中,标准工具是 requests 库:一行代码发送 HTTP 请求,一行代码解析 JSON。本教程带你从"什么是 API"走到"我能为真实服务写出可靠的客户端"。
An API call is just a URL + a method + optional data. The server answers with a status code + optional data. Everything else — auth, pagination, retries, error handling — is polish around that one exchange.
API call बस URL + method + वैकल्पिक data है। Server status code + वैकल्पिक data से जवाब देता है। बाक़ी सब — auth, pagination, retries, error handling — उसी एक लेन-देन को निखारना है।
一次 API 调用就是 URL + 方法 + 可选数据。服务器回答 状态码 + 可选数据。其余一切 — 认证、分页、重试、错误处理 — 都是围绕这一次交换的打磨。
Every example was run for real with requests 2.34.2 on Python 3.13.16. The sandbox has no internet, so the hosts (api.github.com, httpbin.org, …) pointed to a local stand-in server with the same URLs and the same JSON shape. The code is unchanged — run it yourself and it talks to the real services. Live values such as star counts, names and headlines will differ, and they change every day. Seven simulators below are built from these real runs.
हर example requests 2.34.2 और Python 3.13.16 पर सच में चलाया गया। Sandbox में internet नहीं है, इसलिए hosts (api.github.com, httpbin.org, …) एक local stand-in server की ओर मोड़े गए, जिसके URLs और JSON का आकार वही है। Code नहीं बदला — आप ख़ुद चलाएँगे तो यह असली services से बात करेगा। Star counts, नाम और headlines जैसी live values अलग होंगी, और रोज़ बदलती हैं। नीचे के सात simulators इन्हीं असली runs से बने हैं।
每个示例都用 requests 2.34.2 在 Python 3.13.16 上真实运行过。沙箱没有互联网,所以这些主机(api.github.com、httpbin.org 等)被指向一台 本地替身服务器,它的 URL 和 JSON 结构 与真实服务相同。代码没有改动 — 你自己运行时就会访问真实服务。星标数、名字、标题等实时数据会不同,而且每天都在变。下面的七个模拟器都来自这些真实运行。
Visual Diagram — The HTTP Request / Response Cycle 🔄चित्र — HTTP Request / Response चक्र 🔄图解 — HTTP 请求 / 响应循环 🔄
One request goes out (green), one response comes back (orange).
एक request जाती है (हरी), एक response लौटता है (नारंगी)।
一个请求发出(绿色),一个响应返回(橙色)。
Every API call — however fancy — is one arrow going right and one arrow coming back. Everything else in this tutorial is a variation on that pattern.
हर API call — चाहे कितनी भी जटिल हो — एक तीर दाईं ओर जाता है और एक वापस आता है। इस tutorial में बाक़ी सब इसी pattern के रूप हैं।
每次 API 调用 — 无论多复杂 — 都是一支箭头向右发出,一支箭头返回。本教程的其余内容都是这个模式的变化。
The Three Building Blocks of an API Call 🧱API Call की तीन ईंटें 🧱API 调用的三块基石 🧱
https://api.github.com/users/torvalds
https://api.github.com/users/torvalds
https://api.github.com/users/torvalds
?city=London), a JSON body, form data, headers, cookies and auth tokens — the specifics of your request.?city=London), JSON body, form data, headers, cookies और auth tokens — आपकी request की बारीकियाँ।?city=London)、JSON 请求体、表单数据、请求头、Cookie 和认证令牌 — 请求的具体内容。Your First API Callआपकी पहली API Call你的第一次 API 调用
# pip install requests
import requests
# Public API - no auth required
r = requests.get("https://api.github.com/users/torvalds", timeout=5)
print(r.status_code) # 200 = success
print(r.headers["Content-Type"]) # what kind of data came back
data = r.json() # parse JSON -> Python dict
print(data["name"])
print(data["public_repos"])
print(data["bio"]) # JSON null -> Python None
r.json() turned the JSON text into a normal Python dict. JSON null became Python None — that is why bio printed None.
r.json() ने JSON text को सामान्य Python dict में बदला। JSON का null Python का None बन गया — इसीलिए bio ने None print किया।
r.json() 把 JSON 文本变成了普通的 Python dict。JSON 的 null 变成了 Python 的 None — 所以 bio 打印出 None。
1. It is not in the standard library — run pip install requests. 2. Every call returns a Response object with .status_code, .headers, .text, .content and .json(). 3. By default nothing is retried and there is no timeout — a silent server can make your program wait for ever. Every real call needs timeout=.
1. यह standard library में नहीं है — pip install requests चलाइए। 2. हर call एक Response object लौटाती है जिसमें .status_code, .headers, .text, .content और .json() हैं। 3. Default रूप से कुछ भी retry नहीं होता और कोई timeout नहीं होता — चुप server आपके program को हमेशा इंतज़ार करा सकता है। हर असली call को timeout= चाहिए।
1. 它不在标准库中 — 需要运行 pip install requests。2. 每次调用都返回一个 Response 对象,带有 .status_code、.headers、.text、.content 和 .json()。3. 默认情况下不会重试,也 没有超时 — 一个不回应的服务器会让程序永远等下去。每个真实调用都需要 timeout=。
The original first call had no timeout=, while its own tip says every serious call needs one — we added it. Also, the printed numbers (like public_repos) are live data: they were a snapshot when the original was written and are different today.
मूल पहली call में timeout= नहीं था, जबकि उसकी अपनी tip कहती है कि हर गंभीर call को इसकी ज़रूरत है — हमने जोड़ दिया। साथ ही, print हुए numbers (जैसे public_repos) live data हैं: वे मूल लिखते समय का snapshot थे और आज अलग हैं।
原文的第一个调用没有 timeout=,而它自己的提示却说每个正式调用都需要 — 我们补上了。另外,打印出的数字(如 public_repos)是实时数据:那只是原文写作时的快照,今天已经不同。
The HTTP Methods — What They Mean, When to Use Each 🚦HTTP Methods — मतलब, और कब कौन-सा 🚦HTTP 方法 — 含义与使用时机 🚦
| MethodMethod方法 | Purposeउद्देश्य用途 | Idempotent?Idempotent?幂等? | requests callrequests callrequests 调用 |
|---|---|---|---|
GET | Read data — no changes on the serverData पढ़ना — server पर कोई बदलाव नहीं读取数据 — 不改变服务器 | Yesहाँ是 | requests.get(url) |
POST | Create a new resource (or trigger an action)नया resource बनाना (या कोई action चलाना)创建新资源(或触发某个操作) | Noनहीं否 | requests.post(url, json=...) |
PUT | Replace a resource entirelyResource को पूरा बदलना整体替换资源 | Yesहाँ是 | requests.put(url, json=...) |
PATCH | Partial update — change some fieldsआंशिक बदलाव — कुछ fields部分更新 — 只改部分字段 | Not guaranteedगारंटी नहीं不保证 | requests.patch(url, json=...) |
DELETE | Remove a resourceResource हटाना删除资源 | Yesहाँ是 | requests.delete(url) |
HEAD | Like GET, but the response has no body — check existence or headersGET जैसा, पर response में body नहीं — मौजूदगी या headers जाँचने के लिए与 GET 类似,但响应没有响应体 — 用来检查是否存在或查看响应头 | Yesहाँ是 | requests.head(url) |
Idempotent means: sending the same request twice leaves the server in the same state as sending it once. It is about the server state, not the reply — a second DELETE may answer 404, but the item is still just gone.
Idempotent का मतलब: एक ही request दो बार भेजने पर server की हालत वैसी ही रहती है जैसी एक बार भेजने पर। यह server की हालत के बारे में है, जवाब के बारे में नहीं — दूसरा DELETE 404 दे सकता है, पर item बस हटा हुआ ही है।
幂等 的意思是:同一个请求发两次,服务器的状态与只发一次相同。它关注的是 服务器状态,而不是回复 — 第二次 DELETE 可能返回 404,但该条目依然只是被删除了。
The original table marked PATCH as "Usually" idempotent. The HTTP standard (RFC 5789) says PATCH is not idempotent in general. "Set title to X" is safe to repeat; "add one like" is not. The simulator below shows both.
मूल table में PATCH को "Usually" idempotent बताया गया था। HTTP मानक (RFC 5789) कहता है कि PATCH सामान्यतः idempotent नहीं है। "title को X कर दो" दोहराना सुरक्षित है; "एक like जोड़ो" नहीं। नीचे का simulator दोनों दिखाता है।
原表格把 PATCH 标为"通常"幂等。HTTP 标准(RFC 5789)规定 PATCH 一般来说 不是 幂等的。"把标题设为 X"可以安全重复;"点赞数加一"则不行。下面的模拟器两种都展示了。
import requests
BASE = "https://jsonplaceholder.typicode.com" # public sandbox API
# GET - read one post
r = requests.get(f"{BASE}/posts/1", timeout=5)
print("GET ", r.status_code, r.json()["title"][:30])
# POST - create a new post
new_post = {"title": "Hello", "body": "World", "userId": 1}
r = requests.post(f"{BASE}/posts", json=new_post, timeout=5)
print("POST ", r.status_code, r.json())
# PUT - replace an existing post
r = requests.put(f"{BASE}/posts/1",
json={"id": 1, "title": "Updated", "body": "New", "userId": 1},
timeout=5)
print("PUT ", r.status_code, r.json())
# PATCH - change only the title
r = requests.patch(f"{BASE}/posts/1", json={"title": "Just the title"}, timeout=5)
print("PATCH ", r.status_code, r.json()["title"], "| body kept:", r.json()["body"][:12] + "...")
# DELETE - remove
r = requests.delete(f"{BASE}/posts/1", timeout=5)
print("DELETE", r.status_code, r.json())
JSONPlaceholder is a fake API for practice: it answers like a real one (201 for POST, the new id 101), but it never really saves anything. That is perfect for learning — and it is why we use a stateful test API in the simulator below.
JSONPlaceholder अभ्यास के लिए एक नक़ली API है: यह असली की तरह जवाब देता है (POST पर 201, नया id 101), पर असल में कुछ भी save नहीं करता। सीखने के लिए यह बढ़िया है — और इसीलिए नीचे के simulator में हम ऐसा test API लेते हैं जो हालत याद रखता है।
JSONPlaceholder 是一个用于练习的假 API:它像真的一样回应(POST 返回 201,新 id 为 101),但从不真正保存任何东西。这非常适合学习 — 这也是为什么下面的模拟器使用了一个 会保存状态 的测试 API。
🔁 Simulator: Send It Twice🔁 Simulator: दो बार भेजिए🔁 模拟器:发送两次
Pick a request. It is sent twice to a small notes API that really keeps state. Compare the server state after call 1 and after call 2: if they match, the request is idempotent.
एक request चुनिए। इसे एक छोटे notes API को दो बार भेजा जाता है जो सच में हालत याद रखता है। Call 1 और call 2 के बाद server की हालत मिलाइए: अगर दोनों मेल खाएँ, तो request idempotent है।
选一个请求。它会被 发送两次 到一个真正保存状态的小型笔记 API。比较第 1 次和第 2 次调用后的服务器状态:如果相同,这个请求就是幂等的。
Query Params vs Request Body — When to Use Which 📮Query Params बनाम Request Body — कब कौन-सा 📮查询参数 vs 请求体 — 何时用哪个 📮
There are two places to send data with a request: the URL (query string) or the body. Mixing them up is a very common reason for "why won't my POST work?".
Request के साथ data भेजने की दो जगहें हैं: URL (query string) या body। इन्हें गड़बड़ाना "मेरा POST क्यों नहीं चल रहा?" का बहुत आम कारण है।
随请求发送数据有两个位置:URL(查询字符串)或 请求体。把两者搞混是"为什么我的 POST 不工作?"的一个非常常见的原因。
| Aspectपहलू方面 | Detailविवरण细节 |
|---|---|
| Whereकहाँ位置 | After ? in the URLURL में ? के बादURL 中 ? 之后 |
| Typical useआम उपयोग典型用途 | GET — filters, pagination, searchGET — filters, pagination, searchGET — 筛选、分页、搜索 |
| Visible in logsLogs में दिखता日志中可见 | Yes — never put secrets hereहाँ — यहाँ secrets कभी न रखें是 — 千万别放机密 |
| Exampleउदाहरण示例 | ?city=Tokyo&units=metric |
| Aspectपहलू方面 | Detailविवरण细节 |
|---|---|
| Whereकहाँ位置 | Inside the HTTP bodyHTTP body के अंदरHTTP 请求体内部 |
| Typical useआम उपयोग典型用途 | POST / PUT / PATCH — new or changed recordsPOST / PUT / PATCH — नए या बदले recordsPOST / PUT / PATCH — 新建或修改记录 |
| Visible in logsLogs में दिखता日志中可见 | Usually not — safer for structured dataआम तौर पर नहीं — structured data के लिए सुरक्षित通常不会 — 更适合结构化数据 |
| Exampleउदाहरण示例 | {"name": "Ada", "age": 30} |
import requests
# Query params - passed via params={}
r = requests.get(
"https://api.open-meteo.com/v1/forecast",
params={
"latitude": 35.68,
"longitude": 139.65,
"current_weather": True,
},
timeout=5,
)
print(r.url) # the URL requests actually sent
# JSON body - passed via json={}
r = requests.post(
"https://httpbin.org/post",
json={"name": "Ada", "role": "engineer"},
timeout=5,
)
print(r.json()["json"]) # httpbin echoes back what you sent
print(r.request.headers["Content-Type"])
Look at the first line: params= built the whole query string for you, and Python's True was sent as the text True. With json=, requests turned the dict into JSON text and set Content-Type: application/json by itself.
पहली line देखिए: params= ने पूरी query string आपके लिए बनाई, और Python का True text True के रूप में गया। json= के साथ requests ने dict को JSON text में बदला और ख़ुद Content-Type: application/json लगाया।
看第一行:params= 替你拼好了整个查询字符串,Python 的 True 被当作文本 True 发送。使用 json= 时,requests 把字典转成 JSON 文本,并自动设置 Content-Type: application/json。
Writing url + "?q=" + user_input breaks on spaces, &, +, # and non-English letters. Always pass params={...} and let requests encode everything. Same rule for JSON bodies: use json={...}, never hand-write the JSON string.
url + "?q=" + user_input लिखना spaces, &, +, # और ग़ैर-अंग्रेज़ी अक्षरों पर टूट जाता है। हमेशा params={...} दीजिए और requests को सब encode करने दीजिए। JSON bodies के लिए भी यही नियम: json={...} लीजिए, JSON string कभी हाथ से मत लिखिए।
写 url + "?q=" + user_input 遇到空格、&、+、# 和非英文字符就会出错。始终传入 params={...},让 requests 来编码一切。JSON 请求体也一样:用 json={...},永远不要手写 JSON 字符串。
import requests
user_input = "São Paulo & Rio"
# WRONG - glue the URL together by hand
r = requests.get("https://httpbin.org/get?q=" + user_input, timeout=5)
print("concat :", r.json()["args"])
# RIGHT - let requests encode it
r = requests.get("https://httpbin.org/get", params={"q": user_input}, timeout=5)
print("params :", r.json()["args"])
print("sent :", r.url)
🔤 Simulator: URL Encoding Lab🔤 Simulator: URL Encoding Lab🔤 模拟器:URL 编码实验室
Pick a search term. Both versions were really sent; the boxes show the URL that went out and what the server actually received.
एक search term चुनिए। दोनों versions सच में भेजे गए; डिब्बे दिखाते हैं कि कौन-सा URL गया और server को असल में क्या मिला।
选一个搜索词。两个版本都真实发送过;方框里显示发出的 URL 以及服务器实际收到的内容。
Parsing JSON — The Most Common Response Format 🧾JSON Parse करना — सबसे आम Response Format 🧾解析 JSON — 最常见的响应格式 🧾
Most modern web APIs speak JSON. requests parses it with one method call, and you get back normal Python objects: dict, list, str, int, float, bool and None.
ज़्यादातर आधुनिक web APIs JSON बोलते हैं। requests इसे एक method call से parse करता है, और आपको सामान्य Python objects मिलते हैं: dict, list, str, int, float, bool और None।
大多数现代 Web API 都使用 JSON。requests 一次方法调用就能解析它,你得到的是普通的 Python 对象:dict、list、str、int、float、bool 和 None。
| JSONJSONJSON | Python after r.json()r.json() के बाद Pythonr.json() 之后的 Python |
|---|---|
{"a": 1} (object) | dict |
[1, 2] (array) | list |
"text" | str |
42 / 4.2 | int / float |
true / false | True / False |
null | None |
import requests
r = requests.get("https://api.github.com/repos/python/cpython", timeout=5)
data = r.json() # dict[str, Any]
print(data["full_name"])
print(data["stargazers_count"])
print(data["language"])
print(data["license"]["name"]) # nested access is just dict[key]
# Lists in JSON - same rules as normal Python
r = requests.get("https://api.github.com/repos/python/cpython/contributors",
params={"per_page": 3}, timeout=5)
for contributor in r.json():
print(f"{contributor['login']:18s} {contributor['contributions']:>5} commits")
The original showed fixed numbers, such as 62483 stars and "gvanrossum 1230 commits", as if they were the output. Live API data changes every day, and the contributor list is sorted by commit count, which also changes. Our output comes from the stand-in server — treat every number on this page as an example, not a fact about the repository.
मूल में 62483 stars और "gvanrossum 1230 commits" जैसे तय numbers ऐसे दिखाए गए जैसे वे output हों। Live API data रोज़ बदलता है, और contributor सूची commit संख्या से क्रमित होती है, जो भी बदलती है। हमारा output stand-in server से है — इस page के हर number को उदाहरण मानिए, repository के बारे में तथ्य नहीं।
原文把 62483 颗星、"gvanrossum 1230 commits"这样的固定数字当作输出展示。实时 API 数据每天都在变,贡献者列表按提交次数排序,这个次数也在变。我们的输出来自替身服务器 — 请把本页每个数字都当作示例,而不是关于该仓库的事实。
Safe Access to Optional KeysOptional Keys तक सुरक्षित पहुँच安全访问可选键
import requests
data = requests.get("https://api.github.com/users/torvalds", timeout=5).json()
# The key may be missing, or present with the value null (None)
print(data.get("bio")) # None (here: present, but null)
print(data.get("bio") or "no bio available") # handles missing AND null
print(data.get("twitter", "no such key")) # key missing -> default
# Nested optional access - "or {}" protects against a missing or null parent
license_name = (data.get("license") or {}).get("name", "unknown")
print(license_name)
print(data["twitter"]) # KeyError: missing key
Two different problems look alike: a key that is missing (data["twitter"] → KeyError) and a key that is present with the value null (data["bio"] → None). data.get(key) or default handles both.
दो अलग समस्याएँ एक जैसी दिखती हैं: ग़ायब key (data["twitter"] → KeyError) और मौजूद key जिसकी value null है (data["bio"] → None)। data.get(key) or default दोनों संभालता है।
两个看起来相似的问题其实不同:键 缺失(data["twitter"] → KeyError),以及键存在但值为 null(data["bio"] → None)。data.get(key) or default 两种情况都能处理。
The original comment said "walrus + get chains", but the line (data.get("license") or {}).get("name", "unknown") uses no walrus operator (:=). It uses or {} so that a missing or null parent becomes an empty dict.
मूल comment में "walrus + get chains" लिखा था, पर (data.get("license") or {}).get("name", "unknown") line में कोई walrus operator (:=) नहीं है। यह or {} लेती है ताकि ग़ायब या null parent ख़ाली dict बन जाए।
原注释写的是"walrus + get chains",但 (data.get("license") or {}).get("name", "unknown") 这一行根本没有海象运算符(:=)。它用的是 or {},让缺失 或 为 null 的父对象变成空字典。
Parsing XML — For Legacy and Enterprise APIs 🌳XML Parse करना — पुराने और Enterprise APIs के लिए 🌳解析 XML — 用于旧系统和企业 API 🌳
XML still lives on in older or enterprise services — SOAP endpoints, government data feeds, RSS/Atom feeds, some payment gateways. Python's standard library reads it with xml.etree.ElementTree.
XML अब भी पुरानी या enterprise services में है — SOAP endpoints, सरकारी data feeds, RSS/Atom feeds, कुछ payment gateways। Python की standard library इसे xml.etree.ElementTree से पढ़ती है।
XML 仍存在于较老的或企业级服务中 — SOAP 端点、政府数据源、RSS/Atom 订阅、一些支付网关。Python 标准库用 xml.etree.ElementTree 读取它。
import requests
import xml.etree.ElementTree as ET
# Example: NASA's news RSS feed (XML)
r = requests.get("https://www.nasa.gov/feed/", timeout=5)
root = ET.fromstring(r.content) # root is the outermost <rss> element
print(root.tag, root.attrib)
# RSS structure: rss > channel > item*
channel = root.find("channel")
for item in channel.findall("item")[:3]:
title = item.findtext("title")
link = item.findtext("link")
date = item.findtext("pubDate")
print(f"- {title}\n {link}\n {date}")
find() walks one branch; findall() lists all children with a tag.
find() एक शाखा पर चलता है; findall() किसी tag के सारे बच्चे देता है।
find() 沿一条分支走;findall() 列出某个标签的所有子节点。
The original comment called https://www.nasa.gov/feed/ the "Astronomy Picture of the Day" feed — it is NASA's general news feed (APOD is a separate site). It also said the standard library must be replaced by defusedxml "to block XXE attacks". Python's docs say ElementTree does not load external entities, so it is safe from XXE, and with expat 2.4.1 or newer (this Python has 2.6.1) it is also protected against "billion laughs". defusedxml is still a good extra guard for hostile XML, but it is not required for XXE.
मूल comment ने https://www.nasa.gov/feed/ को "Astronomy Picture of the Day" feed कहा — यह NASA की सामान्य news feed है (APOD अलग site है)। यह भी कहा गया कि "XXE attacks रोकने के लिए" standard library की जगह defusedxml लेना ज़रूरी है। Python docs कहते हैं कि ElementTree external entities load नहीं करता, इसलिए XXE से सुरक्षित है, और expat 2.4.1 या नए (इस Python में 2.6.1) के साथ "billion laughs" से भी बचा है। शत्रुतापूर्ण XML के लिए defusedxml अब भी अच्छा अतिरिक्त बचाव है, पर XXE के लिए ज़रूरी नहीं।
原注释把 https://www.nasa.gov/feed/ 称为"每日天文图片"订阅源 — 其实它是 NASA 的综合新闻订阅源(APOD 是另一个网站)。原文还说必须用 defusedxml 替换标准库"以阻止 XXE 攻击"。Python 文档说明 ElementTree 不加载外部实体,因此不受 XXE 影响;在 expat 2.4.1 或更新版本(本 Python 为 2.6.1)下,它也能防御"billion laughs"。对于恶意 XML,defusedxml 仍是不错的额外防护,但并非防 XXE 所必需。
Authentication — Proving Who You Are 🔑Authentication — आप कौन हैं यह साबित करना 🔑认证 — 证明你是谁 🔑
Most useful APIs need you to identify yourself. There are three common patterns. In every case, credentials belong in environment variables — never in code, never in git.
ज़्यादातर काम के APIs आपसे पहचान माँगते हैं। तीन आम patterns हैं। हर हाल में credentials environment variables में रहते हैं — code में कभी नहीं, git में कभी नहीं।
大多数有用的 API 都要求你表明身份。常见的模式有三种。无论哪种,凭据都应放在环境变量中 — 绝不写进代码,绝不提交到 git。
Authorization: Bearer <token> header. Used by GitHub, OpenAI and many modern APIs.Authorization: Bearer <token> header में भेजा जाता है। GitHub, OpenAI और कई आधुनिक APIs इसे लेते हैं।Authorization: Bearer <token> 请求头中发送。GitHub、OpenAI 和许多现代 API 都在用。import os, requests
# 1) API key as query param - the API tells you the parameter name
r = requests.get(
"https://api.openweathermap.org/data/2.5/weather",
params={"q": "Mumbai", "appid": os.environ["OWM_KEY"]},
timeout=5,
)
print("1) key in URL :", r.status_code, r.json()["name"])
# 2) API key as header - many modern APIs prefer this
r = requests.get(
"https://api.example.com/data",
headers={"X-API-Key": os.environ["API_KEY"]},
timeout=5,
)
print("2) key in header:", r.status_code, r.json())
# 3) Bearer token - GitHub, OpenAI, Stripe pattern
token = os.environ["GITHUB_TOKEN"]
r = requests.get(
"https://api.github.com/user",
headers={"Authorization": f"Bearer {token}"},
timeout=5,
)
print("3) bearer token :", r.status_code, r.json()["login"])
# 4) Basic auth - requests has built-in support
r = requests.get(
"https://api.example.com/private",
auth=(os.environ["API_USER"], os.environ["API_PASSWORD"]),
timeout=5,
)
print("4) basic auth :", r.status_code, r.json())
print(" header sent :", r.request.headers["Authorization"])
Look at the last line: YWRhOnMzY3JldA== is just ada:s3cret in base64. Anyone who sees it can decode it in a second — that is why Basic auth needs HTTPS.
आख़िरी line देखिए: YWRhOnMzY3JldA== बस base64 में ada:s3cret है। जो भी इसे देखे, एक सेकंड में decode कर सकता है — इसीलिए Basic auth को HTTPS चाहिए।
看最后一行:YWRhOnMzY3JldA== 只是 ada:s3cret 的 base64 形式。任何看到它的人一秒钟就能解码 — 所以 Basic 认证必须配合 HTTPS。
Log in once, then send the token with every call.
एक बार login, फिर हर call के साथ token भेजिए।
登录一次,之后每次调用都带上令牌。
The original Basic-auth example read the username from os.environ["USER"]. On Linux and macOS, USER is already set by the operating system to your login name — so the API gets the wrong user. Use your own names, such as API_USER and API_PASSWORD (as in the fixed code above). The run below shows the failure.
मूल Basic-auth example ने username os.environ["USER"] से पढ़ा। Linux और macOS पर USER पहले से operating system ने आपके login नाम पर set किया होता है — तो API को ग़लत user मिलता है। अपने नाम लीजिए, जैसे API_USER और API_PASSWORD (जैसा ऊपर के सुधरे code में)। नीचे का run विफलता दिखाता है।
原 Basic 认证示例从 os.environ["USER"] 读取用户名。在 Linux 和 macOS 上,USER 已经被操作系统设置为 你的登录名 — 于是 API 收到了错误的用户。请使用自己的变量名,例如 API_USER 和 API_PASSWORD(如上面修正后的代码)。下面的运行展示了失败情况。
import os, requests
# Danger: USER is already set by your operating system!
print("USER is:", os.environ["USER"]) # your login name, not the API user
r = requests.get("https://api.example.com/private",
auth=(os.environ["USER"], os.environ["API_PASSWORD"]), timeout=5)
print(r.status_code, r.json())
r = requests.get("https://api.github.com/user",
headers={"Authorization": "Bearer wrong-token"}, timeout=5)
print(r.status_code, r.json()["message"])
Never paste an API key or token into your source code. Put it in an environment variable, a .env file listed in .gitignore, or a secrets manager. Load it with os.environ["KEY"] or python-dotenv. Keys leaked in git history are one of the most common security incidents on GitHub.
API key या token को अपने source code में कभी न चिपकाएँ। उसे environment variable, .gitignore में दर्ज .env file, या secrets manager में रखिए। os.environ["KEY"] या python-dotenv से load कीजिए। Git history में leak हुई keys GitHub पर सबसे आम security घटनाओं में से हैं।
永远不要 把 API 密钥或令牌粘贴进源代码。把它放在环境变量、已列入 .gitignore 的 .env 文件或密钥管理器中。用 os.environ["KEY"] 或 python-dotenv 加载。泄露在 git 历史中的密钥是 GitHub 上最常见的安全事件之一。
Handling Status Codes and Errors 🚨Status Codes और Errors संभालना 🚨处理状态码与错误 🚨
The status code is your first check on every response. Ignoring it is how you get confusing bugs at 3 AM.
Status code हर response पर आपकी पहली जाँच है। इसे अनदेखा करना ही रात 3 बजे उलझाने वाले bugs की वजह बनता है।
状态码是每个响应的第一道检查。忽视它,就会在凌晨 3 点遇到让人摸不着头脑的 bug。
The first digit tells you whose problem it is.
पहला अंक बताता है कि समस्या किसकी है।
第一位数字告诉你是谁的问题。
import requests
r = requests.get("https://api.github.com/repos/does-not/exist", timeout=5)
# Option A: check the code manually
if r.status_code == 200:
print(r.json())
elif r.status_code == 404:
print("repo does not exist")
elif r.status_code == 429:
print("rate limited, wait and retry")
else:
print(f"unexpected: {r.status_code}")
# Option B: let requests raise on any 4xx/5xx
try:
r = requests.get("https://api.github.com/repos/does-not/exist", timeout=5)
r.raise_for_status() # raises HTTPError on 4xx/5xx
data = r.json()
except requests.HTTPError as e:
print(f"HTTP failure: {e}")
except requests.Timeout:
print("server took too long")
except requests.ConnectionError:
print("can't reach server (DNS / offline)")
print("r.ok =", r.ok)
The Timeout and ConnectionError branches never ran above. Here they are, triggered on purpose:
ऊपर Timeout और ConnectionError वाली branches कभी नहीं चलीं। यहाँ वे जान-बूझकर चलाई गई हैं:
上面的 Timeout 和 ConnectionError 分支都没有执行。这里我们故意触发它们:
import requests
for url in ["https://api.example.com/hang", # never answers
"https://no-such-host.invalid/"]: # DNS fails
try:
requests.get(url, timeout=1.5)
except requests.Timeout as e:
print("Timeout :", type(e).__name__)
except requests.ConnectionError as e:
print("ConnectionError:", type(e).__name__)
The original diagram said requests follows all 3xx "auto". It follows real redirects such as 301 and 302 (see r.history in the explorer below). But 304 Not Modified has no new location — it is a cache answer, returned as is. And requests.head() does not follow redirects unless you pass allow_redirects=True. Also, 429 is a 4xx code that you should retry (after waiting).
मूल चित्र में कहा गया कि requests हर 3xx को "auto" follow करता है। यह 301 और 302 जैसे असली redirects follow करता है (नीचे explorer में r.history देखें)। पर 304 Not Modified में कोई नई जगह नहीं होती — यह cache वाला जवाब है, जैसा है वैसा लौटता है। और requests.head() redirects follow नहीं करता जब तक आप allow_redirects=True न दें। साथ ही, 429 एक 4xx code है जिसे (इंतज़ार के बाद) retry करना चाहिए।
原图说 requests 会"自动"跟随所有 3xx。它确实会跟随 301、302 这样的真正重定向(见下方探索器中的 r.history)。但 304 Not Modified 没有新地址 — 它是缓存应答,会原样返回。而且 requests.head() 默认 不 跟随重定向,除非传入 allow_redirects=True。另外,429 虽是 4xx,却 应该 在等待后重试。
🚦 Simulator: Status Code Explorer🚦 Simulator: Status Code Explorer🚦 模拟器:状态码探索器
Click a status code. Each one was really requested, and the panel shows what requests did with it: r.ok, raise_for_status(), r.json() and redirects.
एक status code पर click कीजिए। हर एक सच में माँगा गया, और panel दिखाता है कि requests ने उसके साथ क्या किया: r.ok, raise_for_status(), r.json() और redirects।
点击一个状态码。每个都真实请求过,面板显示 requests 如何处理它:r.ok、raise_for_status()、r.json() 以及重定向。
Retries with Exponential Backoff ⏳Exponential Backoff के साथ Retries ⏳指数退避重试 ⏳
Networks fail. Servers hiccup. Rate limits happen. A good client retries temporary errors with a growing pause, so the server has room to recover. This is called exponential backoff.
Networks fail होते हैं। Servers अटकते हैं। Rate limits लगते हैं। अच्छा client अस्थायी errors को बढ़ते विराम के साथ retry करता है, ताकि server संभल सके। इसे exponential backoff कहते हैं।
网络会故障,服务器会卡顿,也会遇到限流。好的客户端会以逐渐变长的间隔重试 临时 错误,给服务器恢复的空间。这叫做 指数退避。
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# Built-in retry - no manual loop needed
retry_policy = Retry(
total=5, # up to 5 RETRIES (6 attempts in all)
backoff_factor=1.0, # sleeps between tries: 0, 2, 4, 8, 16 s
status_forcelist=[429, 500, 502, 503, 504],
allowed_methods=["GET", "HEAD", "PUT", "DELETE", "OPTIONS"], # idempotent only
respect_retry_after_header=True,
)
session = requests.Session()
session.mount("https://", HTTPAdapter(max_retries=retry_policy))
# Now every request through this session auto-retries transient errors
r = session.get("https://api.github.com/users/torvalds", timeout=10)
print(r.status_code)
Three corrections to the original. (1) total=5 means up to 5 retries — 6 attempts in all, not "at most 5 attempts". (2) With urllib3 2.8.0 the first retry happens at once; the sleeps are 0, 2, 4, 8, 16 s for backoff_factor=1.0, not 1, 2, 4, 8, 16 (the run below measures it). (3) The original put "POST" in allowed_methods. POST is not idempotent: if the server did the work but the reply was lost, a retry does it again — two orders, two payments. The default list leaves POST out on purpose.
मूल में तीन सुधार। (1) total=5 का मतलब है 5 तक retries — कुल 6 प्रयास, "ज़्यादा से ज़्यादा 5 प्रयास" नहीं। (2) urllib3 2.8.0 में पहला retry तुरंत होता है; backoff_factor=1.0 के लिए विराम 0, 2, 4, 8, 16 s हैं, 1, 2, 4, 8, 16 नहीं (नीचे का run इसे मापता है)। (3) मूल ने "POST" को allowed_methods में रखा। POST idempotent नहीं है: अगर server ने काम कर दिया पर जवाब खो गया, तो retry उसे फिर करता है — दो orders, दो payments। Default सूची जान-बूझकर POST को बाहर रखती है।
对原文的三处修正。(1) total=5 表示最多 5 次 重试 — 总共 6 次尝试,而不是"最多 5 次尝试"。(2) 在 urllib3 2.8.0 中,第一次重试会立即进行;backoff_factor=1.0 时的等待是 0、2、4、8、16 秒,而不是 1、2、4、8、16(下面的运行实测了这一点)。(3) 原文把 "POST" 放进了 allowed_methods。POST 不是幂等的:如果服务器已经完成了工作但回复丢失,重试会 再做一次 — 两个订单、两次付款。默认列表有意不包含 POST。
import time, requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
# A test endpoint that fails twice with 503, then succeeds
URL = "https://api.example.com/flaky/demo?fail=2&code=503"
retry = Retry(total=5, backoff_factor=0.5, status_forcelist=[429, 500, 502, 503, 504])
s = requests.Session()
s.mount("https://", HTTPAdapter(max_retries=retry))
t0 = time.perf_counter()
r = s.get(URL, timeout=5)
print(r.status_code, r.json(), f"after {time.perf_counter() - t0:.1f} s")
log = requests.get("https://api.example.com/flaky-log/demo", timeout=5).json()
gaps = [round(b - a, 1) for a, b in zip(log["t"], log["t"][1:])]
print("server saw", log["n"], "attempts; gaps between them:", gaps, "s")
🔁 Simulator: Retry Lab🔁 Simulator: Retry Lab🔁 模拟器:重试实验室
A test server fails a chosen number of times, then succeeds. The client uses Retry(total=3, backoff_factor=0.5). Every dot is a real attempt, placed at the time the server saw it. For POST, this server saves the order even when it answers with an error — just like a real server whose reply gets lost.
एक test server चुनी गई बार fail होता है, फिर सफल होता है। Client Retry(total=3, backoff_factor=0.5) लेता है। हर बिंदु एक असली प्रयास है, उसी समय पर जब server ने उसे देखा। POST के लिए यह server error देते हुए भी order save कर लेता है — बिल्कुल उस असली server की तरह जिसका जवाब खो जाता है।
一台测试服务器先失败指定的次数,然后成功。客户端使用 Retry(total=3, backoff_factor=0.5)。每个圆点都是一次真实尝试,放在服务器收到它的时间点上。对于 POST,这台服务器即使返回错误也会保存订单 — 就像回复丢失的真实服务器一样。
Retry network errors (timeouts, dropped connections), 429 Too Many Requests (wait for Retry-After) and 5xx server errors — these are usually temporary. Do not retry other 4xx errors (400, 401, 404): the request itself is wrong, so trying again only wastes time. And only retry non-idempotent requests like POST if the API gives you an idempotency key.
Network errors (timeouts, टूटे connections), 429 Too Many Requests (Retry-After तक रुकिए) और 5xx server errors retry कीजिए — ये आम तौर पर अस्थायी होते हैं। बाक़ी 4xx errors (400, 401, 404) retry न करें: request ही ग़लत है, दोबारा भेजना बस समय गँवाना है। और POST जैसी non-idempotent requests तभी retry करें जब API idempotency key देता हो।
重试 网络错误(超时、连接中断)、429 Too Many Requests(等待 Retry-After)和 5xx 服务器错误 — 它们通常是暂时的。不要重试其他 4xx 错误(400、401、404):请求本身就错了,重试只是浪费时间。像 POST 这样的非幂等请求,只有在 API 提供幂等键时才应重试。
Sessions — Connection Reuse for Speed 🔌Sessions — Speed के लिए Connection दोबारा इस्तेमाल 🔌Session — 复用连接以提速 🔌
If you make more than one request to the same host, use a Session. It keeps the TCP + TLS connection open and reuses it, so later requests skip the connection setup (TCP handshake and TLS negotiation). A Session also keeps headers and cookies for you.
अगर आप एक ही host को एक से ज़्यादा requests भेजते हैं, तो Session लीजिए। यह TCP + TLS connection खुला रखकर दोबारा इस्तेमाल करता है, इसलिए बाद की requests connection बनाने का काम (TCP handshake और TLS negotiation) छोड़ देती हैं। Session आपके लिए headers और cookies भी याद रखता है।
如果你要向同一主机发送多个请求,就用 Session。它保持 TCP + TLS 连接打开并复用,后续请求就能跳过建立连接的步骤(TCP 握手和 TLS 协商)。Session 还会替你保存请求头和 Cookie。
| StepStep步骤 | Example costउदाहरण लागत示例耗时 |
|---|---|
| DNS lookupDNS lookupDNS 查询 | ~20 ms |
| TCP handshakeTCP handshakeTCP 握手 | ~30 ms |
| TLS handshakeTLS handshakeTLS 握手 | ~80 ms |
| Actual GETअसली GET实际 GET | ~50 ms |
| Per requestप्रति request每个请求 | ~180 ms |
| StepStep步骤 | Example costउदाहरण लागत示例耗时 |
|---|---|
| DNS lookupDNS lookupDNS 查询 | skipped (already connected)छूट गया (पहले से जुड़ा)跳过(已连接) |
| TCP handshakeTCP handshakeTCP 握手 | reusedदोबारा复用 |
| TLS handshakeTLS handshakeTLS 握手 | reusedदोबारा复用 |
| Actual GETअसली GET实际 GET | ~50 ms |
| Per requestप्रति request每个请求 | ~50 ms |
The millisecond numbers above are typical examples for a server far away on the internet. Real costs depend on distance and network. The run below is on one machine, so the gap is smaller — but the number of connections tells the real story.
ऊपर के millisecond numbers internet पर दूर के server के लिए आम उदाहरण हैं। असली लागत दूरी और network पर निर्भर है। नीचे का run एक ही machine पर है, इसलिए अंतर छोटा है — पर connections की गिनती असली कहानी बताती है।
上面的毫秒数是访问互联网上远端服务器的典型示例。实际耗时取决于距离和网络。下面的运行在同一台机器上,所以差距较小 — 但连接数说明了真正的问题。
import time, requests
N = 100
def stats(): return requests.get("https://api.example.com/conns?reset=1", timeout=5).json()
stats()
# Bad - new connection every call
t0 = time.perf_counter()
for i in range(N):
requests.get(f"https://api.github.com/repos/python/cpython/issues/{i + 1}", timeout=5)
t_bad = time.perf_counter() - t0
print(f"no session : {t_bad:.2f} s, connections opened: {stats()['connections'] - 1}")
# Good - one session, connections reused
token = "ghp_demo_token_123"
t0 = time.perf_counter()
with requests.Session() as s:
s.headers.update({"Authorization": f"Bearer {token}"})
for i in range(N):
r = s.get(f"https://api.github.com/repos/python/cpython/issues/{i + 1}",
timeout=5)
t_good = time.perf_counter() - t0
print(f"session : {t_good:.2f} s, connections opened: {stats()['connections'] - 1}")
print(f"speed-up : {t_bad / t_good:.1f}x")
The original "bad" loop called requests.get(...) with no timeout=, and the "good" loop used token without defining it, so the snippet could not run as written. It also started at issue number 0, which does not exist. The version above fixes all three and measures both loops.
मूल "bad" loop ने timeout= के बिना requests.get(...) बुलाया, और "good" loop ने token को define किए बिना इस्तेमाल किया, इसलिए snippet जैसा लिखा था वैसा चल नहीं सकता था। यह issue number 0 से भी शुरू होता था, जो मौजूद नहीं है। ऊपर का version तीनों सुधारता है और दोनों loops मापता है।
原"坏"循环调用 requests.get(...) 时没有 timeout=,"好"循环使用了未定义的 token,所以这段代码按原样无法运行。它还从编号 0 的 issue 开始,而该 issue 并不存在。上面的版本修正了这三点,并对两个循环都做了测量。
🔌 Simulator: Session Lab🔌 Simulator: Session Lab🔌 模拟器:Session 实验室
Choose how many requests to send. Each little block is one TCP + TLS connection the server really saw.
चुनिए कितनी requests भेजनी हैं। हर छोटा block एक TCP + TLS connection है जिसे server ने सच में देखा।
选择要发送多少个请求。每个小方块代表服务器真实看到的一个 TCP + TLS 连接。
Pagination — Fetching Big Result Sets 📚Pagination — बड़े Results लाना 📚分页 — 获取大量结果 📚
APIs return data in pages — you rarely get 10,000 items in one call. There are three common styles:
APIs data pages में लौटाते हैं — एक call में 10,000 items शायद ही मिलते हैं। तीन आम तरीके हैं:
API 以 分页 形式返回数据 — 一次调用很少能拿到 10,000 条。常见的方式有三种:
?page=1, ?page=2 … until an empty page arrives.?page=1, ?page=2 … माँगिए जब तक ख़ाली page न आए।?page=1、?page=2 …,直到返回空页。next link (GitHub puts it in the Link header). Follow it until there is none.next link होता है (GitHub इसे Link header में रखता है)। जब तक हो, उसे follow कीजिए।next 链接(GitHub 把它放在 Link 响应头中)。一直跟随,直到没有为止。?offset=200&limit=100. Increase the offset until fewer results than the limit come back.?offset=200&limit=100। Offset बढ़ाइए जब तक limit से कम results न आएँ।?offset=200&limit=100。不断增加 offset,直到返回的结果少于 limit。The original text said "two common patterns" and then listed three styles. There are three, as shown. The original code also used requests and os without importing them; the runnable version below adds the imports.
मूल text में "दो आम patterns" लिखा था और फिर तीन तरीके गिनाए। तीन ही हैं, जैसा दिखाया गया। मूल code ने requests और os को import किए बिना इस्तेमाल किया; नीचे का चलने वाला version imports जोड़ता है।
原文说"两种常见模式",却接着列出了三种。确实是三种,如上所示。原代码还在未导入的情况下使用了 requests 和 os;下面可运行的版本补上了导入。
import os, requests
def fetch_all_issues(repo: str, token: str) -> list[dict]:
session = requests.Session()
session.headers.update({"Authorization": f"Bearer {token}"})
all_issues, page = [], 1
while True:
r = session.get(f"https://api.github.com/repos/{repo}/issues",
params={"page": page, "per_page": 100, "state": "all"},
timeout=10)
r.raise_for_status()
batch = r.json()
print(f"page {page}: {len(batch)} items")
if not batch: # empty page = we're done
break
all_issues.extend(batch)
page += 1
return all_issues
issues = fetch_all_issues("python/cpython", os.environ["GITHUB_TOKEN"])
print(f"fetched {len(issues)} issues")
This works, but it spends one extra request on the empty page. GitHub sends a Link header with the next URL, and requests parses it for you as r.links:
यह चलता है, पर ख़ाली page पर एक अतिरिक्त request ख़र्च करता है। GitHub अगले URL के साथ Link header भेजता है, और requests उसे आपके लिए r.links के रूप में parse करता है:
这样可行,但会在空页上多花一次请求。GitHub 会发送带有下一页 URL 的 Link 响应头,requests 会把它解析为 r.links:
import os, requests
def fetch_all_issues(repo: str, token: str) -> list[dict]:
s = requests.Session()
s.headers.update({"Authorization": f"Bearer {token}"})
url = f"https://api.github.com/repos/{repo}/issues"
params = {"per_page": 100, "state": "all"}
items = []
while url:
r = s.get(url, params=params, timeout=10)
r.raise_for_status()
items.extend(r.json())
print(f"got {len(r.json())} items, next ->", r.links.get("next", {}).get("url"))
url = r.links.get("next", {}).get("url") # GitHub's Link header
params = None # the next URL already has them
return items
print("fetched", len(fetch_all_issues("python/cpython", os.environ["GITHUB_TOKEN"])))
GitHub's issues endpoint also returns pull requests (they carry a pull_request key). If you want only real issues, skip items that have that key.
GitHub का issues endpoint pull requests भी लौटाता है (उनमें pull_request key होती है)। अगर सिर्फ़ असली issues चाहिए, तो उस key वाले items छोड़ दीजिए।
GitHub 的 issues 端点也会返回拉取请求(它们带有 pull_request 键)。如果只想要真正的 issue,就跳过带有该键的条目。
📚 Simulator: Pagination Stepper📚 Simulator: Pagination Stepper📚 模拟器:分页单步演示
237 items wait on the server. Pick a style and a page size, then step through the real requests. Watch how each style knows when to stop.
Server पर 237 items इंतज़ार कर रहे हैं। एक तरीका और page size चुनिए, फिर असली requests step-by-step देखिए। देखिए हर तरीका कैसे जानता है कि कब रुकना है।
服务器上有 237 条数据。选择一种方式和每页大小,然后逐步查看真实请求。观察每种方式如何判断何时停止。
Common Pitfalls (and Fixes) 🕳️आम ग़लतियाँ (और सुधार) 🕳️常见陷阱(及修正)🕳️
| Mistakeग़लती错误 | What happensक्या होता है会发生什么 | Fixसुधार修正 |
|---|---|---|
No timeouttimeout नहीं没有 timeout | Program can hang for ever on a silent serverचुप server पर program हमेशा अटक सकता है遇到不回应的服务器,程序可能永远挂起 | Always pass timeout=5 (or what your service needs)हमेशा timeout=5 (या जो service को चाहिए)始终传入 timeout=5(或服务所需的值) |
Ignoring status_codestatus_code अनदेखा忽略 status_code | An HTML error page is parsed as JSON → JSONDecodeErrorHTML error page को JSON मानकर parse → JSONDecodeError把 HTML 错误页当成 JSON 解析 → JSONDecodeError | Check r.ok or call r.raise_for_status()r.ok जाँचिए या r.raise_for_status() बुलाइए检查 r.ok 或调用 r.raise_for_status() |
| Hard-coded API keyCode में लिखी API key硬编码 API 密钥 | Leaked into git history, credentials stolenGit history में leak, credentials चोरी泄露到 git 历史,凭据被盗 | Environment variables + .gitignoreEnvironment variables + .gitignore环境变量 + .gitignore |
requests.get() in a big loopबड़े loop में requests.get()在大循环里直接 requests.get() | A new connection for every request — slowerहर request के लिए नया connection — धीमा每个请求都新建连接 — 更慢 | Use requests.Session()requests.Session() लीजिए使用 requests.Session() |
| String-built URLsString जोड़कर बने URLs字符串拼接的 URL | Spaces / & / + / # break the querySpaces / & / + / # query तोड़ देते हैं空格 / & / + / # 会破坏查询 | Pass params={...}params={...} दीजिए传入 params={...} |
data= instead of json=json= की जगह data=用 data= 代替 json= | Sent as a form, not JSON — the API sees no JSONForm की तरह गया, JSON नहीं — API को JSON नहीं मिलता以表单发送而非 JSON — API 收不到 JSON | Use json={...} for JSON APIsJSON APIs के लिए json={...}JSON API 用 json={...} |
Optional field read with [key]Optional field [key] से पढ़ना用 [key] 读取可选字段 | KeyError if missing; None if nullग़ायब हो तो KeyError; null हो तो None缺失时 KeyError;为 null 时得到 None | Use data.get(key) or defaultdata.get(key) or default लीजिए使用 data.get(key) or default |
| Retrying all 4xx errors, or POSTहर 4xx error, या POST retry करना重试所有 4xx 错误或 POST | Wasted calls, possible bans, duplicate ordersबेकार calls, ban का ख़तरा, दोहरे orders浪费调用、可能被封禁、重复下单 | Retry 429, 5xx and network errors on idempotent methodsIdempotent methods पर 429, 5xx और network errors retry只对幂等方法重试 429、5xx 和网络错误 |
| Very old XML parser setupबहुत पुराना XML parser非常旧的 XML 解析器 | Old expat (< 2.4.1) can be hit by "billion laughs"पुराना expat (< 2.4.1) "billion laughs" का शिकार हो सकता है旧版 expat(< 2.4.1)可能遭受"billion laughs"攻击 | Keep Python up to date; defusedxml for hostile XMLPython नया रखें; शत्रुतापूर्ण XML के लिए defusedxml保持 Python 更新;恶意 XML 用 defusedxml |
The original said a missing optional field gives "KeyError on … None" — those are two different cases (see Section 06). And it said parsing untrusted XML with the standard library opens an "XXE" hole; as explained in Section 07, modern ElementTree does not expand external entities. We also added the very common data= vs json= mix-up, which the Bug Doctor below shows.
मूल में कहा गया कि ग़ायब optional field से "KeyError on … None" होता है — ये दो अलग मामले हैं (Section 06 देखें)। और कहा गया कि standard library से अविश्वसनीय XML parse करने पर "XXE" छेद खुलता है; Section 07 में समझाए अनुसार आधुनिक ElementTree external entities नहीं फैलाता। हमने बहुत आम data= बनाम json= गड़बड़ी भी जोड़ी, जिसे नीचे Bug Doctor दिखाता है।
原文说缺失的可选字段会导致"KeyError on … None"— 这其实是两种不同的情况(见第 06 节)。原文还说用标准库解析不可信 XML 会产生"XXE"漏洞;如第 07 节所述,现代 ElementTree 不会展开外部实体。我们还补充了非常常见的 data= 与 json= 混用问题,下面的 Bug 医生会演示。
Real-World Example — Complete GitHub Repo Analyzer 🐙असली उदाहरण — पूरा GitHub Repo Analyzer 🐙实战示例 — 完整的 GitHub 仓库分析器 🐙
All the pieces together: a session, auth, timeouts, retries, error handling, JSON parsing and a typed dataclass — in one small, solid client. We added a fourth repo that does not exist, to show the error path.
सारे टुकड़े एक साथ: session, auth, timeouts, retries, error handling, JSON parsing और typed dataclass — एक छोटे, मज़बूत client में। Error वाला रास्ता दिखाने के लिए हमने चौथा repo जोड़ा जो मौजूद नहीं है।
所有部件合在一起:Session、认证、超时、重试、错误处理、JSON 解析和带类型的 dataclass — 组成一个小巧而可靠的客户端。我们加了第四个不存在的仓库,用来展示出错路径。
import os
from dataclasses import dataclass
import requests
from requests.adapters import HTTPAdapter
from urllib3.util.retry import Retry
BASE = "https://api.github.com"
@dataclass(frozen=True)
class RepoStats:
full_name: str
stars: int
forks: int
open_issues: int
language: str | None
top_contributors: list[str]
def build_session(token: str) -> requests.Session:
retry = Retry(total=5, backoff_factor=1.0,
status_forcelist=[429, 500, 502, 503, 504])
s = requests.Session()
s.mount("https://", HTTPAdapter(max_retries=retry))
s.headers.update({
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"User-Agent": "repo-analyzer/1.0",
})
return s
def analyze_repo(session: requests.Session, repo: str) -> RepoStats | None:
try:
r = session.get(f"{BASE}/repos/{repo}", timeout=10)
r.raise_for_status()
info = r.json()
c = session.get(f"{BASE}/repos/{repo}/contributors",
params={"per_page": 5}, timeout=10)
c.raise_for_status()
top = [u["login"] for u in c.json()]
return RepoStats(
full_name = info["full_name"],
stars = info["stargazers_count"],
forks = info["forks_count"],
open_issues = info["open_issues_count"],
language = info.get("language"),
top_contributors = top,
)
except requests.HTTPError as e:
print(f"[{repo}] HTTP error: {e}")
except (requests.Timeout, requests.ConnectionError):
print(f"[{repo}] network problem")
return None
# -- Run it ------------------------------------------------
REPOS = ["python/cpython", "pallets/flask", "psf/requests", "no/such-repo"]
with build_session(os.environ["GITHUB_TOKEN"]) as s:
for repo in REPOS:
stats = analyze_repo(s, repo)
if stats:
print(f"\n{stats.full_name}")
print(f" stars {stats.stars:,} forks {stats.forks:,} issues {stats.open_issues:,}")
print(f" language: {stats.language}")
print(f" top: {', '.join(stats.top_contributors)}")
The original output printed fixed star counts and contributor lists as if they were facts; they were a snapshot and are out of date. Our numbers come from the stand-in server. Also note: GitHub's open_issues_count counts open issues plus open pull requests, so it is not just "issues".
मूल output ने तय star counts और contributor सूचियाँ ऐसे print कीं जैसे वे तथ्य हों; वे snapshot थीं और पुरानी हो चुकी हैं। हमारे numbers stand-in server से हैं। ध्यान दें: GitHub का open_issues_count खुले issues और खुले pull requests दोनों गिनता है, इसलिए यह सिर्फ़ "issues" नहीं है।
原输出把固定的星标数和贡献者列表当作事实打印出来;它们只是快照,已经过时。我们的数字来自替身服务器。另请注意:GitHub 的 open_issues_count 统计的是未关闭的 issue 加上未合并的拉取请求,所以不仅仅是"issue"。
One session with reused connections. Token from an environment variable — no leaks. Automatic retries on 429 and 5xx with backoff, never for POST. A timeout on every call. Clear error handling — the missing repo did not crash the program. Data lands in a typed dataclass. This is the shape of most real API clients you will write.
दोबारा इस्तेमाल होने वाले connections वाला एक session। Environment variable से token — कोई leak नहीं। 429 और 5xx पर backoff के साथ अपने-आप retries, POST के लिए कभी नहीं। हर call पर timeout। साफ़ error handling — ग़ायब repo ने program crash नहीं किया। Data एक typed dataclass में आता है। आप जो असली API clients लिखेंगे, उनमें से ज़्यादातर का यही आकार होगा।
一个复用连接的 Session。令牌来自环境变量 — 不会泄露。对 429 和 5xx 自动退避重试,且绝不重试 POST。每个调用都有超时。清晰的错误处理 — 不存在的仓库没有让程序崩溃。数据落入带类型的 dataclass。你以后写的大多数真实 API 客户端都是这个样子。
Bug Doctor, Golden Rules and Quiz 🏆Bug Doctor, सुनहरे नियम और Quiz 🏆Bug 医生、黄金法则与小测验 🏆
🩺 Simulator: Bug Doctor🩺 Simulator: Bug Doctor🩺 模拟器:Bug 医生
Six real API mistakes. Buggy code on the left, fixed code on the right, changed lines marked. Every output is from a real run (the hanging program was stopped after 4 seconds).
छह असली API ग़लतियाँ। बाईं ओर buggy code, दाईं ओर ठीक किया हुआ, बदली lines चिह्नित। हर output असली run से है (अटका program 4 सेकंड बाद रोका गया)।
六个真实的 API 错误。左边是有 bug 的代码,右边是修正后的代码,改动的行已标出。每个输出都来自真实运行(挂起的程序在 4 秒后被停止)。
timeout=. Without one, a silent server can hang your program for ever. Start with 5–10 seconds for reads.timeout= लगाइए। इसके बिना चुप server आपका program हमेशा के लिए अटका सकता है। पढ़ने के लिए 5–10 सेकंड से शुरू कीजिए।timeout=。没有它,一个不回应的服务器可能让程序永远挂起。读取操作可从 5–10 秒开始。r.raise_for_status() or by looking at r.status_code. Assuming success leads to confusing errors deep in your parser.r.raise_for_status() से या r.status_code देखकर। सफलता मान लेना parser में गहरे उलझाने वाले errors देता है।r.raise_for_status() 或查看 r.status_code。想当然地认为成功,会在解析器深处引发难懂的错误。requests.Session() when you make more than one call to the same host. It reuses connections and keeps shared headers in one place.requests.Session() लीजिए। यह connections दोबारा इस्तेमाल करता है और साझा headers एक जगह रखता है।requests.Session()。它复用连接,并把公共请求头集中在一处。params={} for query strings and json={} for JSON bodies, and let requests do the encoding.params={} और JSON bodies के लिए json={} दीजिए, encoding requests को करने दीजिए।params={},JSON 请求体用 json={},让 requests 负责编码。API_USER — never in code, never in git. Replace any key that leaks at once.API_USER जैसे साफ़ नामों के साथ — code में कभी नहीं, git में कभी नहीं। जो key leak हो, उसे तुरंत बदलिए।API_USER 这样清晰的名字 — 绝不写进代码,绝不提交到 git。任何泄露的密钥要立即更换。Retry-After. Do not retry other 4xx errors, and do not auto-retry POST.Retry-After मानते हुए। बाक़ी 4xx retry न करें, और POST को अपने-आप retry न करें।Retry-After。不要重试其他 4xx,也不要自动重试 POST。defusedxml as an extra guard for XML from untrusted sources.defusedxml को अतिरिक्त बचाव की तरह लीजिए।defusedxml 作为额外防护。Rule 3 promised "~130 ms" saved per request — that number depends on distance and network, so we removed it. Rule 7 said to always replace the standard xml module because "XXE … attacks are real"; with current Python the standard ElementTree is not open to XXE (Section 07). The rule now says what actually matters.
नियम 3 ने हर request पर "~130 ms" बचत का वादा किया — यह number दूरी और network पर निर्भर है, इसलिए हमने हटाया। नियम 7 ने कहा कि standard xml module को हमेशा बदलिए क्योंकि "XXE … attacks असली हैं"; मौजूदा Python में standard ElementTree XXE के लिए खुला नहीं है (Section 07)। नियम अब वही कहता है जो सच में मायने रखता है।
规则 3 承诺每个请求节省"约 130 毫秒"— 这个数字取决于距离和网络,所以我们删去了。规则 7 说要始终替换标准 xml 模块,因为"XXE … 攻击是真实的";在当前的 Python 中,标准 ElementTree 并不受 XXE 影响(第 07 节)。现在的规则说的是真正重要的事。
Quick Quiz — 8 Questionsछोटा Quiz — 8 सवाल小测验 — 8 道题
An API call is a URL, a method and some data; the answer is a status code and some data. Make it reliable with five habits: a timeout on every call, a status check on every response, params=/json= instead of hand-built strings, secrets in environment variables, and retries only for temporary errors on idempotent requests. Wrap it all in a Session and a typed model.
API call एक URL, एक method और कुछ data है; जवाब एक status code और कुछ data है। पाँच आदतों से इसे भरोसेमंद बनाइए: हर call पर timeout, हर response पर status जाँच, हाथ से बनी strings की जगह params=/json=, secrets environment variables में, और retries सिर्फ़ idempotent requests की अस्थायी errors पर। सब कुछ एक Session और typed model में लपेटिए।
一次 API 调用就是 URL、方法和一些数据;回答是状态码和一些数据。用五个习惯让它可靠:每个调用都设 timeout,每个响应都检查状态码,用 params=/json= 代替手工拼接字符串,机密放在环境变量里,只对幂等请求的临时错误进行重试。最后用 Session 和带类型的模型把一切包起来。