pathlib em vez de os.path: manipulação moderna de arquivos

1. Visão geral

Muito código Python antigo manipula caminhos usando os.path.join, os.path.splitext, os.listdir… Essas funções recebem e retornam strings, então o código fica longo, difícil de ler e propenso a erros. O módulo pathlib (disponível desde Python 3.4) representa caminhos através de objetos Path que possuem todos os métodos necessários para juntar, separar, ler, escrever e percorrer arquivos.

Após ler este artigo, você será capaz de converter a maior parte do código que usa os.path para pathlib e conhecerá as armadilhas comuns. Os exemplos usam Python 3.12.

2. Criando objetos Path

Path é o ponto de partida para todas as operações:

from pathlib import Path

reports = Path("data/reports")
home = Path.home()
cwd = Path.cwd()

print(reports)
print(type(reports))
print(home)
data/reports
<class 'pathlib.PosixPath'>
/home/minh

Explicação:

  • Path("data/reports") cria um objeto de caminho. Neste ponto, nenhuma operação é realizada no disco. O diretório não precisa existir.
  • No Linux/macOS, Path retorna PosixPath. No Windows, retorna WindowsPath. Você não precisa se preocupar com a classe específica.
  • Path.home() e Path.cwd() substituem os.path.expanduser("~") e os.getcwd().

Se você só precisa manipular strings de caminho sem acessar arquivos reais, use PurePath, PurePosixPath ou PureWindowsPath. Isso é útil para processar caminhos Windows enquanto estiver rodando no Linux.

3. Concatenando caminhos com o operador /

Essa é a mudança mais visível. Em vez de aninhar os.path.join, você usa o operador /:

import os
from pathlib import Path

# Forma antiga
old = os.path.join("data", "orders", "2026", "order_1001.json")

# Forma nova
base = Path("data")
new = base / "orders" / "2026" / "order_1001.json"

print(old)
print(new)
print(new == Path(old))
data/orders/2026/order_1001.json
data/orders/2026/order_1001.json
True

O operador / aceita tanto Path quanto str no lado direito. O resultado é sempre um Path, então você pode continuar concatenando. O separador se adapta automaticamente ao sistema operacional: / no Linux, \ no Windows.

Se o número de componentes variar em tempo de execução, use joinpath:

parts = ["orders", "2026", "order_1001.json"]
path = Path("data").joinpath(*parts)
print(path)  # data/orders/2026/order_1001.json

4. Separando componentes do caminho

os.path requer várias funções para isso: basename, dirname, splitext. Com pathlib, tudo é um atributo:

from pathlib import Path

invoice = Path("/srv/shop/invoices/2026/invoice_042.final.pdf")

print(invoice.name)      # nome completo do arquivo
print(invoice.stem)      # nome sem a extensão final
print(invoice.suffix)    # extensão final
print(invoice.suffixes)  # todas as extensões
print(invoice.parent)    # diretório que contém
print(invoice.parents[1])
print(invoice.parts)
invoice_042.final.pdf
invoice_042.final
.pdf
['.final', '.pdf']
/srv/shop/invoices/2026
/srv/shop/invoices
('/', 'srv', 'shop', 'invoices', '2026', 'invoice_042.final.pdf')

Explicação:

  • stem e suffix apenas separam no último ponto. Para arquivos .tar.gz, você precisa usar suffixes.
  • parent equivale a os.path.dirname. parents é uma sequência de diretórios pai, subindo gradualmente até a raiz.
  • parts separa o caminho em uma tupla, útil ao comparar cada nível.

Para criar um novo caminho com nome ou extensão diferentes, use os métodos with_*:

invoice = Path("invoices/invoice_042.pdf")

print(invoice.with_suffix(".xml"))    # invoices/invoice_042.xml
print(invoice.with_stem("invoice_043"))  # invoices/invoice_043.pdf
print(invoice.with_name("summary.csv"))  # invoices/summary.csv

Esses métodos não renomeiam o arquivo no disco. Eles apenas retornam um novo Path, pois Path é imutável.

5. Lendo e escrevendo arquivos

Para arquivos pequenos, você não precisa de open():

from pathlib import Path

config = Path("app_config.txt")
config.write_text("currency=VND\ntimeout=30\n", encoding="utf-8")

content = config.read_text(encoding="utf-8")
print(content.splitlines())

logo = Path("logo.bin")
logo.write_bytes(b"\x89PNG")
print(logo.read_bytes())
['currency=VND', 'timeout=30']
b'\x89PNG'
  • write_text abre o arquivo, escreve e fecha. O arquivo antigo é sobrescrito.
  • read_text lê todo o conteúdo como uma string.
  • write_bytes e read_bytes funcionam de forma semelhante com dados binários.

Para arquivos grandes ou ao ler linha por linha, você ainda usa um context manager. Path possui o método open integrado:

with Path("orders.csv").open("w", encoding="utf-8") as f:
    f.write("id,amount\n1001,250000\n1002,99000\n")

with Path("orders.csv").open(encoding="utf-8") as f:
    total = sum(int(line.split(",")[1]) for line in f if not line.startswith("id"))

print(total)  # 349000

Nota: Sempre forneça encoding="utf-8". Se omitido, Python usa a codificação padrão do sistema. No Windows, o padrão geralmente é cp1252, então dados em vietnamita ficarão corrompidos.

6. Verificando e percorrendo diretórios

6.1. Verificando o estado

from pathlib import Path

p = Path("orders.csv")
print(p.exists(), p.is_file(), p.is_dir())
print(p.stat().st_size)
print(p.resolve())
True True False
36
/home/minh/project/orders.csv

resolve() retorna o caminho absoluto e resolve symlinks. Substitui os.path.abspath e os.path.realpath.

6.2. Listando e encontrando arquivos

from pathlib import Path

root = Path("shop")
(root / "orders" / "2026").mkdir(parents=True, exist_ok=True)
(root / "orders" / "2026" / "o1.json").write_text("{}", encoding="utf-8")
(root / "orders" / "o0.json").write_text("{}", encoding="utf-8")
(root / "readme.md").write_text("# Shop", encoding="utf-8")

print(sorted(p.name for p in root.iterdir()))
print(sorted(str(p) for p in root.glob("*.md")))
print(sorted(str(p) for p in root.rglob("*.json")))
['orders', 'readme.md']
['shop/readme.md']
['shop/orders/2026/o1.json', 'shop/orders/o0.json']
  • iterdir() substitui os.listdir(). Retorna Path completos, não apenas nomes de arquivos, então você não precisa concatenar novamente.
  • glob("*.md") busca apenas no diretório atual.
  • rglob("*.json") busca recursivamente, equivalente a glob("**/*.json").

A partir do Python 3.12, Path.walk() substitui os.walk():

for dirpath, dirnames, filenames in Path("shop").walk():
    print(dirpath, filenames)

7. Criando, renomeando e deletando

import shutil
from pathlib import Path

backup = Path("backup/daily")
backup.mkdir(parents=True, exist_ok=True)

src = Path("orders.csv")
dst = src.rename(backup / "orders_2026-10-17.csv")
print(dst)

dst.unlink()
Path("not_there.txt").unlink(missing_ok=True)

backup.rmdir()
shutil.rmtree("backup")
backup/daily/orders_2026-10-17.csv
  • mkdir(parents=True, exist_ok=True) equivale a os.makedirs(..., exist_ok=True).
  • rename retorna um novo Path. No Windows, rename gera erro se o arquivo de destino já existe. Para sobrescrever em qualquer plataforma, use replace.
  • unlink deleta um arquivo. Com missing_ok=True, não gera erro se o arquivo não existir.
  • rmdir remove apenas diretórios vazios. Para remover uma árvore inteira de diretórios, você ainda precisa de shutil.rmtree. Essa função aceita Path normalmente.

8. Tabela de referência rápida

os / os.path pathlib
os.path.join(a, b) Path(a) / b
os.path.basename(p) p.name
os.path.dirname(p) p.parent
os.path.splitext(p) p.stem, p.suffix
os.path.exists(p) p.exists()
os.path.isfile(p) / isdir p.is_file() / p.is_dir()
os.path.abspath(p) p.resolve() ou p.absolute()
os.path.expanduser("~") Path.home() ou p.expanduser()
os.getcwd() Path.cwd()
os.listdir(p) p.iterdir()
glob.glob("**/*.py", recursive=True) p.rglob("*.py")
os.makedirs(p, exist_ok=True) p.mkdir(parents=True, exist_ok=True)
os.remove(p) p.unlink()
os.rename(a, b) a.rename(b)
os.path.getsize(p) p.stat().st_size
os.path.relpath(p, start) p.relative_to(start)

9. Escrevendo testes com pytest

O fixture tmp_path do pytest já retorna um Path, então código de teste usando pathlib é muito conciso. Suponha que você tenha uma função que move arquivos PDF de fatura para um diretório de arquivo:

# archive.py
from pathlib import Path


def archive_invoices(source: Path, target: Path) -> list[Path]:
    target.mkdir(parents=True, exist_ok=True)
    moved = []
    for pdf in sorted(source.rglob("*.pdf")):
        moved.append(pdf.rename(target / pdf.name))
    return moved

O teste correspondente:

# test_archive.py
from pathlib import Path

from archive import archive_invoices


def test_archive_moves_only_pdf_files(tmp_path: Path):
    source = tmp_path / "inbox"
    (source / "2026").mkdir(parents=True)
    (source / "a.pdf").write_bytes(b"%PDF")
    (source / "2026" / "b.pdf").write_bytes(b"%PDF")
    (source / "note.txt").write_text("skip me", encoding="utf-8")
    target = tmp_path / "archive"

    moved = archive_invoices(source, target)

    assert [p.name for p in moved] == ["b.pdf", "a.pdf"]
    assert sorted(p.name for p in target.iterdir()) == ["a.pdf", "b.pdf"]
    assert (source / "note.txt").exists()
    assert not (source / "a.pdf").exists()
pytest -q test_archive.py
.                                                                [100%]
1 passed in 0.02s

A ordem em moved é b.pdf antes de a.pdf. Razão: sorted compara objetos Path, e inbox/2026/b.pdf vem antes de inbox/a.pdf porque o caractere 2 é menor que a. Esse detalhe é fácil de negligenciar quando você muda de comparação de strings de nomes de arquivo para comparação de Path.

10. Erros comuns

Adicionando Path com string. Path("data") + "/x.txt" gera TypeError. Use / em vez de +. Se você realmente precisar concatenar ao nome do arquivo, use p.with_name(p.name + "_old").

O lado direito é um caminho absoluto. O resultado pode surpreendê-lo:

from pathlib import Path

print(Path("/srv/uploads") / "/etc/passwd")  # /etc/passwd

Um componente absoluto do lado direito substitui todo o lado esquerdo. Se o nome do arquivo vem da entrada do usuário, você deve validar antes de usar:

base = Path("/srv/uploads").resolve()
candidate = (base / user_input).resolve()
if not candidate.is_relative_to(base):
    raise ValueError("Caminho inválido")

with_suffix faltando ponto. p.with_suffix("pdf") gera ValueError. Você deve escrever ".pdf".

Esquecer que glob retorna um gerador. Você pode percorrê-lo apenas uma vez. Se precisar reutilizá-lo ou chamar len(), envolva em list(...). A ordem dos resultados também não é garantida, então você precisa de sorted quando a ordem importa.

Arquivos ocultos e stem. Com Path(".env"), suffix é uma string vazia e stem é ".env". Não assuma que todo arquivo tem uma extensão.

Bibliotecas antigas aceitam apenas str. A maioria das APIs padrão aceitam os.PathLike desde Python 3.6. Se encontrar uma biblioteca antiga, use str(p) ou os.fspath(p).

Nota: pathlib não substitui completamente os e shutil. Para copiar arquivos, você ainda usa shutil.copy2 (Python 3.14 adiciona Path.copy). Variáveis de ambiente ainda são lidas via os.environ. Permissões complexas ainda requerem os.chmod ou os.access.

Quando não precisa mudar? Em loops processando milhões de strings de caminho sem acessar o disco, criar objetos Path é mais lento que manipular strings puras. Isso é raro, e você deve medir antes de otimizar. Para código antigo que funciona bem, não precisa refatorar em massa. Mude gradualmente quando trabalhar em cada módulo.

11. Conclusão

pathlib consolida operações espalhadas em os, os.path e glob em um único objeto. Código mais curto, leitura mais natural e menos erros de concatenação de strings. Você viu como concatenar caminhos com /, separar componentes com name, stem, suffix, parent, ler e escrever arquivos com read_text e write_text, percorrer diretórios com iterdir, glob, rglob, walk, e testar com tmp_path do pytest.

Lembre-se de três pontos. Sempre forneça encoding. Cuidado ao concatenar com caminhos absolutos de entrada externa. Ainda use shutil para operações como copiar e remover árvores de diretórios. Para código novo, use Path por padrão e retorne a os.path apenas se houver uma razão específica.

pythonpathlibos.pathfile-iopython3