Code Quality Tooling: Ruff, Formatting, Linting, Type Checking, CI
Part 016 — Code Quality Tooling: Ruff, Formatting, Linting, Type Checking, CI
Membahas quality tooling Python modern: Ruff formatter/linter, import sorting, pytest, mypy/pyright, pyproject.toml, pre-commit, GitHub Actions CI, quality gates, dan workflow team.
Part 016 — Code Quality Tooling: Ruff, Formatting, Linting, Type Checking, CI
1. Tujuan Part Ini
Tooling bukan pengganti engineering judgment. Tetapi tooling yang baik mengurangi noise dan mempercepat feedback.
Tanpa quality tooling, tim menghabiskan energi untuk:
- style debate;
- import order;
- unused import;
- formatting;
- typo sederhana;
- bug pattern yang bisa dideteksi statis;
- test command yang berbeda-beda;
- “works on my machine”;
- PR review yang fokus ke hal mekanis;
- refactor tanpa safety net.
Dengan tooling yang baik, loop berubah:
edit -> format -> lint -> type check -> test -> commit -> CI
Part ini membahas quality tooling untuk Python modern:
- Ruff sebagai formatter dan linter;
- pytest sebagai test runner;
- mypy atau pyright sebagai type checker;
pyproject.tomlsebagai konfigurasi;- pre-commit hooks;
- CI quality gate;
- workflow lokal;
- incremental adoption;
- bagaimana menghindari tool-driven development.
Target setelah part ini:
- Memahami perbedaan formatter, linter, type checker, test runner.
- Menyiapkan Ruff.
- Menyiapkan pytest config.
- Menyiapkan pyright atau mypy.
- Membuat command quality gate.
- Menambahkan pre-commit.
- Menambahkan GitHub Actions CI.
- Menyusun policy untuk team.
- Menghindari over-tooling.
- Menerapkan semua ke
case-tracker.
2. Tool Categories
| Tool Category | Pertanyaan yang Dijawab |
|---|---|
| Formatter | “Apakah layout kode konsisten?” |
| Linter | “Apakah ada pattern yang mencurigakan?” |
| Import sorter | “Apakah import tertata?” |
| Type checker | “Apakah type contract konsisten?” |
| Test runner | “Apakah behavior sesuai expectation?” |
| Coverage | “Bagian mana yang dieksekusi test?” |
| Security scanner | “Apakah ada pattern/dependency risk?” |
| CI | “Apakah checks berjalan konsisten di remote?” |
| Pre-commit | “Apakah checks dijalankan sebelum commit?” |
Tool tidak menggantikan review desain. Tool menurunkan biaya menjaga standar minimum.
3. Recommended Baseline
Untuk project Python modern kecil-menengah:
ruff formatruff checkpytestpyrightataumypy- CI workflow menjalankan semua
- Optional: pre-commit
- Optional later: coverage, security audit
Minimal commands:
python -m ruff format .
python -m ruff check .
python -m pytest
pyright
Atau dengan mypy:
python -m mypy src tests
Jika memakai uv:
uv run ruff format .
uv run ruff check .
uv run pytest
uv run pyright
4. pyproject.toml as Tooling Hub
pyproject.toml bisa menyimpan config banyak tool.
Example:
[project]
name = "case-tracker"
version = "0.1.0"
description = "A small CLI case tracker for learning Python engineering fundamentals."
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[dependency-groups]
dev = [
"pytest",
"ruff",
"mypy",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = [
"-ra",
]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = [
"E",
"F",
"I",
"B",
"UP",
"SIM",
]
ignore = []
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
[tool.mypy]
python_version = "3.12"
mypy_path = "src"
strict = true
Adjust based on team and baseline Python version.
5. Formatter
Formatter automatically formats code.
With Ruff:
python -m ruff format .
Check without modifying:
python -m ruff format --check .
Formatter should be non-negotiable. Humans should not debate whitespace in PR.
5.1 Formatter Philosophy
Good formatter policy:
- one formatter;
- same version across team/CI;
- run locally and in CI;
- avoid style bikeshedding;
- accept formatter output unless readability severely harmed.
If formatter fights readability, restructure code rather than manually formatting weirdly.
6. Linter
Linter finds suspicious code patterns.
Ruff check:
python -m ruff check .
Auto-fix safe issues:
python -m ruff check . --fix
Example issues:
- unused import;
- undefined name;
- import order;
- bugbear warnings;
- unnecessary comprehension;
- outdated syntax;
- simplification opportunities;
- style issues.
6.1 Ruff Rule Families
Common selected prefixes:
| Prefix | Meaning |
|---|---|
E | pycodestyle errors |
F | Pyflakes |
I | import sorting |
B | flake8-bugbear |
UP | pyupgrade |
SIM | flake8-simplify |
N | pep8-naming |
S | bandit-like security checks |
PT | pytest style |
RET | return-related checks |
ARG | unused arguments |
PL | Pylint-inspired checks |
Do not enable all rules blindly. Start with high-signal rules.
Recommended initial:
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP", "SIM"]
Add more as team matures.
7. Import Sorting with Ruff
Ruff can sort imports with I.
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP", "SIM"]
Run:
python -m ruff check . --fix
Before:
from case_tracker.domain import Case
import json
from pathlib import Path
After:
import json
from pathlib import Path
from case_tracker.domain import Case
Import sorting reduces review noise and makes dependency groups visible.
8. Type Checking
Pick one main type checker:
- mypy;
- pyright.
Both can be good. Avoid arguing tool identity too early. The important thing is to run one consistently.
8.1 mypy
Install:
python -m pip install mypy
Run:
python -m mypy src tests
Config:
[tool.mypy]
python_version = "3.12"
mypy_path = "src"
strict = true
Strict may be intense for legacy. For new small project, it teaches good habits.
8.2 pyright
Install via npm, package manager, editor, or Python wrapper depending team workflow.
Basic pyrightconfig.json:
{
"include": ["src", "tests"],
"typeCheckingMode": "strict",
"pythonVersion": "3.12"
}
Run:
pyright
8.3 Which One?
For learning:
- mypy integrates naturally with Python packaging workflows;
- pyright is fast and popular in editor workflows;
- either is fine.
Use one consistently.
9. Type Checker Policy
Recommended:
- Type all public functions.
- Type all domain models.
- Type all service functions.
- Type tests with
-> None. - Contain
Anyat boundaries. - Avoid
type: ignoreunless explained. - Use Protocol for dependencies.
- Use TypedDict/value objects at boundaries.
- Run type checker in CI.
- Tighten strictness gradually.
Example type: ignore policy:
result = third_party_weird_call() # type: ignore[no-untyped-call] # upstream package lacks types
Do not write:
# type: ignore
without reason.
10. pytest Configuration
In pyproject.toml:
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = [
"-ra",
]
markers = [
"integration: tests that touch real infrastructure boundary",
"contract: shared behavior tests for implementations",
]
Run all:
python -m pytest
Run non-integration:
python -m pytest -m "not integration"
-ra shows summary info for skipped/failed/xfailed tests.
11. Coverage
Coverage is useful but not the first tool.
Install:
python -m pip install pytest-cov
Run:
python -m pytest --cov=case_tracker --cov-report=term-missing
Config:
[tool.coverage.run]
branch = true
source = ["case_tracker"]
[tool.coverage.report]
show_missing = true
skip_covered = true
Coverage helps reveal untested areas. It does not prove correctness.
Policy:
- focus on critical domain/failure paths;
- avoid arbitrary 100% goal early;
- do not write meaningless tests to satisfy percentage;
- use coverage as map, not trophy.
12. Security and Dependency Checks
Later, consider:
pip-auditfor dependency vulnerabilities;- Bandit-like checks via Ruff
Srules; - secret scanning in repository;
- dependency pinning/locking;
- license checks if needed.
For early case-tracker, security scanner is optional.
But baseline security mindset:
- no
evalon user input; - no shell command with interpolated input;
- no logging secrets;
- no pickle for untrusted data;
- dependency minimal.
13. Pre-Commit Hooks
Pre-commit runs checks before commit.
Install:
python -m pip install pre-commit
Config .pre-commit-config.yaml:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.0
hooks:
- id: ruff-format
- id: ruff
args: [--fix]
Use actual current Ruff hook version in a real project. Pin versions intentionally.
Install hooks:
pre-commit install
Run all files:
pre-commit run --all-files
13.1 Pre-Commit Policy
Good:
- fast checks;
- formatter;
- linter auto-fixes;
- no network-heavy tests;
- no slow integration tests.
CI still required. Pre-commit is convenience, not source of truth.
14. Local Quality Script
Create a script or Makefile.
Makefile:
.PHONY: format lint type test check
format:
python -m ruff format .
lint:
python -m ruff check .
type:
python -m mypy src tests
test:
python -m pytest
check:
python -m ruff format --check .
python -m ruff check .
python -m mypy src tests
python -m pytest
If using pyright:
type:
pyright
If Windows team lacks make, use:
just;nox;tox;- PowerShell script;
uv runcommands;- documented commands in README.
The important part: one obvious quality command.
15. Nox and Tox Preview
tox and nox help run test sessions across Python versions/environments.
For early project, not necessary.
Use them when:
- supporting multiple Python versions;
- testing package install behavior;
- complex CI matrix;
- docs/lint/test sessions need isolation;
- library distribution.
Do not add tox/nox before problem exists.
16. CI with GitHub Actions
Example .github/workflows/ci.yml:
name: CI
on:
push:
pull_request:
jobs:
quality:
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install project and dev dependencies
run: |
python -m pip install --upgrade pip
python -m pip install -e .
python -m pip install pytest ruff mypy
- name: Check formatting
run: python -m ruff format --check .
- name: Lint
run: python -m ruff check .
- name: Type check
run: python -m mypy src tests
- name: Test
run: python -m pytest
If using pyright, install/run pyright accordingly.
If using uv, CI can be faster and lockfile-aware, but foundation above is clear.
17. CI Principles
Good CI:
- Same commands as local.
- Fails fast enough.
- Deterministic.
- No hidden global state.
- Dependencies pinned/locked for real projects.
- Clear failure output.
- Runs on pull requests.
- Does not rely on developer machine.
- Separates quick checks and slow checks if needed.
- Protects main branch.
CI is not where you discover basic formatting issues for the first time. Local tooling should catch them earlier.
18. Quality Gate Order
Recommended order:
- Format check.
- Lint.
- Type check.
- Unit tests.
- Integration tests.
- Coverage/security optional.
Why?
- format/lint fail fast;
- type check catches static contract issues;
- tests run after cheap checks;
- integration tests may be slower.
For small project, order does not matter much. For large CI, ordering saves time.
19. Incremental Adoption for Existing Codebase
If codebase is legacy:
- Add formatter first.
- Add linter with minimal rules.
- Add pytest command.
- Add type checking only for new files.
- Gradually tighten rules.
- Use per-file ignores temporarily.
- Track ignores as debt.
- Avoid giant formatting + behavior PR mixed.
- Avoid enabling strict mode globally if it blocks all work.
- Create ratchet policy: no new violations.
Do not try to “fix everything” in one heroic PR.
20. Avoiding Tool Fatigue
Too many tools can harm productivity.
Bad early stack:
black + isort + flake8 + pylint + pyupgrade + bandit + mypy + pyright + pyre + tox + nox + pre-commit + custom scripts
Some teams need many tools. Beginners do not.
Start:
ruff format
ruff check
pytest
one type checker
Add only when there is a clear gap.
21. Ruff vs Black/isort/Flake8
Historically, Python projects often used:
- Black for formatting;
- isort for import sorting;
- Flake8 for linting;
- pyupgrade for syntax modernization;
- plugins for bugbear, simplify, etc.
Ruff consolidates many of these checks and includes a formatter. That reduces toolchain complexity.
But some teams still use Black/isort/Flake8 due to existing policy. The core principles remain:
- one formatter policy;
- one import order policy;
- one lint gate;
- consistent CI.
For new project, Ruff-only baseline is reasonable.
22. Editor Integration
Editor should use same tools as CLI.
VS Code:
- select
.venv; - enable Ruff extension;
- format on save;
- configure type checker;
- run tests in project environment.
PyCharm:
- use project interpreter;
- configure pytest;
- enable Ruff external tool/plugin if desired;
- configure mypy/pyright if used.
Avoid:
- editor using global Python;
- editor formatter different from CI formatter;
- test runner using wrong working directory;
- hidden settings not documented in repo.
Repo config should be source of truth.
23. Quality Tooling in case-tracker
Recommended files:
case-tracker/
pyproject.toml
README.md
Makefile
.pre-commit-config.yaml
.github/
workflows/
ci.yml
src/
tests/
Minimum README.md quality section:
## Quality Checks
```bash
python -m ruff format --check .
python -m ruff check .
python -m mypy src tests
python -m pytest
To auto-format:
python -m ruff format .
python -m ruff check . --fix
---
## 24. Example `pyproject.toml` for `case-tracker`
```toml
[project]
name = "case-tracker"
version = "0.1.0"
description = "A small CLI case tracker for learning Python engineering fundamentals."
readme = "README.md"
requires-python = ">=3.12"
dependencies = []
[project.scripts]
case-tracker = "case_tracker.cli:main"
[dependency-groups]
dev = [
"pytest",
"ruff",
"mypy",
]
[tool.pytest.ini_options]
testpaths = ["tests"]
pythonpath = ["src"]
addopts = ["-ra"]
markers = [
"integration: tests that touch real infrastructure boundary",
"contract: shared behavior tests for implementations",
]
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP", "SIM", "PT"]
ignore = []
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
[tool.mypy]
python_version = "3.12"
mypy_path = "src"
strict = true
warn_return_any = true
warn_unused_configs = true
Adjust if using pyright.
25. Example pyrightconfig.json
{
"include": ["src", "tests"],
"typeCheckingMode": "strict",
"pythonVersion": "3.12",
"reportMissingTypeStubs": false
}
Choose mypy or pyright as primary. Running both can be useful in some teams, but usually not necessary early.
26. Example Makefile with uv Option
If using plain Python:
.PHONY: format lint type test check
format:
python -m ruff format .
lint:
python -m ruff check .
type:
python -m mypy src tests
test:
python -m pytest
check:
python -m ruff format --check .
python -m ruff check .
python -m mypy src tests
python -m pytest
If using uv:
.PHONY: format lint type test check
format:
uv run ruff format .
lint:
uv run ruff check .
type:
uv run mypy src tests
test:
uv run pytest
check:
uv run ruff format --check .
uv run ruff check .
uv run mypy src tests
uv run pytest
Do not include both if it confuses the team. Pick one workflow.
27. Handling Lint Exceptions
Sometimes rule violation is intentional.
Prefer local narrow ignore:
def callback(unused_argument: object) -> None: # noqa: ARG001
...
Better with explanation:
def callback(unused_argument: object) -> None: # noqa: ARG001 - callback signature required by framework
...
Avoid broad file-level ignores.
Bad:
# ruff: noqa
Use only for generated files or special cases.
28. Handling Type Ignores
Good:
result = third_party_call() # type: ignore[no-untyped-call] # third-party package has no stubs
Bad:
result = third_party_call() # type: ignore
Policy:
- ignore specific error code;
- explain why;
- isolate around boundary;
- revisit periodically.
29. Quality Tooling and Code Review
After tooling, code review should focus on:
- domain correctness;
- failure semantics;
- architecture boundaries;
- naming;
- test quality;
- operational risk;
- security;
- performance assumptions;
- maintainability.
Not:
- whitespace;
- import order;
- line wrapping;
- unused imports;
- obvious typing mistakes.
Tooling frees humans for judgment.
30. Quality Gate Is Not Enough
Code can pass all checks and still be bad.
Example:
def process_case(case: Case) -> None:
if case.status.value == "DRAFT":
case.status = CaseStatus.CLOSED
It can pass formatter, linter, type checker, and maybe tests if missing.
But domain rule is wrong.
Tooling catches mechanical issues. Tests catch expected behavior. Review catches judgment.
31. Practice: Add Ruff
Install:
python -m pip install ruff
Add config:
[tool.ruff]
line-length = 100
target-version = "py312"
[tool.ruff.lint]
select = ["E", "F", "I", "B", "UP", "SIM"]
Run:
python -m ruff check .
python -m ruff format .
Intentionally add unused import and watch Ruff catch it.
32. Practice: Add Type Checker
With mypy:
python -m pip install mypy
python -m mypy src tests
Add:
[tool.mypy]
python_version = "3.12"
mypy_path = "src"
strict = true
Fix issues:
- missing annotations;
- optional handling;
- Any leakage;
- wrong return type.
Or with pyright:
pyright
Using config:
{
"include": ["src", "tests"],
"typeCheckingMode": "strict",
"pythonVersion": "3.12"
}
33. Practice: Add CI
Create .github/workflows/ci.yml.
Use workflow from section 16.
Then push branch and confirm:
- formatting check fails when code unformatted;
- lint fails on unused import;
- tests fail on broken behavior;
- type check fails on optional misuse.
CI should prove quality gate is real.
34. Practice: Add Pre-Commit
Create .pre-commit-config.yaml.
Example:
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.0.0
hooks:
- id: ruff-format
- id: ruff
args: [--fix]
Replace v0.0.0 with pinned current version in a real repository.
Run:
pre-commit install
pre-commit run --all-files
Observe changed files.
Commit only after reviewing changes.
35. Practice: Quality README
Add:
## Development Workflow
Run tests:
```bash
python -m pytest
Format:
python -m ruff format .
Lint:
python -m ruff check .
Type check:
python -m mypy src tests
Full check:
make check
README should help future you.
---
## 36. Self-Check
Jawab tanpa melihat materi:
1. Apa beda formatter dan linter?
2. Apa yang dicek type checker?
3. Kenapa test runner berbeda dari type checker?
4. Kenapa Ruff bisa menyederhanakan toolchain?
5. Apa command format Ruff?
6. Apa command lint Ruff?
7. Kenapa import sorting penting?
8. Apa rule families awal yang high-signal?
9. Kapan menambah rule baru?
10. Kapan memakai mypy?
11. Kapan memakai pyright?
12. Apa policy `type: ignore` yang sehat?
13. Apa fungsi pre-commit?
14. Kenapa CI tetap wajib meski ada pre-commit?
15. Apa isi minimal GitHub Actions CI?
16. Apa itu quality gate?
17. Kenapa coverage bukan bukti correctness?
18. Bagaimana incremental adoption di legacy codebase?
19. Apa tanda over-tooling?
20. Apa yang tetap harus dinilai manusia saat review?
---
## 37. Definition of Done Part 016
Kamu selesai part ini jika bisa:
1. Menjelaskan formatter/linter/type checker/test runner.
2. Menginstall dan menjalankan Ruff.
3. Mengatur Ruff di `pyproject.toml`.
4. Menjalankan `ruff format`.
5. Menjalankan `ruff check`.
6. Mengaktifkan import sorting.
7. Menginstall dan menjalankan mypy atau pyright.
8. Menambahkan pytest config.
9. Membuat Makefile atau quality command.
10. Menulis GitHub Actions CI.
11. Menambahkan pre-commit config.
12. Menjelaskan ignore policy.
13. Menjelaskan incremental adoption.
14. Menghindari over-tooling.
15. Menjelaskan batas tooling dibanding engineering judgment.
---
## 38. Ringkasan
Quality tooling membuat feedback loop cepat dan konsisten.
Inti part ini:
- formatter menghilangkan style debate;
- linter menangkap pattern mencurigakan;
- type checker memberi static reasoning;
- pytest memverifikasi behavior;
- Ruff bisa menyatukan formatting, linting, import sorting, dan banyak rule;
- mypy/pyright membantu contract typing;
- `pyproject.toml` menjadi pusat konfigurasi;
- pre-commit membantu sebelum commit;
- CI adalah source of truth remote;
- quality gate harus sama antara lokal dan CI;
- legacy adoption harus incremental;
- tooling harus mendukung judgment, bukan menggantikannya.
Part berikutnya akan masuk ke packaging, dependency management, dan reproducible builds: bagaimana Python project didefinisikan, diinstall, dikunci, dan dibagikan secara benar.
---
## 39. Referensi
- Ruff Documentation — Linter and formatter.
- Ruff Documentation — Formatter.
- pytest Documentation — Configuration and invocation.
- mypy Documentation — Configuration file and command line.
- Pyright Documentation — Configuration.
- Python Packaging User Guide — `pyproject.toml`.
- pre-commit Documentation.
- GitHub Actions Documentation — Python workflow.
You just completed lesson 16 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.