The web is the most common place a Python program reaches out to today. Calling a payment service, fetching a price list, sending an order to a partner system, serving a product catalog to a mobile app, they all happen over HTTP. This lesson covers what a Python developer needs to know before writing web code: the request and response model, what a URL is made of, the common methods and status codes, the role of headers and bodies, how cookies and sessions track users, and what HTTPS adds on top.
The web is built on a simple split. One program asks for something, another answers. The asker is the client. The answerer is the server.
A web browser opening https://shop.example.com/products/101 is a client. A Python script calling a price service is also a client. The thing on the other end, the program that holds the data and decides what to return, is the server. From Python's side, you can be either. A script that reads a product catalog from an external API is acting as a client. A Flask app that serves orders to a mobile app is acting as a server. Most real applications do both.
The diagram shows one exchange. The client sends a request, the network carries it, the server returns a response, the network carries that back. Every page load, every API call, every form submission is one (or more) round trips of this kind.
There's one important rule baked into this model: the server only speaks when spoken to. The client starts every conversation. The server has no way to randomly push something to the client out of the blue. (Modern extensions like WebSockets and Server-Sent Events change this, but they're built on top of an initial HTTP handshake.)
The language client and server speak is HTTP (HyperText Transfer Protocol). It's a text-based protocol where the client writes a request, the server writes a response, and both follow a fixed format.
A raw HTTP request to fetch a product looks like this on the wire:
The first line says which method (GET), which path (/products/101), and which version of HTTP. The next few lines are headers, each a Name: Value pair giving the server context about who's asking and what they want back. There's no body here because GET requests usually don't carry one.
The response that comes back has a similar shape:
The first line is the status line: HTTP version, a three-digit status code, and a short message. Then headers describe the response. A blank line separates the headers from the body, which here is the JSON the server wants to send back.
You rarely see this raw form in Python. Libraries like requests build the request text for you and parse the response into Python objects. Understanding the shape helps when something goes wrong, when a header is missing or a status code is unexpected.
Every HTTP request targets a URL (Uniform Resource Locator). A URL is more than a web address, it's a structured string with several parts, each playing a specific role.
Take this URL:
Break it into pieces:
| Part | Value | What It Says |
|---|---|---|
| Scheme | https | Which protocol to use. http or https for the web. |
| Host | shop.example.com | The domain name to resolve to an IP address. |
| Port | 443 | Which TCP port on the server to talk to. Defaults: 80 for http, 443 for https. |
| Path | /products/101 | Which resource on the server. The server decides what paths mean. |
| Query string | currency=usd&promo=summer | Extra key=value parameters separated by &, after a ?. |
| Fragment | reviews | A client-side anchor. Never sent to the server. |
The scheme and host together tell the client where to send the request. The path tells the server which resource is being asked for. The query string carries optional parameters, often used for filtering, sorting, or paginating (?page=2&sort=price). The fragment is for the browser only, used to scroll to an anchor on the page.
Python's standard library has helpers for picking URLs apart:
urlparse splits a URL into its named pieces. parse_qs parses the query string into a dictionary. The values are lists because the same key can repeat (?tag=sale&tag=clearance).
The method tells the server what kind of operation the client is asking for. There are several methods, but five cover almost everything in common use.
| Method | Purpose | Body? | Safe? | Idempotent? |
|---|---|---|---|---|
GET | Fetch a resource | Usually no | Yes | Yes |
POST | Create a new resource, or trigger an action | Yes | No | No |
PUT | Replace a resource with the body's contents | Yes | No | Yes |
PATCH | Update part of a resource | Yes | No | Not required |
DELETE | Remove a resource | Usually no | No | Yes |
Two terms in the table need definition. Safe means the method shouldn't change anything on the server. GET should never delete or create data. Idempotent means running the same request twice has the same effect as running it once. DELETE /products/101 twice still leaves no product 101. POST /orders twice creates two orders, so it's not idempotent.
The methods map naturally onto a product catalog:
| Action | Method + Path |
|---|---|
| Get product 101 | GET /products/101 |
| List all products | GET /products |
| Create a new product | POST /products |
| Replace product 101 entirely | PUT /products/101 |
| Update product 101's price only | PATCH /products/101 |
| Delete product 101 | DELETE /products/101 |
This is the REST style, which most modern HTTP APIs follow. It's a convention, not a rule. Servers can technically use any method for any operation, but matching methods to verbs makes URLs predictable and lets clients reason about them.
The three-digit number on the response status line is the status code. The first digit groups codes into families, each with a meaning that holds without knowing every individual code.
| Family | Range | Meaning | Common Examples |
|---|---|---|---|
| 1xx | 100-199 | Informational | 100 Continue (rare) |
| 2xx | 200-299 | Success | 200 OK, 201 Created, 204 No Content |
| 3xx | 300-399 | Redirection | 301 Moved Permanently, 302 Found, 304 Not Modified |
| 4xx | 400-499 | Client error | 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 429 Too Many Requests |
| 5xx | 500-599 | Server error | 500 Internal Server Error, 502 Bad Gateway, 503 Service Unavailable |
Two rules of thumb cover most cases. 4xx means "your request was bad." Wrong URL, missing auth, malformed JSON, exceeded rate limit. The client should fix the request before retrying. 5xx means "the server messed up." A retry might work; the problem isn't with the client's request.
A few specific codes show up often:
200 OK: standard success for GET.201 Created: success for POST when a new resource was made. The response often includes a Location header pointing at the new resource.204 No Content: success with no body, common after DELETE.301 / 302: the resource lives at a different URL. The client should follow the Location header. 301 is permanent, 302 is temporary.400 Bad Request: the server couldn't parse what you sent.401 Unauthorized: missing or invalid auth credentials.403 Forbidden: you're authenticated but not allowed.404 Not Found: the path doesn't match any resource.429 Too Many Requests: a rate limit has been hit. Often paired with a Retry-After header.500 Internal Server Error: an unhandled error on the server, no detail.Treating every non-200 as the same error is a common source of broken integrations. A retry on 503 makes sense; a retry on 400 hits the same wall twice. Pay attention to the family, and to specific codes that change behaviour.
Headers are the metadata of an HTTP message. Each one is a Name: Value pair sitting between the start line and the body. There are dozens defined in the spec, but a small set carries most of the weight.
| Header | Direction | What It Does |
|---|---|---|
Host | Request | Which domain the request is for. Required in HTTP/1.1. |
Content-Type | Both | Format of the body (application/json, text/html, application/x-www-form-urlencoded). |
Content-Length | Both | Size of the body in bytes. |
Accept | Request | Formats the client is willing to receive. Accept: application/json says "send me JSON, please." |
Authorization | Request | Credentials. Bearer <token> is the most common form today. |
User-Agent | Request | Identifies the client software. Useful for analytics, sometimes used for blocking. |
Set-Cookie | Response | Asks the client to store a cookie and send it on future requests. |
Cookie | Request | Cookies the client has stored for this domain. |
Cache-Control | Both | Caching rules (no-cache, max-age=3600). |
Location | Response | Where to find the resource (used with redirects and 201 Created). |
A request to fetch a product as JSON, with an auth token, looks like this:
The server reads Accept: application/json and knows to return JSON instead of HTML. The Authorization header carries the token that proves who's asking. User-Agent identifies the client, which servers sometimes log for debugging or restrict for abuse control.
Header names are case-insensitive (Content-Type and content-type are the same header), but values are case-sensitive. Most Python libraries normalize names for you when you read them back.
The body is the actual payload of a message: the JSON for an API call, the HTML for a page, the bytes of an image, the form fields of a submission. Two things govern what the body looks like: the method (does it even have one?) and the Content-Type header (what format is it in?).
For modern APIs, the body is almost always JSON. A request to create an order might look like:
The Content-Type: application/json tells the server the body is JSON. The server parses it, processes the order, and replies with another JSON body containing the new order's id and status.
Other common content types:
| Content-Type | Used For |
|---|---|
application/json | Modern API payloads. The default for new services. |
application/x-www-form-urlencoded | Traditional HTML form submissions (name=ria&email=ria%40example.com). |
multipart/form-data | Form submissions that include file uploads. |
text/html | HTML pages returned to browsers. |
text/plain | Plain text. |
application/octet-stream | Raw binary, like a downloaded file with no specific type. |
The next two lessons cover how Python builds and reads each of these. For now, the key idea is that the body alone is bytes. The Content-Type header is what tells the receiving side how to make sense of them.
HTTP is stateless. Every request stands on its own. The server doesn't remember what the client asked for last time. A call to GET /products/101 followed by GET /products/102 produces two requests that are completely separate from the server's point of view. Nothing carries over between them.
That sounds limiting, especially for an e-commerce site. How does the server know which cart belongs to whom, or which user is logged in?
The client carries any state it needs in the request itself. Each request that needs context attaches that context, usually through cookies or an auth header. The server reads it on every request, looks up the matching user or session, and decides what to do. The server itself stays stateless; the client (or a session store the server checks) holds the state.
This design has practical consequences. Servers can be added and removed easily because no single server has to "own" a user. Requests can be retried because nothing depends on a previous request. Load balancers can route any request to any server. The trade-off is that the client has to send the right context every time, and the server has to look it up every time, both of which cost a little work.
A cookie is a small piece of data the server asks the browser (or any HTTP client) to store and send back on future requests to the same domain. Cookies are the standard way to identify a returning user.
The flow:
Set-Cookie header with a name and value.Cookie header.The diagram traces a login followed by two protected calls. The first response sets a session cookie. From then on, every request carries it, and the server uses it to look up the client's identity and cart contents.
A session is the server-side concept that pairs with the cookie. The cookie stores an opaque ID (abc123); the server stores the actual session data (user id, cart contents, login state) in a database, cache, or signed token. When a request arrives with Cookie: session=abc123, the server looks up abc123 in its session store and finds out which user it belongs to.
Cookies have a handful of attributes that affect how the client treats them. Expires sets when the cookie should be deleted. Secure means "only send over HTTPS." HttpOnly means JavaScript on the page can't read it (a defence against script-based attacks). SameSite controls whether the cookie is sent on requests coming from other sites. Session cookies typically need Secure, HttpOnly, and SameSite=Lax or Strict.
For programmatic clients (Python scripts calling APIs), the more common pattern today is bearer tokens carried in the Authorization header, not cookies. This section covers both.
Plain HTTP sends everything as readable text. Anyone between the client and the server, on the same Wi-Fi, at an ISP, on a backbone router, can read it, including passwords, cookies, and credit card numbers. That's unacceptable for anything sensitive, which today means almost everything.
HTTPS is HTTP wrapped in a layer called TLS (Transport Layer Security). TLS does three things:
shop.example.com and not an imposter, by presenting a certificate signed by a trusted authority.From a Python developer's perspective, HTTPS is mostly invisible. Use https:// in the URL instead of http://, and the library handles the rest. The only place it shows up is when something goes wrong with certificates: an expired certificate, a self-signed certificate on a test server, a misconfigured machine without the trusted root certificates installed. Most modern libraries refuse to connect in those cases, which is the safe default.
There's a port distinction too. HTTP defaults to port 80, HTTPS to port 443. If the URL doesn't specify a port, the client picks the matching default based on the scheme.
TLS adds a handshake at the start of each connection. The handshake costs a few extra round trips before the first request goes through. Once the connection is established, subsequent requests on the same connection don't pay it again, which is why connection reuse (via sessions, covered in the requests lesson) matters for performance.
Don't use plain http:// for anything beyond local development. Modern APIs all require HTTPS, and the security gain is large for almost no cost in code.
The format of an API today is JSON for almost everything. Older systems used XML, and some legacy enterprise systems still do, but new APIs default to JSON.
JSON's appeal is straightforward: it's human-readable, language-neutral, and maps cleanly onto Python's built-in types. A product fetched from a service comes back as a dict that can be indexed and passed around without any conversion step.
A typical request and response pair:
Both sides carry JSON. The request asks for an order to be created; the response describes the order that was made. In Python, both bodies are dict once they're parsed, and the format itself takes almost no work to handle. That's why JSON won.
Some APIs still return XML, some return protocol buffers, some stream binary frames. For most Python web code today, JSON is the default.
A Python program touches the web from either side, sometimes both.
As a client, Python reaches out to other services. Fetching weather, calling a payment processor, syncing inventory with a supplier, scraping a public page, posting a log line to a metrics service. The standard library provides urllib.request for this; the third-party requests library is what most code uses.
As a server, Python answers requests from other clients. Hosting an API for a mobile app to talk to, serving a website, exposing a webhook for a partner system to call. Python has several web frameworks: Flask for small services and microservices, FastAPI for typed async APIs, Django for full-stack applications, and others.
The diagram shows both directions side by side. On the left, Python is the client calling out. On the right, Python is the server being called. Many real applications do both: a Flask app that handles browser requests on one side and calls a payment service on the other side is two of these diagrams stitched together.
10 quizzes