Wróć na górę
Artykuły / Python

Kompatybilność Pythona: od zmian w składni do C API 3.14

Autor: Markus Blom 18 kwietnia 2024 ok. 15 min czytania

W świecie utrzymania środowisk i budowania nowoczesnej infrastruktury IT aktualizacja wersji Pythona rzadko kiedy jest tak prosta jak apt-get update albo podmiana base image w Dockerfile. Od migracji z Pythona 2, aż po eksperymenty z Pythonem 3.14, łatwo natknąć się na rozsypujące się biblioteki i przestające działać narzędzia.

Te"niekompatybilności” nie biorą się z kaprysu twórców języka, tylko są efektem ewolucji architektury. W tym tekście patrzymy na to z dwóch perspektyw: historycznego przeskoku z 2.x na 3.x (składnia, typy, stdlib) oraz najnowszych zmian w C API w 3.14, które szczególnie dotykają ekosystem rozszerzeń w C/C++.

I. Co naprawdę zmienił Python 3?

Przejście z Pythona 2 na 3 to największa w historii języka zmiana łamiąca kompatybilność. Celem było pozbycie się historycznego balastu i dopasowanie języka do nowoczesnych wymagań inżynierii oprogramowania.

Najbardziej odczuwalne zmiany dotyczyły typów danych i podstawowej składni:

  • Napisy i kodowanie – w Pythonie 2 wszechobecne UnicodeDecodeError wynikały z mieszania str i unicode. Python 3 sprawę uciął: domyślne str jest Unicode, a bajty to osobny typ bytes.
  • Dzielenie liczb całkowitych – w 3.x operator / zawsze zwraca float (np. 3 / 2 == 1.5), a dzielenie całkowite wymaga jawnego //. Zniknęła część"magii” implicit castów znanych z 2.x.
  • Iteratory zamiast listxrange zniknął, a range stał się leniwym generatorem, co radykalnie zmniejsza zużycie pamięci przy dużych zakresach.

Na poziomie kodu migrujące projekty często najpierw trafiały na błędy składni:

Python 2 → Python 3
# stary styl w Pythonie 2 – w 3.x SyntaxError
try:
    do_something()
except Exception, e:
    pass

# zalecany zapis w Pythonie 3
try:
    do_something()
except Exception as e:
    pass

Jeśli w nowym interpreterze widzisz SyntaxError wskazujący na linie except Exception, e lub na operator <>, najprawdopodobniej kod utknął w epoce Pythona 2. Pomóc może 2to3 albo ręczne zastąpienie <> przez !=.

Równolegle mocno posprzątano bibliotekę standardową: scalono moduły (np. StringIO / cStringIOio.StringIO), usunięto przestarzałe i potencjalnie niebezpieczne moduły (np. stare md5/sha), uproszczono API.

II. Głębsza woda: C API w Pythonie 3.14

Podczas gdy pierwsze wydania Pythona 3 wprowadzały głównie zmiany"widziane” w kodzie, Python 3.14 dotyka fundamentów – sposobu, w jaki rozszerzenia w C integrują się z interpreterem.

Jednym z najważniejszych kierunków jest eksperymentalny wariant pozbawiony GIL (3.14t), czyli interpreter przystosowany do bezpiecznej pracy wielu wątków równolegle. Żeby to osiągnąć, trzeba było zmienić sposób zarządzania obiektami i referencjami.

C API – stary vs nowy styl
/* stary, problematyczny sposób:
   bezpośredni dostęp do ob_refcnt */
Py_ssize_t count = obj->ob_refcnt;

/* zalecane podejście z makrem – zgodne z nowszym C API */
Py_ssize_t count = Py_REFCNT(obj);

Bezpośrednie czytanie ob_refcnt łamie enkapsulację wewnętrznego stanu. W środowisku bez GIL licznik referencji musi być aktualizowany w sposób atomowy; stare skróty przestają być akceptowalne i mogą kończyć się nie tylko błędem kompilacji, ale i segfaultem.

Skutkiem ubocznym jest to, że część projektów korzystających z niskopoziomowego C API (np. specjalistyczne narzędzia do layoutu układów scalonych) zwyczajnie nie kompiluje się pod 3.14. Dla osób budujących obrazy dockera lub utrzymujących systemy oznacza to, że"niewinne” podbicie wersji Pythona może"wysadzić” build już na etapie gcc/clang.

III. Ekosystem nie nadąża: biblioteki i narzędzia

Rdzeń języka rozwija się szybko, ale ekosystem nie zawsze jest w stanie reagować w tym samym tempie. Przy planowaniu upgrade’u trzeba patrzeć szerzej niż sam interpreter.

1. Biblioteki data science i inne ciężkie zależności

Frameworki takie jak TensorFlow, PyTorch czy duże biblioteki naukowe zazwyczaj mają opóźnienie w stosunku do najnowszych wersji Pythona – czasem rzędu wielu miesięcy. Przykładowo:

  • TensorFlow przez długi czas wspierał jedynie zakres 3.9–3.12.
  • PyDICOM wymaga min. Pythona 3.10, ale wnętrze biblioteki i ostrzeżenia o deprecjacjach mogą robić problemy w nowszych środowiskach.
2. Lintery i narzędzia statycznej analizy

W nowoczesnych pipeline’ach CI/CD to Ruff, MyPy, Flake8 i spółka decydują o tym, czy build przejdzie. Jeżeli trzymasz się starszych wersji tych narzędzi, mogą one nie rozumieć nowszych konstrukcji Pythona (np. match / case, nowe typy w typing), co kończy się fałszywymi błędami.

3. IDE i debugger

Zmiany w C API uderzają również w narzędzia developerskie. Przykładowo, jeżeli debugger w PyCharmie czy rozszerzenie Python dla VS Code nie zostały jeszcze dostosowane do nowej wersji Pythona, możesz zobaczyć crashe debuggera, znikające zmienne albo brak możliwości wchodzenia w funkcje – mimo że sam kod działa poprawnie.

IV. Trzy rzeczy do sprawdzenia przed (i po) upgrade
  • Stan paczek z C extension na PyPI – przed przeskokiem na nowszy minor (np. 3.12 → 3.14) sprawdź, czy kluczowe zależności mają już gotowe .whl dla tej wersji. Brak wheel’a oznacza kompilację ze źródła, a to przy zmianach C API szybko kończy się niepowodzeniem.
  • Wersje linterów i narzędzi w CI – po zmianie wersji Pythona zsynchronizuj również Ruffa, MyPy, Flake8 itd. Stare wersje nie rozumieją nowego AST i generują lawinę fałszywych błędów.
  • Kompatybilność debuggera – jeśli po upgrade widzisz dziwne zachowanie w trybie debugowania (brak podglądu zmiennych, samoczynne przerwanie sesji), zacznij od changelogów IDE i wtyczek, zanim uznasz, że to"bug w Pythonie”.
Poprzedni artykuł Instalacja Pythona na Windows: własna ścieżka i poprawne PATH
Następny artykuł Python asyncio w praktyce: mechanizmy i debugowanie"wolnego” API