Trzecia część serii o zmianach w Pythonie. Ta część obejmuje wersje 3.12 i 3.13: nową składnię generyków, instrukcję type, sformalizowane f-stringi i nowy interaktywny REPL.

Składnia parametrów typów

Wersja3.12
KategoriaSkładnia
PEPPEP 695
OpisGeneryczne funkcje i klasy deklaruje się w nawiasach kwadratowych, bez ręcznego TypeVar.

Przed 3.12 parametr typu był zmienną modułu, a klasa generyczna musiała dziedziczyć po Generic:

from typing import Generic, TypeVar

T = TypeVar("T")
S = TypeVar("S", str, bytes)

class Stack(Generic[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()

def first(items: list[T]) -> T:
    return items[0]

def longest(a: S, b: S) -> S:
    return a if len(a) >= len(b) else b

Od 3.12 parametry deklaruje się przy nazwie klasy lub funkcji, a ich zasięg jest ograniczony do tej definicji:

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()

def first[T](items: list[T]) -> T:
    return items[0]

def longest[S: (str, bytes)](a: S, b: S) -> S:   # S tylko str albo bytes
    return a if len(a) >= len(b) else b

Instrukcja type

Wersja3.12
KategoriaSkładnia
PEPPEP 695
OpisDefiniuje aliasy typów, wyliczane leniwie.

Przed 3.12 alias był zwykłym przypisaniem z adnotacją TypeAlias, a typ rekurencyjny wymagał cudzysłowów:

from typing import TypeAlias, TypeVar

T = TypeVar("T")

Pair: TypeAlias = tuple[T, T]
Json: TypeAlias = "dict[str, Json] | list[Json] | str | int | None"

Od 3.12:

type Pair[T] = tuple[T, T]
type Json = dict[str, Json] | list[Json] | str | int | None   # rekurencja bez cudzysłowów

def swap[T](pair: Pair[T]) -> Pair[T]:
    return pair[1], pair[0]

Pair.__value__      # tuple[T, T], wyliczone dopiero przy odczycie

Sformalizowane f-stringi

Wersja3.12
KategoriaSkładnia
PEPPEP 701
OpisWewnątrz {} można używać tych samych cudzysłowów co na zewnątrz, odwrotnych ukośników, komentarzy i wielu linii.

Przed 3.12 wewnątrz {} trzeba było użyć innego rodzaju cudzysłowów, a odwrotny ukośnik wymagał zmiennej pomocniczej:

songs = [{"title": "Intro"}, {"title": "Outro"}]
print(f"Utwory: {', '.join(s['title'] for s in songs)}")

newline = "\n"
print(f"{newline.join(s['title'] for s in songs)}")

Od 3.12:

songs = [{"title": "Intro"}, {"title": "Outro"}]
print(f"Utwory: {", ".join(s["title"] for s in songs)}")
print(f"{"\n".join(s["title"] for s in songs)}")

total = f"{
    sum(len(s["title"]) for s in songs)   # komentarz w wyrażeniu
}"

typing.override

Wersja3.12
KategoriaTypowanie
PEPPEP 698
OpisOznacza metodę nadpisującą metodę klasy bazowej. Type checker zgłasza błąd, gdy w bazie takiej metody nie ma.

Dekorator chroni przed cichym rozjechaniem się hierarchii przy refaktoryzacji. Po zmianie nazwy metody w klasie bazowej metoda w podklasie przestaje ją nadpisywać, ale kod dalej działa i wywołuje implementację z bazy. Z @override type checker wskaże każdą podklasę, której metoda nie ma już odpowiednika w bazie.

from typing import override

class Base:
    def save(self) -> None: ...

class User(Base):
    @override
    def save(self) -> None: ...     # OK

    @override
    def sav(self) -> None: ...      # type checker: w Base nie ma metody sav

Unpack[TypedDict] dla **kwargs

Wersja3.12
KategoriaTypowanie
PEPPEP 692
OpisPrecyzyjne typowanie **kwargs: każdy argument nazwany ma własny typ.

Najwięcej daje w funkcjach opakowujących, które przekazują opcje dalej (klient HTTP, wywołanie subprocess, fabryka połączeń). Zestaw opcji opisuje się raz jako TypedDict i używa go w wielu sygnaturach, bez powtarzania listy parametrów i bez gubienia typów w **kwargs.

Przed 3.12 adnotacja **kwargs określała jeden typ dla wszystkich argumentów, bez ich nazw:

def request(url: str, **kwargs: float) -> None: ...

request("https://example.com", timeout=5, retry=3)    # OK dla type checkera, literówka niewykryta

Od 3.12:

from typing import NotRequired, TypedDict, Unpack

class Options(TypedDict):
    timeout: float
    retries: NotRequired[int]

def request(url: str, **kwargs: Unpack[Options]) -> None: ...

request("https://example.com", timeout=5)             # OK
request("https://example.com", timeout=5, retry=3)    # type checker: nieznany argument retry

itertools.batched()

Wersja3.12
KategoriaBiblioteka
PEP—
OpisDzielenie na paczki: batched("ABCDEFG", 3) → ABC, DEF, G.

Przed 3.12:

from itertools import islice

def batched(iterable, n):
    it = iter(iterable)
    while chunk := tuple(islice(it, n)):
        yield chunk

Od 3.12:

from itertools import batched

list(batched("ABCDEFG", 3))     # [('A', 'B', 'C'), ('D', 'E', 'F'), ('G',)]

for chunk in batched(range(10_000), 500):
    print(len(chunk))           # np. zapis do bazy po 500 wierszy

pathlib.Path.walk(), sys.monitoring

Wersja3.12
KategoriaBiblioteka
PEPPEP 669 (sys.monitoring)
OpisOdpowiednik os.walk() dla Path oraz lekkie API monitorowania wykonania dla debuggerów i profilerów.

Przed 3.12 do przejścia drzewa katalogów służył os.walk(), który zwraca ścieżki jako napisy:

import os
from pathlib import Path

for root, dirs, files in os.walk("src"):
    dirs[:] = [d for d in dirs if d != "__pycache__"]
    for name in files:
        if name.endswith(".py"):
            print(Path(root) / name)

Od 3.12 root jest od razu obiektem Path:

from pathlib import Path

for root, dirs, files in Path("src").walk():
    dirs[:] = [d for d in dirs if d != "__pycache__"]   # pomija katalogi
    for name in files:
        if name.endswith(".py"):
            print(root / name)

sys.monitoring jest przeznaczony dla narzędzi: debuggerów, profilerów i mierników pokrycia kodu. Stare sys.settrace() i sys.setprofile() wywołują callback dla każdego zdarzenia w całym programie i spowalniają go wielokrotnie. Nowe API pozwala zapisać się tylko na wybrane zdarzenia, a callback może zwrócić sys.monitoring.DISABLE, żeby wyłączyć zdarzenie w danym miejscu kodu. Kilka narzędzi (np. debugger i coverage) działa naraz, bo każde ma własny identyfikator.

import sys

mon = sys.monitoring
calls = {}

def on_start(code, offset):
    calls[code.co_name] = calls.get(code.co_name, 0) + 1

def fib(n):
    return n if n < 2 else fib(n - 1) + fib(n - 2)

mon.use_tool_id(mon.PROFILER_ID, "licznik wywołań")
mon.register_callback(mon.PROFILER_ID, mon.events.PY_START, on_start)
mon.set_events(mon.PROFILER_ID, mon.events.PY_START)

fib(10)

mon.set_events(mon.PROFILER_ID, 0)
mon.free_tool_id(mon.PROFILER_ID)
print(calls)    # {'fib': 177}

distutils

Wersja3.12
KategoriaUsunięte
PEPPEP 632
OpisModuł usunięty. Jego rolę przejęły setuptools i inne backendy budowania.

Nowy interaktywny REPL

Wersja3.13
KategoriaInterpreter
PEP—
OpisREPL oparty na kodzie z PyPy: edycja wielu linii, historia całych bloków, kolorowe prompty i tracebacki, komendy exit i help bez nawiasów, tryb wklejania (F3).

Do 3.12 samo exit tylko wypisywało podpowiedź:

$ python3.12
>>> exit
Use exit() or Ctrl-D (i.e. EOF) to exit

Od 3.13:

$ python3.13
>>> def greet(name):
...     return f"Cześć, {name}!"
...
>>> greet("Ala")
'Cześć, Ala!'
>>> exit

Strzałka w górę przywraca całą funkcję do edycji, a nie pojedynczą linię. Stary REPL włącza zmienna PYTHON_BASIC_REPL=1.


warnings.deprecated

Wersja3.13
KategoriaBiblioteka
PEPPEP 702
OpisDekorator, który zgłasza DeprecationWarning w czasie działania i jest rozpoznawany przez type checkery.

Przed 3.13 ostrzeżenie zgłaszało się ręcznie w ciele funkcji, a type checker o nim nie wiedział:

import warnings

def connect() -> None:
    warnings.warn("Użyj connect_v2()", DeprecationWarning, stacklevel=2)

Od 3.13:

from warnings import deprecated

@deprecated("Użyj connect_v2()")
def connect() -> None: ...

connect()   # DeprecationWarning: Użyj connect_v2(), type checker też oznacza wywołanie

Domyślne parametry typów, TypeIs, ReadOnly

Wersja3.13
KategoriaTypowanie
PEPPEP 696, PEP 742, PEP 705
OpisDomyślne wartości parametrów typów, precyzyjniejsze zawężanie typu i pola tylko do odczytu w TypedDict.

Przed 3.13 do własnych funkcji sprawdzających typ służył TypeGuard, który zawęża typ tylko w gałęzi if:

from typing import TypeGuard

def is_str(value: object) -> TypeGuard[str]:
    return isinstance(value, str)

def handle(value: int | str) -> None:
    if is_str(value):
        value.upper()         # tu value: str
    else:
        value + 1             # type checker: value nadal int | str

Od 3.13:

from typing import ReadOnly, TypedDict, TypeIs

class Box[T = int]: ...       # Box bez parametru oznacza Box[int]

def is_str(value: object) -> TypeIs[str]:
    return isinstance(value, str)

def handle(value: int | str) -> None:
    if is_str(value):
        value.upper()         # tu value: str
    else:
        value + 1             # tu value: int

class Settings(TypedDict):
    name: ReadOnly[str]

def rename(settings: Settings) -> None:
    settings["name"] = "x"    # type checker: pole tylko do odczytu

Usunięte moduły

Wersja3.13
KategoriaUsunięte
PEPPEP 594
OpisUsunięte: cgi, cgitb, crypt, telnetlib, nntplib, audioop i inne. smtpd zniknął już w 3.12.