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:
- Are sens codul ăsta dacă te uiți la type hints? (type checker-ul)
- 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ă
-> UserreturneazăNonepe 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înuidîn trei locuri din patru. Al patrulea apelant încă trimiteuser_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ă, fiecareAny, fiecareOptionalimplicit. 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.