To check if a string is a URL in Python, parse it with urllib.parse.urlparse and confirm it has both a scheme and a network location; for stricter format checking add the validators library, but neither step alone tells you the URL is safe to use. The gap between "this parses as a URL" and "this is a URL I can safely fetch or render" is where the real bugs live, so this guide covers both: how to check if a URL is valid in Python, and the protocol and SSRF checks that turn a format test into a security control.
The Standard-Library Approach: urllib.parse
The simplest correct way to check if a string is a URL uses only the standard library:
from urllib.parse import urlparse
def is_url(value: str) -> bool:
try:
result = urlparse(value)
return all([result.scheme, result.netloc])
except (ValueError, AttributeError):
return False
urlparse splits the string into scheme, netloc, path, params, query, and fragment. Requiring both scheme (like https) and netloc (the host) rejects the common false positives: a bare path /foo/bar has no scheme, and http:// has no netloc. This is enough for "does this look like a real URL" in most application code.
Be aware of what urlparse does not do. It is lenient by design; it will happily parse strings a browser would reject and does not validate that the host is a real, well-formed domain. urlparse('http://not a real host') returns a result with a netloc. So is_url above answers "is this shaped like a URL", not "is this a good URL".
Stricter Format Checking: The validators Library
When you want to reject malformed hosts and get closer to real URL rules, the validators package is the common choice:
import validators
def is_valid_url(value: str) -> bool:
return validators.url(value) is True
validators.url returns True for a well-formed URL and a ValidationError (falsy) otherwise, applying stricter host and scheme rules than urlparse. It is a reasonable way to check if a URL is valid in Python when you need more rigor than the standard library, for instance validating a URL a user typed into a form before storing it.
Two caveats. First, it is a third-party dependency, so it is now part of your supply chain and its version needs to stay patched. Second, like every validator, it is checking format, and its notion of valid may differ from what the client library you later hand the URL to (requests, httpx, urllib) considers valid. A parsing-differential between your validator and your fetcher is a real bypass vector.
Regex Is Almost Never the Right Answer in Python Either
You will find plenty of Python URL regexes. Skip them for the same reasons they fail in every language: the URL grammar is too complex for a readable pattern, an ambitious pattern rejects valid URLs and accepts invalid ones, and a poorly written one can cause catastrophic backtracking (ReDoS) on crafted input, which in a synchronous request handler is a per-worker hang. urlparse plus explicit component checks is more correct and safer than any regex you would want to maintain.
The Critical Step Everyone Skips: Protocol Allowlisting
Here is the security bug that format validation misses. Both urlparse and validators will accept schemes you almost certainly do not want:
from urllib.parse import urlparse
urlparse("javascript:alert(1)").scheme # 'javascript'
urlparse("file:///etc/passwd").scheme # 'file'
urlparse("gopher://internal:70/").scheme # 'gopher'
If that URL later gets rendered as a link, a javascript: scheme is XSS. If your server fetches it, file: reads local files and exotic schemes reach internal services. So the load-bearing check is a scheme allowlist:
from urllib.parse import urlparse
ALLOWED_SCHEMES = {"http", "https"}
def is_safe_web_url(value: str) -> bool:
try:
parsed = urlparse(value)
except ValueError:
return False
return (
parsed.scheme in ALLOWED_SCHEMES
and bool(parsed.netloc)
)
Allowlist the schemes you actually support and reject everything else. This single check closes off the majority of the dangerous inputs that pass a naive "is it a URL" test.
If Your Server Fetches the URL: SSRF Defense
The moment Python code retrieves a user-supplied URL (webhooks, link unfurling, "import from URL", avatar fetchers), validation is not protection. A well-formed http://169.254.169.254/latest/meta-data/ passes every format check and hands an attacker your cloud instance credentials. This is server-side request forgery, CWE-918, and it needs defenses beyond parsing:
import ipaddress
import socket
from urllib.parse import urlparse
def resolved_ip_is_public(hostname: str) -> bool:
# Resolve and reject private/loopback/link-local ranges
infos = socket.getaddrinfo(hostname, None)
for family, _, _, _, sockaddr in infos:
ip = ipaddress.ip_address(sockaddr[0])
if (ip.is_private or ip.is_loopback
or ip.is_link_local or ip.is_reserved):
return False
return True
Then, critically, make the actual request to the resolved and vetted IP, not by hostname again, because DNS can rebind between your check and your fetch (a validated host resolving to a public IP once, then to 127.0.0.1 on the real request). Also disable or tightly constrain redirects in requests/httpx, since a permitted host can redirect you inward. Where the use case allows, an allowlist of destination hosts beats any blocklist. This whole class of issue is what a DAST scan tends to surface against a staging deployment, because it exercises the fetch path an attacker would.
Putting It Together
A practical layering for "accept a URL from a user in Python":
- Parse with
urlparse(orvalidators.urlfor stricter format rules), reject unparseable input. - Allowlist the scheme to
http/https, always. This is non-negotiable and catchesjavascript:,file:, and friends. - If you will fetch it server-side, resolve the host, reject private/internal IP ranges, pin the resolved IP, and constrain redirects.
- Keep validation dependencies patched, since a validator or HTTP client with a parsing bug is a vulnerability you inherited; an SCA tool flags those transitively so a stale
validatorsorrequestsversion does not quietly reopen a bypass.
Steps 1 and 2 answer "is this a valid, acceptable URL." Steps 3 and 4 are what make it safe to actually use.
FAQ
How do I check if a string is a URL in Python?
Parse it with urllib.parse.urlparse and verify it has both a scheme and a netloc. That rejects bare paths and scheme-only strings. For stricter format validation of the host, use the validators library's validators.url function.
Is urlparse enough to validate a URL?
For a basic "does this look like a URL" check, yes. But urlparse is deliberately lenient and accepts dangerous schemes like javascript: and file:, so always add a scheme allowlist (accept only http/https) before trusting or using the URL.
Should I use a regex to validate URLs in Python?
No. The URL grammar is too complex for a maintainable regex, and ambitious patterns can cause catastrophic backtracking (ReDoS) that hangs a worker on crafted input. urlparse with explicit component checks is both more correct and safer.
Does checking if a URL is valid prevent SSRF?
No. A perfectly valid URL can point at internal addresses like 169.254.169.254 or localhost. Preventing SSRF requires resolving the hostname, rejecting private and link-local IP ranges, requesting the pinned resolved IP, and constraining redirects, ideally with a destination host allowlist.