O módulo urllib.parse no Python separa URLs em componentes, reconstrói endereços, combina caminhos relativos e codifica dados para query strings. Ele é essencial quando uma aplicação precisa manipular URLs sem depender de concatenação manual de strings.
A biblioteca é prática e compatível com código antigo, mas não funciona como um validador completo. urlsplit() e urlparse() podem aceitar entradas incomuns e devolver componentes vazios em vez de lançar erro. Quando a URL tem implicações de segurança, valide esquema, host, porta, credenciais, caminho e destino após o parsing.
Separe uma URL com urlsplit
from urllib.parse import urlsplit
resultado = urlsplit(
"https://usuario:senha@api.exemplo.com:8443/v1/itens"
"?pagina=2&ordem=nome#detalhes"
)
print(resultado.scheme)
print(resultado.hostname)
print(resultado.port)
print(resultado.path)
print(resultado.query)
print(resultado.fragment)urlsplit() retorna scheme, netloc, path, query e fragment. O resultado também expõe username, password, hostname e port.
Prefira urlsplit para URLs modernas
urlparse() devolve um campo extra chamado params, baseado em uma sintaxe histórica. Para a maioria das URLs HTTP atuais, urlsplit() é mais simples. Use urlparse() apenas quando a separação de parâmetros de caminho realmente fizer parte do formato que você precisa tratar.
A presença de // importa
from urllib.parse import urlsplit
print(urlsplit("exemplo.com/pagina"))
print(urlsplit("//exemplo.com/pagina"))
print(urlsplit("https://exemplo.com/pagina"))Sem //, a primeira entrada é interpretada como caminho, não como hostname. Não adicione esquema automaticamente sem uma regra clara, porque strings como javascript:..., caminhos locais ou identificadores internos podem ser transformados de forma insegura.
Valide esquema e hostname
from urllib.parse import urlsplit
ESQUEMAS = {"https"}
HOSTS = {"api.exemplo.com", "cdn.exemplo.com"}
def validar_url(texto: str):
partes = urlsplit(texto)
if partes.scheme.lower() not in ESQUEMAS:
raise ValueError("esquema não permitido")
if partes.hostname not in HOSTS:
raise ValueError("host não permitido")
if partes.username is not None or partes.password is not None:
raise ValueError("credenciais na URL não são aceitas")
try:
porta = partes.port
except ValueError as erro:
raise ValueError("porta inválida") from erro
if porta not in (None, 443):
raise ValueError("porta não permitida")
return partesO atributo hostname é normalizado para minúsculas, enquanto netloc mantém a forma textual. Leia port dentro de tratamento de erro, porque portas fora da faixa geram ValueError.
Parsing não é validação
A documentação oficial deixa explícito que as funções priorizam funcionalidade prática. Elas não garantem conformidade total com RFC 3986 nem com a especificação WHATWG dos navegadores. Defina o que sua aplicação considera válido.
Uma política pode exigir HTTPS, host conhecido, porta padrão, ausência de usuário e senha, caminho absoluto, quantidade máxima de caracteres e ausência de controles. Outra aplicação pode aceitar URLs relativas. A validação deve refletir esse contrato.
URLs e endereços IP
Quando o hostname pode ser um IP, valide-o com ipaddress no Python. Essa verificação ajuda a identificar loopback, link-local e redes privadas, mas não resolve SSRF sozinha. Hostnames precisam ser resolvidos, todos os IPs retornados precisam ser verificados e redirecionamentos devem passar pela mesma política.
Remova fragmentos
from urllib.parse import urldefrag
resultado = urldefrag("https://exemplo.com/manual#instalacao")
print(resultado.url)
print(resultado.fragment)O fragmento não é enviado ao servidor em uma requisição HTTP normal. Removê-lo ajuda a criar chaves de cache ou comparar URLs, mas URLs ainda podem ser equivalentes mesmo com diferenças em porta padrão, percent-encoding ou path.
Reconstrua com geturl ou urlunsplit
from urllib.parse import urlsplit
partes = urlsplit("HTTP://Exemplo.com/pagina?#")
limpa = partes._replace(fragment="").geturl()
print(limpa)
_replace() cria um novo resultado. geturl() pode normalizar o esquema para minúsculas e remover delimitadores vazios. Não dependa da reconstrução para preservar exatamente a string original.
Combine caminhos relativos com urljoin
from urllib.parse import urljoin
base = "https://docs.exemplo.com/guias/python/"
print(urljoin(base, "instalacao.html"))
print(urljoin(base, "../referencia/api.html"))urljoin() segue regras de resolução de URLs relativas. Ele é útil em crawlers, leitores de feeds e links de documentação.
Cuidado com urljoin e entrada externa
from urllib.parse import urljoin
base = "https://site.exemplo.com/usuarios/"
print(urljoin(base, "https://atacante.exemplo/roubo"))Uma segunda URL absoluta substitui esquema e host da base. Portanto, não use urljoin(base, valor_do_usuario) esperando que o resultado permaneça no domínio. Antes, use urlsplit() para rejeitar scheme e netloc na entrada relativa, e valide novamente o resultado final.
from urllib.parse import urljoin, urlsplit
def juntar_interno(base: str, relativo: str) -> str:
entrada = urlsplit(relativo)
if entrada.scheme or entrada.netloc:
raise ValueError("URL absoluta não permitida")
final = urljoin(base, relativo)
base_host = urlsplit(base).hostname
if urlsplit(final).hostname != base_host:
raise ValueError("destino fora do host")
return finalCodifique componentes com quote
from urllib.parse import quote
nome = "Relatório de vendas/2026"
print(quote(nome))
print(quote(nome, safe=""))quote() usa percent-encoding. Por padrão, a barra é considerada segura, pois a função foi pensada para caminhos. Para codificar um único segmento, use safe="" para que a barra não seja interpretada como separador.
Não codifique a URL inteira de uma vez
Esquema, hostname, path, query e fragmento possuem regras diferentes. Codificar uma URL completa com quote() pode transformar :, / e ? em dados e quebrar sua estrutura. Monte e codifique cada componente separadamente.
quote versus quote_plus
from urllib.parse import quote, quote_plus
texto = "python avançado"
print(quote(texto)) # python%20avan%C3%A7ado
print(quote_plus(texto)) # python+avan%C3%A7adoquote_plus() é indicado para dados de formulário e query strings, onde espaço vira +. Em paths, prefira quote(). Um sinal de mais literal precisa ser codificado para não virar espaço ao usar unquote_plus().
Crie query strings com urlencode
from urllib.parse import urlencode
parametros = {
"busca": "python web",
"pagina": "2",
"idioma": "pt-BR",
}
query = urlencode(parametros)
url = f"https://api.exemplo.com/pesquisa?{query}"
print(url)urlencode() evita erros de caracteres especiais. No Python 3.14, passar certos objetos falsy diferentes de strings vazias, bytes e None está depreciado. Converta números e valores de domínio explicitamente.
Parâmetros repetidos com doseq
from urllib.parse import urlencode
query = urlencode(
{"tag": ["python", "web"], "pagina": "1"},
doseq=True,
)
print(query)Sem doseq=True, a lista pode ser convertida como uma representação Python em vez de gerar vários pares.
Preserve a ordem quando necessário
from urllib.parse import urlencode
pares = [
("ordem", "nome"),
("ordem", "data"),
("pagina", "1"),
]
print(urlencode(pares))Uma sequência de pares mantém ordem e parâmetros duplicados. Isso é útil em assinaturas de requisição e APIs que atribuem significado à ordem, embora o protocolo deva documentar essa exigência.
Leia queries com parse_qs e parse_qsl
from urllib.parse import parse_qs, parse_qsl
query = "tag=python&tag=web&vazio="
print(parse_qs(query, keep_blank_values=True))
print(parse_qsl(query, keep_blank_values=True))parse_qs() agrupa valores por chave; parse_qsl() mantém a sequência de pares. Defina max_num_fields em entradas externas para evitar consumo excessivo.
dados = parse_qs(
query_recebida,
keep_blank_values=True,
strict_parsing=True,
max_num_fields=200,
)Decodificação e erros
unquote() usa UTF-8 e, por padrão, substitui sequências inválidas. Em dados críticos, escolha errors="strict" para rejeitar bytes inválidos. Use unquote_to_bytes() quando você precisa dos octetos originais antes de decidir a codificação.
Evite dupla decodificação
Uma entrada como %252e%252e%252f vira %2e%2e%2f após uma decodificação e ../ após outra. Decodifique em uma única camada bem definida. Componentes usados como caminhos locais também precisam de validação contra path traversal; o artigo de tarfile no Python mostra riscos semelhantes em extrações.
str e bytes
As funções de parsing aceitam str ou bytes ASCII. Não misture os tipos na mesma chamada. Bytes não ASCII geram UnicodeDecodeError. Em aplicações web, normalmente é melhor decodificar o transporte de forma explícita e trabalhar com str.
URLs com IPv6
Um IPv6 literal no hostname precisa de colchetes, como https://[2001:db8::1]:8443/. Colchetes não balanceados geram erro. Após o parsing, valide hostname com ipaddress e trate zone IDs conforme a política.
Logs e dados sensíveis
URLs podem conter senhas, tokens e dados pessoais na query. Não registre a URL completa automaticamente. Remova credenciais e filtre chaves como token, key, password e signature.
from urllib.parse import parse_qsl, urlencode, urlsplit, urlunsplit
SENSIVEIS = {"token", "password", "key", "signature"}
def url_para_log(url: str) -> str:
p = urlsplit(url)
query = urlencode([
(chave, "***" if chave.lower() in SENSIVEIS else valor)
for chave, valor in parse_qsl(p.query, keep_blank_values=True)
])
host = p.hostname or ""
if p.port:
host = f"{host}:{p.port}"
return urlunsplit((p.scheme, host, p.path, query, ""))Configuração e observabilidade
Carregue listas de esquemas, hosts e portas com validação, por exemplo usando configparser no Python. Para investigar decisões de redirecionamento e parsing, use trace no Python. Em crawlers concorrentes, distribua URLs com queue no Python e aplique limites por host.
Testes recomendados
Teste URLs absolutas e relativas, host ausente, credenciais, portas inválidas, IPv6, caracteres de controle, Unicode, fragmentos, queries duplicadas, parâmetros em excesso, percent-encoding inválido, dupla codificação, urljoin() com URL absoluta e redirecionamentos para redes privadas.
Boas práticas
- Prefira
urlsplit()para URLs modernas. - Valide cada componente após o parsing.
- Use allowlists para esquemas e destinos críticos.
- Não confie em
urljoin()com entrada externa. - Codifique cada componente separadamente.
- Limite campos de query.
- Evite dupla decodificação.
- Remova segredos de logs.
- Combine URL, DNS e validação IP contra SSRF.
Conclusão
O urllib.parse no Python elimina manipulações frágeis de URLs e oferece APIs claras para decomposição, resolução, quoting e query strings. Seu uso seguro exige uma camada de validação baseada no contrato da aplicação.
Consulte a documentação oficial do urllib.parse e a RFC 3986. Parsing fornece componentes; confiar no destino é uma decisão separada.







