$ pip install aiohttpPure-Python core; pulls inyarl,multidict,frozenlist.$ pip install aiohttp[speedups]Adds C accelerators:aiodns(fast DNS),Brotli, faster parsing.import aiohttp, asyncio★You always needasynciotoo — aiohttp is async-only.from aiohttp import webThe server half. Import it only when you're building a server.aiohttp.__version__Check the installed version, e.g.'3.14.3'.
async def main(): …★All aiohttp calls live inside a coroutine.asyncio.run(main())★The one sync entry point — starts the event loop, runs to completion.r = await …★awaitanything that does I/O: the body readers,ws.receive(), etc.await asyncio.gather(*tasks)Fire many requests concurrently — the whole point of async.# there is no requests.get() equivalentnoteEvery call must be awaited; you can't use aiohttp from plain sync code.
async with session.get(url) as resp:★The idiomatic call —async withfrees the connection when done.async with session.post(url, json=…) as r:★Create / submit. Body goes injson=/data=.session.put · .patch · .deleteSame shape for every verb.session.head · .optionsHeaders-only / capability probes.session.request("GET", url)Generic form — verb as a string.aiohttp.request("GET", url)avoidOne-off without a session — discouraged; you lose pooling.
session.get(url, params={"q": "x", "n": 2})★Dict →?q=x&n=2, encoded for you.params=[("k", "a"), ("k", "b")]List of tuples for repeated keys →?k=a&k=b.params=MultiDict(…)aiohttp usesmultidict— multiple values per key are first-class.resp.urlThe finalyarl.URLobject with params applied — great for debugging.resp.url is a yarl.URL, not strgotchaWrap instr(resp.url)when you need a plain string.
post(url, json={"a": 1})★Serializes to JSON & setsContent-Type: application/json.post(url, data={"a": 1})★Dict → form-encoded (application/x-www-form-urlencoded).post(url, data=b"raw bytes")bytes / str body sent as-is; no Content-Type added.post(url, data=file_obj)A file-like or async iterable streams the body out.data=aiohttp.FormData(…)Multipart forms & file uploads — see card 16.# custom serializer at session leveltipClientSession(json_serialize=orjson.dumps)to speed up big payloads.
get(url, headers={"User-Agent": "app/1.0"})★Per-request headers as a plain dict.headers={"Authorization": "Bearer <tok>"}★API tokens go in a header (see card 14).ClientSession(headers={…})Default headers merged into every request from this session.ClientSession(skip_auto_headers=["User-Agent"])Stop aiohttp adding its default header for those names.resp.headers["Content-Type"]Response headers are a case-insensitiveCIMultiDict.
resp.status★int attribute, no parens —200,404…resp.okTruefor < 400. (Yes, aiohttp has.ok.)resp.headers · resp.cookiesAvailable immediately, before you read the body.resp.content_type · resp.charsetParsed from the Content-Type header.resp.url · resp.real_urlyarl.URL;real_urlkeeps the un-normalized form.resp.historyTuple of responses from any redirects followed.resp.content_length · resp.versionLength header & HTTP version (aiohttp client is HTTP/1.1).
data = await resp.json()★Parse JSON → dict / list. Must be awaited.txt = await resp.text()★Decodedstr(charset auto-detected).raw = await resp.read()Full body asbytes.await resp.json() → ContentTypeErrorgotchaRaised if Content-Type isn't JSON. Skip withresp.json(content_type=None).# read the body INSIDE the async withkeyAfter the block exits the connection is released → reading fails.resp.contentAStreamReaderfor chunked reads — card 15.
# a 4xx/5xx is still a valid respkeyYou must check the status yourself, or opt into raising.resp.raise_for_status()★RaisesClientResponseErroron 4xx/5xx; no-op otherwise.if resp.ok: …Quick success test (status < 400).ClientSession(raise_for_status=True)Auto-raise on every request from this session.session.get(url, raise_for_status=True)Or opt in per request — unique to aiohttp.
async with aiohttp.ClientSession() as s:★The right lifecycle — closes the pool cleanly on exit.# create ONE, reuse everywherekeyA session per request destroys pooling & leaks connectors.ClientSession(base_url="https://api.x.com")★Then call relative paths:s.get("/users").ClientSession(headers=…, auth=…, timeout=…)Defaults applied to every request.await session.close()If not usingasync with, close it manually.# "Unclosed client session" warninggotchaMeans you forgot to close — always use the context manager.
conn = aiohttp.TCPConnector(limit=100)★Total simultaneous connections cap (default 100).TCPConnector(limit_per_host=10)Per-host cap — default0(unlimited).TCPConnector(ttl_dns_cache=300)Cache DNS results (default 10s);use_dns_cache=True.ClientSession(connector=conn)★Attach the connector to the session.TCPConnector(ssl=ssl_ctx)Custom TLS: anssl.SSLContext,Falseto disable, or aFingerprint.aiohttp.UnixConnector(path="/tmp/s.sock")Talk to a service over a Unix domain socket.
# default total timeout = 300s (5 min)keyNot infinite, but long — almost always set your own.t = aiohttp.ClientTimeout(total=10)★Cap the whole operation (connect + send + read).ClientTimeout(sock_connect=3, sock_read=10)Fine-grained: TCP-connect vs between-reads limits.ClientSession(timeout=t)Session-wide default for every request.session.get(url, timeout=t)Override per request.except asyncio.TimeoutError:gotchaTotal-timeout raises this — not aClientError. Catch both.
except aiohttp.ClientError:★Root of every aiohttp client failure — catch broad.except aiohttp.ClientConnectorError:Couldn't reach the host (DNS / refused / network).except aiohttp.ClientResponseError:Raised byraise_for_status()on 4xx/5xx.except aiohttp.ContentTypeError:json()on a non-JSON body (subclass of ClientResponseError).except aiohttp.ServerTimeoutError:Socket connect / read timed out (also aTimeoutError).except asyncio.TimeoutError:keyThe total timeout — sits outside the ClientError tree.
auth=aiohttp.BasicAuth("user", "pass")★HTTP Basic auth, per request or on the session.headers={"Authorization": "Bearer <tok>"}★Bearer / API tokens go in a header, notauth=.ClientSession(auth=BasicAuth(…))Send credentials on every request.BasicAuth.decode(header_value)Parse an incoming Authorization header (handy server-side).# auth is dropped on cross-host redirectsnoteSensitive headers are stripped when a redirect changes host.
async for chunk in resp.content.iter_chunked(8192):★Stream the body in fixed-size byte chunks — the download idiom.chunk = await resp.content.read(1024)Read up to N bytes from theStreamReader.async for line in resp.content:Iterate a streaming body line by line.resp.content.iter_any()Yield whatever arrives, as it arrives (no fixed size).with open("f.bin","wb") as f: f.write(chunk)Write each chunk to disk — constant memory.# don't call read() then json()gotchaThe stream is consumed once; pick one way to read the body.
fd = aiohttp.FormData()★Build a multipart / form body.fd.add_field("name", "Milo")Add a plain text field.fd.add_field("file", f, filename="a.csv",Attach a file object with a name & content type…content_type="text/csv")…thenpost(url, data=fd).post(url, data={"file": open("a.png","rb")})Shortcut: a dict with a file object also uploads multipart.
async with session.ws_connect(url) as ws:★Open a client WebSocket — no extra library.await ws.send_str("hi") · ws.send_json(obj)Alsosend_bytes. Many writer tasks are fine.async for msg in ws:★Read the incoming stream — use one reader task only.if msg.type == aiohttp.WSMsgType.TEXT:Dispatch on type;msg.dataholds the payload.msg = await ws.receive()Or pull one message explicitly.await ws.close()Closes the handshake (automatic withasync with).
# a session persists cookies automatically★Log in once, later requests carry the cookie.resp.cookies["session"]Read cookies the server just set.ClientSession(cookies={"k": "v"})Seed the jar with starting cookies.session.cookie_jar.update_cookies(…)Add cookies to the live jar.ClientSession(cookie_jar=aiohttp.DummyCookieJar())Disable cookie storage entirely (e.g. for a crawler).
from aiohttp import webThe server framework — unique among Python HTTP clients.async def handler(request): …A handler is a coroutine taking aRequest, returning aResponse.return web.json_response({"ok": True})★Orweb.Response(text=…)/web.Response(body=…).app = web.Application()The app holds routes, middlewares & shared state.app.add_routes([web.get("/", handler)])★Register routes; or use aweb.RouteTableDef()decorator.request.match_info["id"] · request.queryPath params & query string;await request.json()for the body.web.run_app(app, port=8080)★Start serving — builds & runs the event loop for you.