Python, de la zero Lecția 21 / 60

mypy / pyright + ruff: stiva de analiza statica

Type checking pentru a prinde bug-uri, ruff pentru formatare si linting si workflow-ul care le ruleaza pe ambele rapid.

Testele prind bug-uri la runtime. Analiza statică le prinde înainte ca codul să ruleze măcar, ideal la momentul salvării în editor. Sunt două întrebări separate la care o unealtă de analiză statică poate răspunde:

  1. Are sens codul ăsta dacă te uiți la type hints? (type checker-ul)
  2. Respectă codul ăsta stilul și evită greșelile frecvente? (linter-ul / formatter-ul)

În 2026, răspunsul la prima e pyright sau mypy, iar răspunsul la a doua e covârșitor ruff. Lecția asta e despre cum le configurezi pe toate trei, cum arată configurările lor și ce eșecuri vei vedea efectiv.

De ce merită type checking-ul

Am acoperit type hints în lecția 2: def add(a: int, b: int) -> int. Hints sunt no-op-uri la runtime, Python le ignoră. Contează doar atunci când o unealtă le citește și îți spune că ceva nu se potrivește.

Cum arată „nu se potrivește”? Exemple reale din codebase-uri reale:

  • O funcție adnotată -> User returnează None pe calea de eroare. Testele n-au prins-o pentru că ramura de eroare era rară. Type checker-ul, da.
  • Un refactor a redenumit un parametru din user_id în uid în trei locuri din patru. Al patrulea apelant încă trimite user_id=. Fără tipuri, testele la runtime în CI o prind. Cu tipuri, sublinierea din editor o prinde în momentul în care salvezi.
  • O funcție primește list[str] și cineva îi pasează tuple[str, str]. „Funcționează” până când cineva apelează .append.

Type checker-ele găsesc această clasă de bug-uri instantaneu, pe fiecare fișier, la fiecare salvare. Te costă timpul necesar adăugării hints-urilor și nu prea mai mult.

pyright vs mypy

Două type checkere principale în 2026:

pyright este al Microsoft, scris în TypeScript. Rapid (poate reverifica un fișier mare în zeci de milisecunde), alimentează implicit extensia Pylance din VS Code, are inferența cea mai agresivă. Implicit pentru proiectele noi.

mypy este implementarea de referință, scrisă în Python de echipa care a împins PEP 484. Mai lent decât pyright, dar mai configurabil, cu o coadă lungă de butoane pentru cazuri-limită și o bază instalată uriașă. Încă standardul în multe companii mari.

Le poți rula pe amândouă. Uneori sunt în dezacord pe cazuri-limită, iar acel dezacord e uneori util ca semnal că în cod se petrece ceva neobișnuit. Pentru un proiect nou, alege pyright; pentru un codebase existent care deja rulează mypy, păstrează mypy.

Setarea lui pyright

Instalare:

uv add --dev pyright

Configurare în pyproject.toml:

[tool.pyright]
include = ["src", "tests"]
exclude = ["**/__pycache__", "**/.venv"]
pythonVersion = "3.13"
typeCheckingMode = "standard"
reportMissingImports = "error"
reportMissingTypeStubs = "warning"

typeCheckingMode e butonul cel mare:

  • off: doar verificări sintactice, fără inferență reală de tipuri.
  • basic: ușor, prinde lucrurile evidente.
  • standard: noul implicit din 2025; severitate rezonabilă pentru majoritatea proiectelor.
  • strict: semnalează fiecare funcție netipată, fiecare Any, fiecare Optional implicit. Fantastic pentru cod nou; dureros la retrofitare.

Rulare:

pyright

Output-ul arată cam așa:

src/my_app/users.py:42:5 - error: Argument of type "str | None" cannot be assigned to parameter "name" of type "str" in function "create_user"
  Type "str | None" is not assignable to type "str"
    "None" is not assignable to "str"

Asta e type checker-ul care îți spune că o valoare care ar putea fi None e pasată acolo unde None nu e permis. Soluția e fie să o îngustezi înainte de apel, fie să schimbi parametrul în str | None.

Setarea lui mypy

Instalare:

uv add --dev mypy

Configurare:

[tool.mypy]
python_version = "3.13"
files = ["src", "tests"]
strict = true
warn_return_any = true
warn_unused_ignores = true

# Suprascrieri per-modul - biblioteci third-party fara type stubs
[[tool.mypy.overrides]]
module = ["legacy_lib.*", "old_internal.*"]
ignore_missing_imports = true

strict = true e prescurtarea pentru vreo zece flag-uri individuale (disallow_untyped_defs, disallow_any_generics, warn_redundant_casts, …). Flag-urile granulare există dacă ai nevoie să faci opt-out pentru un modul anume.

Rulare:

mypy src/

Formatul output-ului e similar cu cel al lui pyright: fișier, linie, coloană, cod de eroare, mesaj.

Erori frecvente și soluții

Câteva pe care le vei vedea constant.

Optional neîngustat:

def greet(name: str | None) -> str:
    return f"Hello {name.upper()}"  # error: name might be None

Soluție: îngustează întâi.

def greet(name: str | None) -> str:
    if name is None:
        return "Hello stranger"
    return f"Hello {name.upper()}"

Tip de retur care nu se potrivește:

def find_user(uid: int) -> User:
    user = db.get(uid)
    return user  # error: db.get returns User | None, declared User

Soluție: schimbă semnătura în -> User | None sau aruncă o excepție pe cazul lipsă.

Drift de semnătură după redenumire:

def create_user(name: str, email: str) -> User: ...

create_user(name="Ada", mail="ada@example.com")
# error: unexpected keyword argument "mail"; did you mean "email"?

Ăsta e tipul de bug care plătește singur type checking-ul. Testarea pură la runtime îl prinde doar dacă un test exersează exact acel call site. Type checker-ul îl prinde instantaneu.

Otrăvirea cu Any:

import json

config: dict = json.load(open("config.json"))  # config inferred as dict[Any, Any]
port: str = config["port"]  # no error, but config["port"] is actually int

Any se potrivește cu orice, ceea ce înseamnă că dezactivează type checking-ul. Flag-urile stricte avertizează când Any intră în codul tău neadnotat. Repară fie dând load-ului un tip real, fie folosind un parser ca Pydantic care returnează un model tipat.

Integrarea în CI

Ambele unelte ies non-zero pe erori, ceea ce e tot ce-ți trebuie:

# .github/workflows/ci.yml
- run: uv run pyright
- run: uv run pytest

Pentru mypy:

- run: uv run mypy src/

Într-un codebase mare, configurează mypy --install-types sau caching prin pre-commit ca să-l ții rapid. Pyright e suficient de rapid din start încât să-l rulezi la fiecare push fără să te gândești.

Intră ruff: formatter-ul și linter-ul

Până în 2023, lanțul de unelte de stil din Python arăta așa: black pentru formatare, isort pentru sortarea import-urilor, flake8 pentru linting (cu cinci plugin-uri), pylint dacă voiai verificări mai profunde. Patru unelte, patru config-uri, patru invocări. Toate scrise în Python și toate erau lente.

ruff le-a înlocuit pe aproape toate. Scris în Rust de echipa Astral (aceiași oameni din spatele lui uv), rulează echivalentul lui black + isort + flake8 + cea mai mare parte din pylint în milisecunde pe un proiect tipic. Începând cu 2026, e implicit de facto pentru lucrul nou în Python.

Instalare:

uv add --dev ruff

Două comenzi:

ruff check .       # lint
ruff format .      # format (inlocuitorul lui black)

Asta e tot. Fără să mai ții minte care unealtă face ce.

Configurarea lui ruff

Tot în pyproject.toml:

[tool.ruff]
line-length = 100
target-version = "py313"
src = ["src", "tests"]

[tool.ruff.lint]
# Familii de reguli de activat
select = [
    "E",      # pycodestyle errors
    "W",      # pycodestyle warnings
    "F",      # pyflakes
    "I",      # isort (import order)
    "N",      # pep8-naming
    "UP",     # pyupgrade (modernize syntax)
    "B",      # flake8-bugbear (likely bugs)
    "C4",     # flake8-comprehensions
    "SIM",    # flake8-simplify
    "RUF",    # ruff's own rules
]
ignore = [
    "E501",   # line too long (formatter handles this)
]

[tool.ruff.lint.per-file-ignores]
"tests/**" = ["N802", "N803"]    # pytest test/fixture names

[tool.ruff.format]
quote-style = "double"
indent-style = "space"

Limbajul de selectare a regulilor e funcția-killer. Fiecare prefix de două sau trei litere e o familie de reguli: E e pycodestyle, F e pyflakes, B e bugbear, UP modernizează sintaxa veche. Selectezi familii întregi ("B"), sub-reguli specifice ("B008") sau totul dintr-o unealtă ("ALL", apoi ignori ce nu vrei).

Un set practic de pornire e ["E", "W", "F", "I", "B", "UP"]. Adaugă "SIM" și "C4" odată ce te-ai obișnuit cu ele.

Ce prinde efectiv ruff

Exemple din output real ruff check:

def make_widgets(items=[]):  # B006: mutable default argument
    items.append(...)
    return items

import os, sys  # E401: multiple imports on one line

x = lambda: 5   # E731: do not assign a lambda; def is clearer

if x == None:   # E711: use `is None`

for i in range(len(things)):  # B007 / SIM-style: enumerate is cleaner
    print(i, things[i])

import json
import json  # F811: redefinition of unused import

Cele mai multe au reparare automată:

ruff check --fix .

Ruff va rescrie fișierul. Flag-ul --unsafe-fixes activează reparări care schimbă comportamentul; revizuiește diff-ul înainte să comiti.

ruff format: înlocuitorul lui black

ruff format e byte-compatibil cu output-ul lui black pentru >99% din input-uri (echipa Astral urmărește explicit divergențele). Dacă proiectul tău folosește black, îl poți schimba cu ruff format și diff-ul va fi gol.

ruff format .          # reformateaza pe loc
ruff format --check .  # mod CI: iese cu 1 daca s-ar schimba ceva

Integrarea cu pre-commit hook

Pattern-ul standard e să rulezi ruff (și type checker-ul) înainte de fiecare commit. Cu pre-commit:

# .pre-commit-config.yaml
repos:
  - repo: https://github.com/astral-sh/ruff-pre-commit
    rev: v0.7.0
    hooks:
      - id: ruff
        args: [--fix]
      - id: ruff-format

  - repo: https://github.com/RobertCraigie/pyright-python
    rev: v1.1.380
    hooks:
      - id: pyright

Apoi:

uv run pre-commit install

Acum git commit rulează ruff și pyright pe fișierele schimbate și refuză commit-urile care nu trec. Combină cu pytest în CI și ai trei straturi de protecție: sublinieri în editor, poartă pre-commit, rulare completă în CI.

Punând totul împreună

O secțiune [tool.*] completă din pyproject.toml pentru un proiect Python modern, în 2026:

[tool.pyright]
include = ["src", "tests"]
pythonVersion = "3.13"
typeCheckingMode = "standard"

[tool.ruff]
line-length = 100
target-version = "py313"

[tool.ruff.lint]
select = ["E", "W", "F", "I", "N", "B", "UP", "C4", "SIM", "RUF"]
ignore = ["E501"]

[tool.ruff.format]
quote-style = "double"

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra --strict-markers"

Trei unelte, un fișier de configurare, un mediu virtual prin uv. Toată bucla, ruff format, ruff check, pyright, pytest, rulează pe un proiect mic tipic în câteva secunde în total. Asta e diferența dintre a prinde bug-uri la tastatură și a le prinde în producție.

Stiva de analiză statică și stiva de teste sunt complementare: analiza statică îți spune că codul are sens cu sine însuși, testele îți spun că face ce ai intenționat. Modulul 4 a acoperit ambele. Modulul următor: workflow-uri augmentate cu AI, locul în care 2026 arată cu adevărat diferit față de 2022.

Citații: pyright documentation, mypy documentation, ruff documentation, pytest documentation. Consultat 2026-05-01.

Caută