URL parser: how a URL breaks into parts
Take a real address and pull it apart. In https://, 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
| Part | In the example | What it does |
|---|---|---|
| scheme | https | How to talk to the server: the protocol. |
| user info | user:pw@ (optional) | Credentials before the host. Rare today; never put real passwords in URLs. |
| host | shop.example.com | The machine, by domain name or IP. |
| port | 8443 (optional) | The door on that machine. Hidden when it is the default: 80 for http, 443 for https. |
| path | /catalog/shoes | What you want on that machine. Servers route on it. |
| query | color=red&size=41... | Extra parameters after the ?: key=value pairs joined by &. |
| fragment | #reviews | A 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:
- Encoding. Spaces cannot live in a URL, so they become
%20or a+depending on who did the encoding. Reserved characters (&,=,?,#) inside a value get percent-encoded too, which is whyq=a%26bis the valuea&band not two parameters. - Repeated keys.
size=41&size=42is how lists travel. Some stacks read both values; some keep only the last. APIs that need arrays in one key often usea=1&a=2or the PHP-stylea[]=1; there is no single standard, so check what your target expects. - Blank values.
?debugis a flag with an empty value. Parsers disagree on whether it survives, so flag-style parameters deserve an explicit test.
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() insteadThree 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
- The missing scheme. Browsers hide
https://in the address bar, so people pasteexample.com/cataloginto code that expects a full URL. Python'surlparsedoes not throw: it silently puts everything inpathand leavesnetlocempty, and the bug surfaces later. JavaScript'sURLthrows instead. Validate the scheme explicitly when input comes from humans. - Encoded slashes.
%2Finside a path segment is a slash that some servers decode early and others reject outright, so a path meant to carry an encoded slash can 404 depending on the stack in front of you. APIs that accept encoded slashes often ask for them double-encoded; read their docs, do not assume. - Fragment versus path. Everything after
#never reaches the server: the browser strips it before the request leaves the machine. Anything the server needs to know cannot live in the fragment, which is why single-page apps usehistory.pushStatepaths instead of#routes. - Internationalized domains. A host with non-ASCII letters, like a German
münchen, travels as punycode on the wire:münchen.examplebecomesxn--mnchen-3ya.example. Both forms can appear in logs and redirects; normalize with IDNA encoding before comparing hosts.
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.