Build CoreOrdered learning track

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.

12 min read2335 words
PrevNext
Lesson 1635 lesson track0719 Build Core
#python#ruff#linting#formatting+4 more

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.toml sebagai konfigurasi;
  • pre-commit hooks;
  • CI quality gate;
  • workflow lokal;
  • incremental adoption;
  • bagaimana menghindari tool-driven development.

Target setelah part ini:

  1. Memahami perbedaan formatter, linter, type checker, test runner.
  2. Menyiapkan Ruff.
  3. Menyiapkan pytest config.
  4. Menyiapkan pyright atau mypy.
  5. Membuat command quality gate.
  6. Menambahkan pre-commit.
  7. Menambahkan GitHub Actions CI.
  8. Menyusun policy untuk team.
  9. Menghindari over-tooling.
  10. Menerapkan semua ke case-tracker.

2. Tool Categories

Tool CategoryPertanyaan 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.


Untuk project Python modern kecil-menengah:

  1. ruff format
  2. ruff check
  3. pytest
  4. pyright atau mypy
  5. CI workflow menjalankan semua
  6. Optional: pre-commit
  7. 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:

PrefixMeaning
Epycodestyle errors
FPyflakes
Iimport sorting
Bflake8-bugbear
UPpyupgrade
SIMflake8-simplify
Npep8-naming
Sbandit-like security checks
PTpytest style
RETreturn-related checks
ARGunused arguments
PLPylint-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:

  1. Type all public functions.
  2. Type all domain models.
  3. Type all service functions.
  4. Type tests with -> None.
  5. Contain Any at boundaries.
  6. Avoid type: ignore unless explained.
  7. Use Protocol for dependencies.
  8. Use TypedDict/value objects at boundaries.
  9. Run type checker in CI.
  10. 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-audit for dependency vulnerabilities;
  • Bandit-like checks via Ruff S rules;
  • secret scanning in repository;
  • dependency pinning/locking;
  • license checks if needed.

For early case-tracker, security scanner is optional.

But baseline security mindset:

  • no eval on 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 run commands;
  • 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:

  1. Same commands as local.
  2. Fails fast enough.
  3. Deterministic.
  4. No hidden global state.
  5. Dependencies pinned/locked for real projects.
  6. Clear failure output.
  7. Runs on pull requests.
  8. Does not rely on developer machine.
  9. Separates quick checks and slow checks if needed.
  10. 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:

  1. Format check.
  2. Lint.
  3. Type check.
  4. Unit tests.
  5. Integration tests.
  6. 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:

  1. Add formatter first.
  2. Add linter with minimal rules.
  3. Add pytest command.
  4. Add type checking only for new files.
  5. Gradually tighten rules.
  6. Use per-file ignores temporarily.
  7. Track ignores as debt.
  8. Avoid giant formatting + behavior PR mixed.
  9. Avoid enabling strict mode globally if it blocks all work.
  10. 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.
Lesson Recap

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.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.