Chapter 50, Advanced
Quality and automation
What is not checked by a machine is forgotten by a human. This chapter sets up the minimum set of checks that I put in any project.
Code for this chapter: avancado/cap50_qualidade.py
Linting and formatting with ruff
The ruff combines linter and formatter into a single, very fast tool. The linter finds errors and suspicious patterns. The formatter standardizes the style, which ends discussions about commas in code review. See code with real issues:
import os, sys
def adicionar(item, lista=[]):
try:
lista.append(item)
except:
pass
return lista
uv run ruff check exemplos/qualidade/antes.py --output-format concise
exemplos/qualidade/antes.py:1:1: E401 [*] Multiple imports on one line
exemplos/qualidade/antes.py:1:1: I001 [*] Import block is un-sorted or un-formatted
exemplos/qualidade/antes.py:1:8: F401 [*] `os` imported but unused
exemplos/qualidade/antes.py:1:12: F401 [*] `sys` imported but unused
exemplos/qualidade/antes.py:4:27: B006 Do not use mutable data structures for argument defaults
exemplos/qualidade/antes.py:7:5: E722 Do not use bare `except`
Found 6 errors.
[*] 4 fixable with the `--fix` option (1 hidden fix can be enabled with the `--unsafe-fixes` option).
Ruff pointed out six findings: two unused imports, imports on the same line and out of order, a mutable default value (the bug from chapter 18), and a except that swallows any error. The four marked with [*] it corrects automatically with --fix. The other two, the mutable value and the except empty one, require a decision from you, because they change behavior. The corrected version:
def adicionar(item, lista=None):
if lista is None:
lista = []
lista.append(item)
return lista
assert adicionar(1) == [1]
assert adicionar(2) == [2]
print("sem os problemas apontados")
sem os problemas apontados
Configuration in one place
I explicitly choose the rule groups, so that the configuration is a decision and not the tool's default. The mypy stays in the same file:
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP"]
[tool.mypy]
python_version = "3.12"
strict = true
files = ["src"]
| Group | What it checks |
|---|---|
E | Style and syntax errors (pycodestyle) |
F | Simple logical errors, such as unused names (pyflakes) |
I | Order of imports |
B | Probable bug traps (flake8-bugbear) |
UP | Old syntax that can be modernized |
Before the commit: pre-commit
The pre-commit runs the checks at commit time, and prevents the problem from reaching the repository. Install with uv tool install pre-commit and activate with pre-commit install:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.12.0 # atualize com: pre-commit autoupdate
hooks:
- id: ruff-check
args: [--fix]
- id: ruff-format
After the push: continuous integration
The pre-commit is a local convenience and can be skipped. The guarantee is the CI, which runs in a clean environment on every push and pull request. This GitHub Actions flow uses uv and tests three versions of Python:
name: ci
on:
push:
branches: [main]
pull_request:
jobs:
qualidade:
runs-on: ubuntu-latest
strategy:
matrix:
python: ["3.12", "3.13", "3.14"]
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v6
with:
python-version: ${{ matrix.python }}
- run: uv sync --locked
- run: uv run ruff check .
- run: uv run ruff format --check .
- run: uv run mypy
- run: uv run pytest -q
Check the versions of the actions
The versions of the actions (
actions/checkout,astral-sh/setup-uv) evolve. Before copying this file, check the latest version of each one on the action's own page. The flow format changes little, but the tags change.
Coverage
Coverage measures which lines the tests execute. It tells what does not is tested, and not that what is tested is correct. I use it to discover gaps, and not as a numerical goal:
uv add --dev pytest-cov
uv run pytest --cov=calc_notes --cov-report=term-missing
Exercise 1
Regression test
For the function adicionar corrected, write a test that would fail in the version with a mutable default value.