Leitura e Escrita de Arquivos

Leitura e Escrita de Arquivos

Arquivos de texto, CSV, JSON e pickle em Python, com pathlib e o with. E os defeitos que não dão erro: o open() sem encoding que corrompe acentos no Windows, o CSV que devolve nota como texto e ordena 10 depois de 7, e o modo "w" que apaga o arquivo antes de gravar.
Python

• • 21 min de leitura

Programas raramente existem de forma isolada — eles leem configurações, processam dados de entrada, geram relatórios e persistem informações. Arquivos são o mecanismo mais fundamental de persistência em qualquer sistema. Python oferece uma API limpa e expressiva para trabalhar com arquivos de texto, binários e formatos estruturados como CSV e JSON.

Abrindo e Fechando Arquivos

A função open() abre um arquivo e retorna um objeto de arquivo:

# Forma manual — exige fechar explicitamente
arquivo = open("dados.txt", "r", encoding="utf-8")
conteudo = arquivo.read()
arquivo.close()

# Forma recomendada — with fecha automaticamente
with open("dados.txt", "r", encoding="utf-8") as arquivo:
    conteudo = arquivo.read()

Sempre especifique encoding="utf-8" — evita problemas com acentuação em diferentes sistemas operacionais.

O conselho merece a explicação, porque o motivo não é óbvio. Sem esse argumento, o Python usa a codificação preferida do sistema, e não uma codificação fixa: em Linux e macOS isso costuma ser UTF-8, e no Windows, historicamente, é uma página de código regional — cp1252 no Brasil. O mesmo programa, lendo o mesmo arquivo, funciona numa máquina e levanta UnicodeDecodeError na outra. Pior: às vezes não levanta erro nenhum e apenas troca os acentos por caracteres errados, o que passa despercebido até alguém ler o relatório.

Como é fácil esquecer, o Python oferece uma forma de caçar as ocorrências: executando com -X warn_default_encoding, todo open() sem encoding passa a emitir um aviso apontando a linha.

python -X warn_default_encoding -W error::EncodingWarning programa.py
# EncodingWarning: 'encoding' argument not specified

Vale rodar a suíte de testes assim de vez em quando. A regra só não se aplica ao modo binário, com "rb" e "wb", em que não há decodificação e passar encoding é erro.

Modos de Abertura

# Leitura (padrão) — erro se arquivo não existir
with open("arquivo.txt", "r") as f: ...

# Escrita — cria o arquivo ou sobrescreve se existir
with open("arquivo.txt", "w") as f: ...

# Adição — cria ou adiciona ao final
with open("arquivo.txt", "a") as f: ...

# Leitura e escrita
with open("arquivo.txt", "r+") as f: ...

# Modo binário — para imagens, PDFs, etc.
with open("imagem.png", "rb") as f: ...
with open("saida.png",  "wb") as f: ...

# Criação exclusiva — erro se arquivo já existir
with open("novo.txt", "x") as f: ...

Lendo Arquivos

# read() — lê o arquivo inteiro como string
with open("texto.txt", "r", encoding="utf-8") as f:
    conteudo = f.read()
    print(conteudo)

# readline() — lê uma linha por vez
with open("texto.txt", "r", encoding="utf-8") as f:
    primeira = f.readline()
    segunda  = f.readline()

# readlines() — retorna lista de linhas
with open("texto.txt", "r", encoding="utf-8") as f:
    linhas = f.readlines()
    print(linhas)  # ['linha1\n', 'linha2\n', 'linha3\n']

# Iteração direta — mais eficiente para arquivos grandes
with open("texto.txt", "r", encoding="utf-8") as f:
    for linha in f:
        print(linha.strip())  # strip() remove o \n do final

Para arquivos grandes, a iteração direta é preferível — não carrega tudo na memória de uma vez.

Escrevendo Arquivos

# Escrita simples
with open("saida.txt", "w", encoding="utf-8") as f:
    f.write("Primeira linha\n")
    f.write("Segunda linha\n")

# writelines() — escreve lista de strings
linhas = ["Ana\n", "Bruno\n", "Carla\n"]
with open("nomes.txt", "w", encoding="utf-8") as f:
    f.writelines(linhas)

# print() com file= — prático para formatação
with open("relatorio.txt", "w", encoding="utf-8") as f:
    print("=== Relatório ===", file=f)
    print(f"Total: {42}", file=f)

# Adicionando ao final
with open("log.txt", "a", encoding="utf-8") as f:
    from datetime import datetime
    f.write(f"{datetime.now()} — Evento registrado\n")

Trabalhando com Caminhos: pathlib

O módulo pathlib oferece uma forma orientada a objetos e multiplataforma de trabalhar com caminhos — substitui o antigo os.path:

from pathlib import Path

# Criando caminhos
base    = Path("/home/usuario/projetos")
arquivo = base / "dados" / "resultado.txt"

print(arquivo)          # /home/usuario/projetos/dados/resultado.txt
print(arquivo.name)     # resultado.txt
print(arquivo.stem)     # resultado
print(arquivo.suffix)   # .txt
print(arquivo.parent)   # /home/usuario/projetos/dados

# Verificações
print(arquivo.exists())
print(arquivo.is_file())
print(arquivo.is_dir())

# Leitura e escrita diretas
arquivo.parent.mkdir(parents=True, exist_ok=True)
arquivo.write_text("Conteúdo do arquivo", encoding="utf-8")
conteudo = arquivo.read_text(encoding="utf-8")

# Listando arquivos
pasta = Path(".")
for item in pasta.iterdir():
    print(item)

# Filtrando por extensão
for py_file in pasta.glob("**/*.py"):
    print(py_file)

# Caminho do usuário
home = Path.home()
config = home / ".config" / "meu_app" / "config.json"

Arquivos CSV

CSV (Comma-Separated Values) é o formato mais comum para dados tabulares:

import csv
from pathlib import Path

# Escrevendo CSV
alunos = [
    {"nome": "Ana",    "nota": 9.5, "turma": "A"},
    {"nome": "Bruno",  "nota": 7.0, "turma": "B"},
    {"nome": "Carla",  "nota": 8.5, "turma": "A"},
]

with open("alunos.csv", "w", newline="", encoding="utf-8") as f:
    campos = ["nome", "nota", "turma"]
    writer = csv.DictWriter(f, fieldnames=campos)
    writer.writeheader()
    writer.writerows(alunos)

# Lendo CSV
with open("alunos.csv", "r", encoding="utf-8") as f:
    reader = csv.DictReader(f)
    for linha in reader:
        print(f"{linha['nome']:10} — nota: {linha['nota']}")
        # atenção: linha['nota'] é a STRING "9.5", não o número

# CSV com delimitador diferente
with open("dados.csv", "r", encoding="utf-8") as f:
    reader = csv.reader(f, delimiter=";")
    for linha in reader:
        print(linha)

Duas ressalvas sobre CSV, e a primeira é a que mais produz relatório errado sem dar erro: o módulo csv devolve tudo como texto. Escrevemos 9.5 como número e lemos de volta a string "9.5". A f-string do exemplo acima esconde isso perfeitamente, porque imprimir string ou número dá o mesmo resultado na tela — o problema aparece na primeira conta.

notas = [l["nota"] for l in csv.DictReader(f)]

sum(notas)
# TypeError: unsupported operand type(s) for +: 'int' and 'str'

sorted(["9.5", "10.0", "7.0"])
# ['10.0', '7.0', '9.5']   — ordem alfabética: "10" vem antes de "7"

A soma ao menos falha barulhentamente. A ordenação é o caso perigoso: devolve uma lista plausível, com a nota 10 em primeiro lugar por começar com o algarismo 1, e ninguém percebe. A conversão é responsabilidade de quem lê, e deve acontecer na entrada — float(linha["nota"]) —, tratando o valor ausente ou malformado ali mesmo. Quando a tabela é grande e tem muitas colunas com tipos diferentes, vale considerar o pandas, que infere os tipos ao ler.

A segunda ressalva é o newline="". O exemplo o usa ao escrever, e a documentação do módulo pede que ele seja usado também na leitura — pelo motivo concreto de que, sem ele, uma quebra de linha dentro de um campo entre aspas é convertida em silêncio:

# um campo com \r\n dentro, como o Excel grava
with open("dados.csv", newline="", encoding="utf-8") as f:
    list(csv.reader(f))   # ['1', 'linha um\r\nlinha dois']  — preservado

with open("dados.csv", encoding="utf-8") as f:
    list(csv.reader(f))   # ['1', 'linha um\nlinha dois']    — alterado

Não há erro, e o dado que sai do programa não é o que entrou. Na escrita o argumento evita o problema espelhado: no Windows, sem ele, o terminador \r\n produzido pelo módulo é traduzido outra vez e o arquivo sai com uma linha em branco entre cada registro. A regra é curta — newline="" em toda abertura de arquivo CSV, nos dois sentidos.

Arquivos JSON

JSON é o formato padrão para troca de dados em APIs e configurações:

import json
from pathlib import Path

# Serialização — Python → JSON
config = {
    "host":    "localhost",
    "porta":   5432,
    "debug":   True,
    "tags":    ["producao", "v2"],
    "timeout": None
}

# Para string
json_str = json.dumps(config, indent=2, ensure_ascii=False)
print(json_str)

# Para arquivo
with open("config.json", "w", encoding="utf-8") as f:
    json.dump(config, f, indent=2, ensure_ascii=False)

# Desserialização — JSON → Python
with open("config.json", "r", encoding="utf-8") as f:
    dados = json.load(f)

print(dados["host"])   # localhost
print(type(dados))     # <class 'dict'>

# Usando pathlib diretamente
config_path = Path("config.json")
dados = json.loads(config_path.read_text(encoding="utf-8"))

Correspondência de tipos:

JSON          Python
─────────────────────
object    →   dict
array     →   list
string    →   str
number    →   int / float
true/false→   True / False
null      →   None

Arquivos Binários e pickle

Para serializar objetos Python arbitrários:

import pickle
from dataclasses import dataclass
from typing import List

@dataclass
class ModeloML:
    nome:       str
    parametros: dict
    acuracia:   float
    versao:     str

modelo = ModeloML(
    nome="ClassificadorSpam",
    parametros={"learning_rate": 0.01, "epochs": 100},
    acuracia=0.94,
    versao="1.0.0"
)

# Salvando
with open("modelo.pkl", "wb") as f:
    pickle.dump(modelo, f)

# Carregando
with open("modelo.pkl", "rb") as f:
    modelo_carregado = pickle.load(f)

print(modelo_carregado)
print(modelo_carregado.acuracia)  # 0.94

Atenção: nunca carregue arquivos .pkl de fontes não confiáveis — o pickle pode executar código arbitrário durante a desserialização.

Exemplo Completo: Sistema de Log

import json
from pathlib import Path
from datetime import datetime
from enum import Enum


class Nivel(str, Enum):
    DEBUG   = "DEBUG"
    INFO    = "INFO"
    AVISO   = "AVISO"
    ERRO    = "ERRO"
    CRITICO = "CRITICO"


class Logger:
    def __init__(self, nome: str, pasta: str = "logs"):
        self.nome  = nome
        self.pasta = Path(pasta)
        self.pasta.mkdir(parents=True, exist_ok=True)

        data_hoje         = datetime.now().strftime("%Y-%m-%d")
        self._arquivo_txt  = self.pasta / f"{nome}_{data_hoje}.log"
        self._arquivo_json = self.pasta / f"{nome}_{data_hoje}.jsonl"

    def _registrar(self, nivel: Nivel, mensagem: str, **extras):
        agora  = datetime.now()
        entrada = {
            "timestamp": agora.isoformat(),
            "nivel":     nivel.value,
            "logger":    self.nome,
            "mensagem":  mensagem,
            **extras
        }

        # Formato legível para .log
        linha_txt = (f"[{agora.strftime('%H:%M:%S')}] "
                     f"[{nivel.value:7}] "
                     f"[{self.nome}] {mensagem}")

        if extras:
            linha_txt += f" | {extras}"

        with open(self._arquivo_txt, "a", encoding="utf-8") as f:
            f.write(linha_txt + "\n")

        # Formato estruturado para .jsonl (JSON Lines)
        with open(self._arquivo_json, "a", encoding="utf-8") as f:
            f.write(json.dumps(entrada, ensure_ascii=False) + "\n")

    def debug(self, msg, **extras):
        self._registrar(Nivel.DEBUG, msg, **extras)

    def info(self, msg, **extras):
        self._registrar(Nivel.INFO, msg, **extras)

    def aviso(self, msg, **extras):
        self._registrar(Nivel.AVISO, msg, **extras)

    def erro(self, msg, **extras):
        self._registrar(Nivel.ERRO, msg, **extras)

    def ler_logs(self, nivel: Nivel = None) -> list:
        """Lê e filtra logs do arquivo JSONL."""
        if not self._arquivo_json.exists():
            return []

        logs = []
        # iteração linha a linha: arquivo de log é justamente o que cresce
        with open(self._arquivo_json, encoding="utf-8") as f:
            for linha in f:
                entrada = json.loads(linha)
                if nivel is None or entrada["nivel"] == nivel.value:
                    logs.append(entrada)
        return logs


logger = Logger("sistema")

logger.info("Aplicação iniciada", versao="2.1.0")
logger.debug("Conectando ao banco", host="localhost", porta=5432)
logger.info("Usuário autenticado", usuario_id=42)
logger.aviso("Tentativa de login falhou", tentativas=3)
logger.erro("Falha na conexão com o banco", codigo=500)

print("\n=== Logs de ERRO ===")
for log in logger.ler_logs(Nivel.ERRO):
    print(f"  {log['timestamp']} — {log['mensagem']}")

print(f"\n=== Total de logs: {len(logger.ler_logs())} ===")

Abrir, ler e gravar é a parte fácil, e o with resolve a maior parte do que costumava dar errado: o arquivo fecha mesmo quando o bloco levanta exceção. O que sobra são decisões que o Python não toma por você. A codificação é a primeira delas — sem encoding="utf-8", quem decide é o sistema, e o mesmo arquivo lido no Windows vira Relatório em vez de dar erro. A segunda é o tamanho: read() e read_text() carregam tudo de uma vez, e só a iteração linha a linha mantém a memória constante, o que importa justamente nos arquivos que crescem, como os de log. O pathlib junta caminho, verificação, leitura e escrita num só objeto, com a ressalva de que mkdir(exist_ok=True) só cria a última pasta; a árvore inteira pede também parents=True.

Nos formatos estruturados, o cuidado muda de lugar. O csv devolve tudo como texto, e converter na entrada é o que impede uma ordenação alfabética de passar por ranking de notas; o newline="" vale nos dois sentidos, ou uma quebra de linha dentro de um campo muda sem aviso. O json é o formato de troca, mas não faz ida e volta perfeita: chaves inteiras voltam como texto e tuplas voltam como listas. O pickle preserva qualquer objeto Python, e por isso mesmo só deve abrir arquivo de origem confiável, porque desserializar pode executar código. Por fim, o JSON Lines do exemplo de log mostra por que um objeto por linha funciona tão bem para registro: cada entrada nova é um append e a leitura pode parar em qualquer ponto.

Fontes e leituras recomendadas

Exercícios

Exercício 1

Um script lê notas.csv com csv.DictReader e publica no mural da escola o primeiro colocado, usando sorted(linhas, key=lambda l: l["nota"], reverse=True)[0]. O arquivo tem Ana com 9.5, Bruno com 10.0 e Carla com 7.0. O mural anuncia Ana. Não houve erro, e o script funcionou certo durante todo o semestre anterior. Explique o que aconteceu e por que só agora.

Ver resposta

✓ Resposta: O módulo csv não interpreta tipos: tudo o que ele devolve é texto, mesmo quando o arquivo foi escrito a partir de números. As notas lidas são '9.5', '10.0' e '7.0', e a ordenação de strings compara caractere a caractere. O primeiro caractere de '10.0' é '1', que vem antes de '7' e de '9', então em ordem decrescente a lista fica '9.5', '7.0', '10.0' — o 10 vai para o último lugar. No semestre anterior ninguém tirou 10, e com todas as notas entre '0.0' e '9.9', com um algarismo antes do ponto, a ordem alfabética coincide com a numérica. Por isso o defeito sempre esteve lá e só se manifestou quando apareceu a primeira nota de dois dígitos, que é exatamente a que mais importa num ranking. É o tipo de erro mais caro que existe, porque o resultado é plausível: Ana tem mesmo uma nota alta, e nada na tela sugere que falta alguém. A soma, ao contrário, teria falhado de imediato com TypeError. A correção é converter na entrada, num lugar só — nota = float(linha["nota"]) logo ao ler, tratando ali o campo vazio ou malformado —, e não espalhar float() por cada ponto do programa que usa o valor, onde um deles sempre acaba esquecido. Com key=lambda l: float(l["nota"]), o primeiro colocado passa a ser Bruno.

Exercício 2

Um programa que roda há anos num servidor Linux é instalado no notebook Windows de um cliente. Ele lê um arquivo de texto gerado pelo próprio sistema, com open("relatorio.txt"), sem outros argumentos. No Linux o relatório aparece correto; no Windows, a linha "Relatório de manutenção" sai como Relatório de manutenção, e ninguém vê mensagem de erro. Explique o que acontece e como impedir que o problema volte em outro ponto do código.

Ver resposta

✓ Resposta: Sem o argumento encoding, o open() em modo texto usa a codificação preferida do sistema, não uma codificação fixa. No servidor Linux ela é UTF-8, a mesma com que o arquivo foi gravado, e tudo bate. No Windows, com a configuração regional do Brasil, costuma ser cp1252. As letras acentuadas em UTF-8 ocupam dois bytes — o ó é C3 B3 —, e o cp1252 lê cada byte como um caractere separado: C3 vira à e B3 vira ³. Os bytes das minúsculas acentuadas do português têm todos um caractere definido em cp1252, e é por isso que não há erro: o programa segue adiante com o texto corrompido, e ele pode até ser gravado de volta assim. Pior, a falha é intermitente. O Á maiúsculo em UTF-8 é C3 81, e o byte 81 não tem caractere em cp1252: no dia em que o relatório trouxer "ÁREA" ou "Índice", o mesmo programa levanta UnicodeDecodeError: 'charmap' codec can't decode byte 0x81, e a investigação começa pelo lugar errado, porque "sempre funcionou". O caminho inverso é mais honesto: um arquivo gravado em cp1252 e lido como UTF-8 levanta UnicodeDecodeError: 'utf-8' codec can't decode byte 0xf3, porque o ó de um byte só não forma sequência UTF-8 válida. A correção pontual é open("relatorio.txt", encoding="utf-8"). Para não depender de lembrar em cada chamada, vale rodar a suíte de testes com python -X warn_default_encoding -W error::EncodingWarning, que transforma cada open() sem encoding em erro apontando a linha. O modo -X utf8, ou a variável PYTHONUTF8=1, também faz o UTF-8 valer como padrão, mas corrige apenas o ambiente onde foi configurado; o encoding explícito viaja com o código.

Exercício 3

Um programa guarda preferências do usuário em config.json. Ao salvar, faz with open("config.json", "w", encoding="utf-8") as f: json.dump(config, f). Numa versão nova, alguém guardou um set dentro de config. O salvamento levanta TypeError: Object of type set is not JSON serializable, o erro é capturado e registrado em log — e na próxima execução o programa não abre mais, porque não consegue ler a configuração. O que havia no arquivo, e como salvar de forma que uma falha nunca destrua a versão anterior?

Ver resposta

✓ Resposta: O modo "w" trunca o arquivo no momento em que ele é aberto, antes de qualquer escrita. A configuração antiga deixou de existir na linha do open. Depois, o json.dump escreve à medida que percorre o objeto, e só descobre o set quando chega nele. Se o config era {"a": {1, 2}}, o arquivo fica com {"a": — um JSON cortado ao meio, que o json.load da execução seguinte recusa. O with fechou o arquivo direitinho, como prometido, e fechar direitinho um arquivo pela metade não ajuda. A técnica segura é gravar num arquivo temporário na mesma pasta e só então trocá-lo pelo definitivo:

import json, os
from pathlib import Path

def salvar_json(caminho: Path, dados) -> None:
    tmp = caminho.with_suffix(caminho.suffix + ".tmp")
    try:
        with open(tmp, "w", encoding="utf-8") as f:
            json.dump(dados, f, ensure_ascii=False, indent=2)
            f.flush()
            os.fsync(f.fileno())
    except BaseException:
        tmp.unlink(missing_ok=True)
        raise
    os.replace(tmp, caminho)

Se a serialização falhar, a exceção sai antes do os.replace e o config.json original fica intacto; medido, ele continua com o conteúdo anterior. O os.replace substitui o destino numa operação só, e o fsync garante que os bytes chegaram ao disco antes da troca, o que protege também contra queda de energia no meio. O temporário precisa estar na mesma pasta (no mesmo sistema de arquivos), senão a troca deixa de ser atômica. Por fim, vale notar que o except que registrou o erro e seguiu em frente escondeu o problema até a execução seguinte: falha ao salvar configuração é o tipo de erro que merece chegar ao usuário.

Exercício 4

Uma API em Python guarda um cache de cotações em JSON: {1: 5.12, 2: 5.40}, onde a chave é o id da moeda. Depois de recarregar o cache do disco, cache[1] passa a levantar KeyError, embora o arquivo tenha as duas cotações. Na mesma revisão, alguém nota que as coordenadas salvas como tupla (lat, lon) voltam de outro jeito, e que um valor float("nan") vindo de um cálculo produziu um arquivo que o frontend em JavaScript se recusa a ler. Explique os três casos.

Ver resposta

✓ Resposta: Os três casos têm a mesma raiz: JSON tem menos tipos que Python, e a conversão não faz ida e volta perfeita. No primeiro, objeto JSON só aceita chave de texto. O json.dumps converte a chave inteira em string sem avisar, e json.loads(json.dumps({1: "a"})) devolve {'1': 'a'}. O cache recarregado tem as chaves '1' e '2', e cache[1] procura o inteiro, que não está lá. Tupla como chave, por sua vez, nem chega a ser convertida: levanta TypeError: keys must be str, int, float, bool or None, not tuple. No segundo, JSON não tem tupla, só array, então (1, 2) volta como a lista [1, 2]. Na maior parte do código isso passa despercebido, até a coordenada ser usada como chave de dicionário ou comparada com == a uma tupla, e [1, 2] == (1, 2) é False. No terceiro, o Python escreve NaN sem aspas por padrão, mas NaN não é JSON válido, e o JSON.parse do navegador rejeita o arquivo inteiro. A solução comum aos três é tratar o JSON como formato de fronteira, com conversão explícita nos dois sentidos: converter as chaves com {int(k): v for k, v in dados.items()} ao carregar, reconstruir a tupla onde o tipo importa e serializar com json.dumps(dados, allow_nan=False). Esse último levanta ValueError: Out of range float values are not JSON compliant no Python, onde o problema nasce, em vez de deixá-lo estourar no cliente. Datas seguem a mesma lógica: datetime nem serializa, e o costume é gravar isoformat() e reconverter ao ler.

Exercício 5

O Logger do exemplo deste artigo está em produção e o arquivo .jsonl do dia chega a 20 MB. Numa revisão de código, alguém propõe simplificar ler_logs assim: linhas = self._arquivo_json.read_text(encoding="utf-8").splitlines(), seguido do mesmo filtro. Avalie a proposta e aponte outros dois pontos do Logger que merecem atenção antes de ele crescer mais.

Ver resposta

✓ Resposta: A proposta é mais curta e troca memória constante por memória proporcional ao arquivo. Medido com tracemalloc num .jsonl de 200 mil linhas e 21 MB, a leitura com read_text().splitlines() atinge um pico de 49 MiB, porque a string inteira e a lista de linhas coexistem, enquanto a iteração com for linha in f fica em 146 KiB. Log é justamente o arquivo que só cresce, e a versão linha a linha é também a que permite parar cedo — buscar os dez últimos erros, por exemplo — sem ler o resto. A proposta deve ser recusada. O primeiro ponto de atenção é o próprio ler_logs: ele acumula as entradas em lista, e com filtro amplo o problema de memória volta pela porta dos fundos. Transformá-lo em gerador, com yield entrada no lugar do append, deixa o chamador decidir quanto consumir. Vale também tolerar uma linha corrompida (um processo morto no meio da escrita deixa a última linha pela metade) com try/except json.JSONDecodeError por linha, em vez de perder o arquivo inteiro na primeira falha. O segundo ponto é a escrita: cada registro abre e fecha dois arquivos, o que custa caro em volume alto, e a data do nome do arquivo é calculada só no __init__, de modo que um processo que atravessa a meia-noite continua gravando no arquivo do dia anterior. Para uso real, o módulo logging da biblioteca padrão já resolve rotação por data (TimedRotatingFileHandler) e concorrência entre threads, e um Formatter próprio produz o JSON Lines. O exemplo do artigo é bom para entender o formato; em produção, o logging é o ponto de partida.

Comentários

Mais em Python

Módulos, Pacotes e Organização de Projetos
Módulos, Pacotes e Organização de Projetos

Módulos, pacotes, ambientes virtuais e a estrutura de um projeto Python…

Dominando o Python
Dominando o Python

A série começa pelo terreno: o que Python é hoje, onde a linguagem de fato…

Templates HTML com Jinja2 e Deploy com Docker
Templates HTML com Jinja2 e Deploy com Docker

Jinja2 no Flask e no FastAPI, e o deploy com Docker e Compose: os filtros que…