Bun venit la lecția a doua. Sărim peste prologul „hello world” cu care încep majoritatea tutorialelor Python, fiindcă dacă citești lecția a doua a acestui curs poți deja să scrii o instrucțiune print și avem lucruri mai bune de făcut. În schimb, începem cu cea mai mare schimbare culturală din Python din ultimul deceniu: type hints au încetat să fie opționale.
În 2014, când PEP 484 a introdus typing-ul în Python, era o idee disputată. Jumătate de comunitate era încântată să primească în sfârșit verificare statică; cealaltă jumătate scria postări mânioase pe blog despre cum typing-ul era o trădare a sufletului dinamic al Python. În 2026, argumentul ăsta s-a încheiat. Fiecare bibliotecă majoră e tipizată. Fiecare descriere de job pentru roluri Python senior menționează mypy sau pyright. Fiecare code review într-un atelier competent va semnala un type hint lipsă. Asistenții AI îți dau sugestii dramatic mai bune când tipurile sunt prezente. Runtime-ul încă nu-i pasă: va rula bucuros Python netipizat până la moartea termică a universului, dar oamenii și uneltele din jurul runtime-ului acum chiar le pasă.
Lecția asta e ghidul practic: ce să tipizezi, cum să tipizezi, când să sari peste și ce unelte să îndrepți spre codul tău. De aici încolo, totul folosește type hints implicit, așa că hai să ne asigurăm că suntem pe aceeași pagină.
De ce să tipizezi, când runtime-ul le ignoră
Type hints din Python nu sunt impuse la runtime. Asta îi încurcă pe toți cei care vin din Java, C#, TypeScript, Rust sau, în general, orice alt limbaj tipizat. Poți scrie asta:
def add(x: int, y: int) -> int:
return x + y
print(add("hello", "world")) # afișează "helloworld". Da, chiar așa.
Python vede int și ridică din umeri. Operatorul + din Python funcționează pe string-uri, funcția returnează un string, programul rulează. Type hint-ul a fost un comentariu pe care interpretorul l-a ignorat politicos.
Atunci de ce să te obosești? Patru motive, ordonate după cât de des îți salvează pielea în practică.
1. Suport în editor. Editoarele moderne (VS Code cu Pylance, PyCharm, Cursor, Neovim cu pyright-langserver) citesc type hints și îți oferă autocomplete, ajutor pentru parametri, click-through la definiții și sublinieri roșii acolo unde folosești greșit o funcție. Fără type hints, toate astea degradează la „ghicește”. Odată ce ai lucrat într-o bază de cod complet tipizată, nu te mai întorci, la fel cum nimeni care a folosit un debugger nu se mai întoarce la instrucțiuni print (în general).
2. Prinderea bug-urilor la momentul editării. Unelte precum pyright și mypy îți vor spune, înainte să rulezi codul, că pasezi un str acolo unde o funcție așteaptă un int, sau că o funcție ar putea returna None și tu ai uitat să tratezi cazul. O fracțiune surprinzătoare de bug-uri „reale” în baze de cod Python netipizate sunt exact din această categorie: o funcție care de obicei returnează o valoare, dar uneori returnează None, iar apelantul nu a verificat niciodată. Type hints le fac imposibil de ignorat.
3. Auto-documentare. O semnătură de funcție de tipul def fetch(client, query, timeout=None, retries=3) îți spune aproape nimic. Aceeași funcție ca def fetch(client: HttpClient, query: str, timeout: float | None = None, retries: int = 3) -> Response îți spune exact ce să pasezi și ce vei primi înapoi. Docstring-ul are încă treabă de făcut, dar contractul e în semnătură.
4. Asistenții AI devin dramatic mai buni. Asta e nouă. Copilot, Claude și restul sunt antrenați pe cod tipizat; cu cât baza ta de cod are mai multe tipuri, cu atât prezic mai exact ce să scrie în continuare. Erorile de tip curg în contextul AI-ului și-i modelează sugestiile. Vom reveni la asta.
Bazele, sintaxa modernă
Dacă ai învățat typing dintr-o postare de blog din 2018, secțiunea asta va arăta ușor diferit față de ce-ți amintești. Python 3.9 (sintaxa list[int]) și 3.10 (sintaxa X | None) au curățat ergonomia dramatic. Vom folosi formele moderne pe tot parcursul.
# Argumente de funcție și tip de retur
def greet(name: str) -> str:
return f"Hello, {name}"
# Adnotare de variabilă (rar necesară; tipul e de obicei dedus)
count: int = 0
ratio: float = 0.5
# Tipuri de container built-in: notă: lowercase, fără import
names: list[str] = ["Alice", "Bob"]
ages: dict[str, int] = {"Alice": 30, "Bob": 25}
coords: tuple[float, float] = (45.46, 9.19)
unique_ids: set[int] = {1, 2, 3}
# Valori opționale: preferă sintaxa `|` în locul `Optional[X]`
def find_user(user_id: int) -> User | None:
...
# Uniune de mai multe tipuri
def parse(value: str | int | float) -> Decimal:
...
# Valori implicite cu tipuri
def fetch(url: str, timeout: float = 5.0) -> bytes:
...
Câteva lucruri de remarcat:
list[str], nuList[str].Listcu L mare dintypinge forma de dinainte de 3.9. Renunță la ea.int | None, nuOptional[int]. Ambele sunt legale; forma nouă e mai scurtă și nu are nevoie de import.tuple[float, float]e un tuple de exact două float-uri.tuple[int, ...]e „un tuple cu orice număr de int-uri” (...chiar este sintaxa).- Python deduce acum tipurile în cazurile evidente.
count = 0e fin;count: int = 0e redundant. Adnotează variabilele doar când tipul nu e evident din atribuire, sau când declari o variabilă fără valoare (buffer: list[bytes] = []e fin;buffer = []lasă checker-ul nesigur ce intră acolo).
Funcții, callables și iterables
Codul real pasează funcții. Codul real consumă generatoare. Tipizează-le și pe acelea.
from collections.abc import Callable, Iterable, Iterator
# O funcție care primește o funcție
def apply(func: Callable[[int, int], int], a: int, b: int) -> int:
return func(a, b)
# O funcție care primește orice poți itera
def total(numbers: Iterable[float]) -> float:
return sum(numbers)
# O funcție generator
def count_up(stop: int) -> Iterator[int]:
n = 0
while n < stop:
yield n
n += 1
De remarcat că importăm din collections.abc, nu din typing. typing.Iterable și prietenii sunt aliasuri depreciate din 3.9; collections.abc e casa modernă. Ambele funcționează; codul nou folosește calea nouă.
Callable[[int, int], int] se citește „un callable care primește două int-uri și returnează un int.” Dacă nu-ți pasă de semnătură, doar Callable e fin, dar arunci cea mai mare parte din valoarea tipizării.
Generics: noua sintaxă
Uneori vrei o funcție care lucrează pe „o listă de ceva” și returnează „acel ceva.” Acel ceva este o type variable, iar 3.12 a făcut declararea ei mult mai curată.
# Pre-3.12 (încă funcționează, încă obișnuit în cod mai vechi)
from typing import TypeVar
T = TypeVar("T")
def first(items: list[T]) -> T:
return items[0]
# 3.12+
def first[T](items: list[T]) -> T:
return items[0]
# Aceeași idee, cu o clasă
class Stack[T]:
def __init__(self) -> None:
self._items: list[T] = []
def push(self, item: T) -> None:
self._items.append(item)
def pop(self) -> T:
return self._items.pop()
# Type aliases au primit același tratament
type UserId = int
type Json = dict[str, "Json"] | list["Json"] | str | int | float | bool | None
Folosește sintaxa nouă pentru cod nou. Vei vedea în continuare vechea formă TypeVar în biblioteci scrise înainte de 3.12: nu e greșită, doar verbose.
Tipuri cu scop special, de știut
from typing import Any, Literal, Final, TypedDict, NotRequired
# Any: portița de scăpare. Înseamnă „renunț la tipizare pentru această valoare."
# Folosește-l cu măsură; fiecare Any e o gaură în siguranța de tip.
def parse_unknown(blob: Any) -> dict[str, Any]:
...
# Literal: doar valorile specifice sunt permise
def set_mode(mode: Literal["read", "write", "append"]) -> None:
...
# Final: numele ăsta e setat o singură dată și niciodată reasignat
MAX_RETRIES: Final = 5
# TypedDict: un dict cu chei și tipuri de valoare cunoscute
class UserDict(TypedDict):
id: int
name: str
email: NotRequired[str] # poate fi sau nu prezent
TypedDict e genuin util când parsezi JSON și nu vrei să definești o clasă completă. Literal face parametrii „enum-tipiți de string” mai siguri. Final e documentație care prinde și reasignările accidentale.
from __future__ import annotations
Vei vedea linia asta în capul unor baze de cod mai vechi:
from __future__ import annotations
Context: type hints erau evaluate la momentul definirii funcției, ceea ce cauza probleme cu forward references (o clasă care se referea pe ea însăși, tipuri definite mai târziu în fișier etc.). Soluția a fost PEP 563, „postponed evaluation of annotations”, care face ca toate adnotările să fie string-uri evaluate doar când le cere ceva.
În 3.10+ în general nu ai nevoie de el. Forward references funcționează în general. În 3.13 rămâne opt-in, dar nu e implicit; abordarea originală PEP 649 „deferred evaluation” a aterizat în 3.14 și schimbă din nou peisajul. Pentru acest curs poți ignora importul __future__ decât dacă lovești o eroare specifică de forward reference, caz în care adăugarea lui rezolvă de obicei problema.
Type checkers: pyright și mypy
Două opțiuni reale.
pyright (Microsoft, scris în TypeScript, livrat în VS Code/Cursor ca Pylance). Mai rapid decât mypy la o margine largă. Defaults mai stricte. Mai bun la verificare incrementală. Dacă folosești VS Code, deja rulează la fiecare apăsare de tastă. Ca să rulezi din CLI:
uv tool install pyright
pyright src/
mypy (originalul, întreținut de comunitatea de typing din Python). Mai configurabil, defaults mai conservative, mai bun la unele cazuri la limită care implică generics. Ca să rulezi:
uv tool install mypy
mypy src/
Ambele produc rezultate similare pe majoritatea codului; ambele vor fi ocazional în dezacord, de obicei despre cât de strict să fie pe cazurile la limită. Alege unul pentru un proiect dat și configurează-l în pyproject.toml. Nu rula ambele: doar te vei certa cu două unelte în loc de una.
Un bloc rezonabil de pornire în pyproject.toml pentru pyright:
[tool.pyright]
include = ["src"]
typeCheckingMode = "standard"
pythonVersion = "3.13"
Setează typeCheckingMode = "strict" odată ce ești confortabil. E un salt brusc de zgomot, dar acolo ajung bazele de cod serioase.
Pont AI: Pyright + Copilot/Claude devine mult mai util odată ce codul tău e tipizat: erorile de tip curg în contextul AI-ului, iar sugestiile devin mult mai exacte. Tipizează-ți întâi interfețele, chiar dacă internele rămân tipizate lejer. Granițele dintre module și API-urile publice ale claselor tale sunt locul unde tipizarea plătește cele mai multe dividende, atât pentru oameni, cât și pentru modelul care stă în editorul tău.
Când să NU tipizezi
Tipizarea nu e gratuită. Adăugarea de tipuri unui script de 30 de linii pe care îl ștergi mâine e o pierdere. Specific:
- Explorare în REPL. Nimeni nu tipizează în REPL. Nu începe.
- Scripturi de unică folosință. Un script de 50 de linii care scrapează o pagină web o singură dată nu e locul unde tipizarea își câștigă pâinea.
- Notebook-uri de lipici. Notebook-uri Jupyter care fac analiză exploratorie. Ritmul e prea rapid și ciclul de viață e prea scurt.
- Cod genuin dinamic. Unele pattern-uri de metaprogramare sfidează genuin tipizarea statică. Când te trezești că te lupți cu sistemul de tipuri o jumătate de oră, un comentariu
# type: ignoree fin.
Ce ar trebui tipizat: orice cod care se livrează, orice cod din src/, orice funcție pe care altcineva ar putea-o apela, orice API public. Regula 80/20 se aplică: tipizează granițele, clasele de date, semnăturile de funcție. Helpers interne de o linie pot rămâne lejere.
Un exemplu lucrat
Să tipizăm o funcție mică așa cum ai scrie-o efectiv în 2026.
from collections.abc import Iterable
from dataclasses import dataclass
from decimal import Decimal
@dataclass
class LineItem:
sku: str
quantity: int
unit_price: Decimal
def order_total(items: Iterable[LineItem], vat_rate: Decimal = Decimal("0.22")) -> Decimal:
"""Compute the VAT-inclusive total for a sequence of line items."""
subtotal = sum((item.unit_price * item.quantity for item in items), start=Decimal("0"))
return subtotal * (1 + vat_rate)
Trei lucruri de remarcat:
Iterable[LineItem], nulist[LineItem]. Funcția nu are nevoie de o listă; iterează o singură dată. AcceptândIterable, lași un apelant să paseze un generator, un tuple sau o listă. Fii liberal în ce accepți.Decimal, nufloat, pentru bani. Mereu. Float-urile sunt pentru știință; decimalele sunt pentru bani.pyrightte va prinde dacă le amesteci.- Tipul de retur e explicit. Funcția e suficient de scurtă încât deducția ar fi funcționat, dar scriindu-l îl transformi într-un contract pentru apelanți.
Încheiere
Tipurile nu mai sunt opționale în Python modern. Folosește sintaxa nouă (list[int], int | None, def f[T](...)). Folosește pyright sau mypy. Tipizează cel puțin granițele modulelor tale, iar asistentul AI te va răsplăti pentru tipizare suplimentară. Nu tipiza REPL-ul sau scripturile de aruncat.
Lecția 3 (marți) e despre trei caracteristici sintactice care au schimbat felul în care se scrie Python la lucru: f-strings, walrus operator și pattern matching. Vom scrie cod, nu teorie.
Lecturi suplimentare
- PEP 484: Type Hints: originalul.
- PEP 695: Type Parameter Syntax: revizuirea de generics din 3.12.
- Documentația de typing din Python: referință cuprinzătoare.
- Documentația pyright: checker-ul pe care ne vom sprijini.