URL parser: how a URL breaks into parts

Take a real address and pull it apart. In https://shop.example.com:8443/catalog/shoes?color=red&size=41&size=42&page=2#reviews, the scheme is https, the host is shop.example.com, the port is 8443, the path is /catalog/shoes, the query string holds the three distinct parameters after the ?, and the fragment is reviews. Every language's URL parser hands you exactly these pieces, and once you can name them, debugging a bad link or an API callback becomes a lookup instead of a guess. This page walks through each part, shows how to parse them in Python and JavaScript, and finishes with the traps that cause most parsing bugs.

The parts, one by one

PartIn the exampleWhat it does
schemehttpsHow to talk to the server: the protocol.
user infouser:pw@ (optional)Credentials before the host. Rare today; never put real passwords in URLs.
hostshop.example.comThe machine, by domain name or IP.
port8443 (optional)The door on that machine. Hidden when it is the default: 80 for http, 443 for https.
path/catalog/shoesWhat you want on that machine. Servers route on it.
querycolor=red&size=41...Extra parameters after the ?: key=value pairs joined by &.
fragment#reviewsA position inside the page. The browser handles it; the server never sees it.

Two details worth memorizing. First, the query starts at the first ? and runs to the first #; the fragment runs to the end of the string. Second, the port belongs to the authority section together with the host, but parsers usually hand you them separately, as they should: the port is transport plumbing, the host is identity.

Query strings: the fiddly part

Parameters look simple and behave subtly. Three conventions do most of the work:

To see all of this on any real URL, paste it into our URL Parameter Inspector: every query parameter appears decoded, with the raw original beside it and a rebuilt URL that updates as you edit. It runs entirely in your browser.

Parse a URL in Python

Python ships a parser in the standard library, urllib.parse. No install, no dependencies:

from urllib.parse import urlparse, parse_qs

url = "https://shop.example.com:8443/catalog/shoes?color=red&size=41&size=42&page=2#reviews"
parts = urlparse(url)

parts.scheme    # 'https'
parts.netloc    # 'shop.example.com:8443'  (user info stays here if present)
parts.hostname  # 'shop.example.com'      (lowercased, port removed)
parts.port      # 8443                    (int, or None)
parts.path      # '/catalog/shoes'
parts.query     # 'color=red&size=41&size=42&page=2'  (still encoded)
parts.fragment  # 'reviews'               (no leading #)

The query string turns into usable data with parse_qs:

params = parse_qs(parts.query)
# {'color': ['red'], 'size': ['41', '42'], 'page': ['2']}

params['size']   # ['41', '42']   repeated keys become lists
params['missing']  # KeyError: check with .get() instead

Three behaviors bite everyone once. parse_qs drops blank values: parse_qs("debug=&page=2") returns {'page': ['2']}, and only keep_blank_values=True keeps debug. Repeated keys always fold into a list, so take params['size'][0] when you want one. And urlparse does not decode anything: parts.query is still percent-encoded, parse_qs is the step that decodes. The round trip back to text is urlencode, which encodes spaces as + by default:

from urllib.parse import urlencode
urlencode({"q": "red sneakers"})   # 'q=red+sneakers'

Parse a URL in JavaScript

JavaScript's URL class does the same job, built into every browser and Node:

const u = new URL("https://shop.example.com:8443/catalog/shoes?color=red&size=41&size=42&page=2#reviews");

u.protocol   // 'https:'
u.hostname   // 'shop.example.com'
u.port       // '8443'   (empty string when default)
u.pathname   // '/catalog/shoes'
u.search     // '?color=red&size=41&size=42&page=2'
u.hash       // '#reviews'  (includes the #)

The query string gets its own interface, searchParams, which decodes for you:

u.searchParams.get("color")        // 'red'
u.searchParams.getAll("size")      // ['41', '42']
new URLSearchParams({q: "red sneakers"}).toString()  // 'q=red+sneakers'

Two differences from Python to keep straight. new URL() refuses relative input: new URL("example.com/catalog") throws, because it cannot know the scheme; pass a base as the second argument or use the fully qualified form. And unlike Python, searchParams keeps blank values, so ?debug= survives as an empty string.

Traps that cause most parsing bugs

Frequently Asked Questions

What are the parts of a URL?

A full URL has up to seven parts: the scheme (https), the host (shop.example.com), an optional port (8443), the path (/catalog/shoes), the query string (?color=red&page=2), and the fragment (#reviews), plus optional user info before the host. Everything between the ? and the # is the query string; everything after the # is the fragment.

How do I parse a query string in Python?

Use urllib.parse. urlparse(url) splits the URL into scheme, netloc, path, query and fragment fields, and parse_qs(urlparse(url).query) turns the query string into a dictionary where every value is a list, because keys can repeat. One trap: parse_qs drops empty values unless you pass keep_blank_values=True.

What does ?key=value in a URL mean?

It is a query parameter: data handed to the server as part of the address. Everything after the first ? is a list of key=value pairs separated by &. The server or page reads these values to decide what to show, which is why they are the first place to look when a page renders something unexpected.