Всем привет! Решил попробовать новый формат на хабре: делиться результатами своей работы за прошедший месяц. С интересными ссылками на события, проекты, обсуждения, релизы из мира питона, моего телеграм канала и нашего замечательного чата (куда много людей приходят пиарить свои проекты).

No AI slop
No AI slop

Давайте сразу договоримся: я не публикую ИИ слоп, пишу статью ручками (потому что уважаю своего читателя) и рассказываю интересное и актуальное из своей бесплатной работы в опенсорсе, а с вас лайк статье (если она будет для вас интересной).

Погнали смотреть, что у нас в питоне происходило!


CPython

Начнем с самого важного и большого - с питона. Мой месяц в питоне прошел под знаком frozendict. Я старался активно улучшать поддержку иммутабельных типов в CPython.

Написал PEP и сделал Reference Implementation нового синтаксиса для frozendict / frozenset

Вместе с Донхи На (членом Steering Council) написали ПЕП для добавления нового синтаксиса frozen контейнеров в питон.

Такого синтаксиса явно не хватает. Наша главная задача: повысить частоту использования иммутабельных типов данных перед Free-Threading era.

fd = f{1: 2, 3: 4}
assert isinstance(fd, frozendict)

fs = f{1, 2}
assert isinstance(fs, frozenset)

Абсолютно знакомый синтаксис. Ведь мы просто добавили f (как frozen) перед существующими dict и set литералами.

Поддерживаем все виды синтаксиса и comprehensions, конечно же:

  • f{1, *other, 2}

  • f{**first, **second, 'default': 0}

  • f{x for x in range(10) if x % 2 == 0}

  • f{x: x for x in range(10)}

  • f{x async for x in arange(10)}

  • PEP-798 f{**items for nums in list_of_items}

Пока идея (PEP и реализация) в процессе доработки и обсуждения. Целимся в 3.16 :)

Предложил добавить новое C-API для оптимизации создания frozendict из dict

PR: https://github.com/python/cpython/pull/153413

Сейчас создание frozendict - довольно дорогая операция. PyFrozenDict_New вызывает dict_update_common, которые копирует данные из словаря за O(n). А мой PyDict_AsFrozenDictAndClear сделает такое за O(1).

Так как PyDictObject и PyFrozenDictObject C-структуры имеют очень похожее внутреннее устройство, то я могу просто выдрать куски из словаря и вставить их в новый frozendict.

Аллоцируем новый пустой frozendict, затем под локом - выдираем куски из старого словаря. Детали реализации (чуть упрощено для чтения):

PyObject *
PyDict_AsFrozenDictAndClear(PyObject *dict)
{
    if (dict == NULL || !PyDict_Check(dict)) {
        PyErr_BadInternalCall();
        return NULL;
    }

    PyObject *res = frozendict_new_untracked(&PyFrozenDict_Type);
    if (res == NULL) {
        return NULL;
    }

    Py_BEGIN_CRITICAL_SECTION(dict);
    transfer_keys_and_values_lock_held(res, dict);
    Py_END_CRITICAL_SECTION();

    _PyObject_GC_TRACK(res);
    assert(_PyFrozenDictObject_CAST(res)->ma_hash == -1);
    return res;
}

Хеш во frozendict считается лениво по запросу __hash__, а не сразу при создании; потому он остается -1.

А вот как мы выдираем куски (забираем ключи и значения), очищаем входной словарь:

static void
transfer_keys_and_values_lock_held(PyObject *res, PyObject *dict)
{
    PyDictObject *new = (PyDictObject *)res; 
    PyDictObject *old = (PyDictObject *)dict;
    assert(can_modify_dict(old));

    // Fast path: do nothing on an empty dict:
    if (old->ma_keys == Py_EMPTY_KEYS) {
        return;
    }

    Py_ssize_t used = old->ma_used;
    PyDictKeysObject *keys = old->ma_keys;
    PyDictValues *values = old->ma_values;

    // Clear the old dict keys and values, but do not decref them: 
    clear_common(old);
    set_keys(old, Py_EMPTY_KEYS);
    set_values(old, NULL);
    ASSERT_CONSISTENT(old);

    // Transfer keys and values from dict to frozendict:
    new->ma_used = used;
    new->ma_keys = keys;
    new->ma_values = values;
    ASSERT_CONSISTENT(new);
}

Прикольно, да? Такое же АПИ мы готовим для frozenset и set. Там все сложнее, к сожалению старые фукнции C-API для PySet умеют мутировать frozenset :( Надо будет такое запрещать, но для начала деприкейтить.

Поддержал добавление новых двух методов для dict и set

На основе моего АПИ Виктор Стиннер (топ2 по коммитам в CPython после Гвидо) предложил добавить два метода: dict.take_frozendict и set.take_frozenset, которые будут превращать мутабельные данные в имммутабельные данные (реализация). Он поделился со мной идеей, я был только за!

fs = {1, 2, 3}.take_frozenset()

assert type(fs) is frozenset
assert fs == frozenset({1, 2, 3})

У нас уже есть bytearray.take_bytes, который за O(1) и без копирования данных превращает bytearray в иммутабельный bytes.

Возможно, что появятся аналоги и для других типов.

Пофиксил очень крутой баг, который показывает опасность мутабельных типов данных

PR: https://github.com/python/cpython/pull/152483

То формально было 29 июня, но какая разница!

Вот такой простой код содержит в себе 2 критичных бага:

class Evil:
    def __eq__(self, other):
        return other

leaked = vars(list) == Evil()
name = "example"
leaked[name] = lambda self: "probe"
print(getattr(list, name)([]))
del leaked[name]
print(hasattr(list, name))

Почему?

Потому что types.MappingProxyType выставляет наружу внутренний тип (dict) при сравнении и операции |. И мы уже его можем полностью мутировать. А тут мы выставляем наружу list.__dict__, который можем мутировать таким хитрым способом. И мы мутируем ->tp_dict встроенного типа данных! Потом интерпретатор падает со стектрейсом от таких приколов.

Полный разбор бага (длинный текст!)

Я просто сделал PR, который отправляет копии данных в небезопасные классы. Сейчас, однако, идет обсуждение: возможно мое изменение нужно откатывать. Некоторые участники сообщества считают, что просто так делать не надо. И бага нет. Обсуждаем дальше!

msgspec

репозиторий | зеркало

Для тех, кто не знает: самый быстрый json / msgspack сериализатор и десериализатор в Python. Написан на C, использует просто уйму оптимизаций.

В июне я рассказывал, почему msgspec такой быстрый, а в июле я добавил:

Ждем новый релиз, пока даем коду настояться ?️️

django-modern-rest

репозиторий | зеркало

Самый быстрый, строгий и удобный REST API слой для Django. Если не знакомы, то я публиковал статью на хабре про него. В июле было сделано два новых релиза. Важные изменения:

>>> from dmr.settings import Settings
>>> from dmr.security import SyncOrAsyncAuth
>>> from dmr.security.http import HttpBasicAsyncAuth, HttpBasicSyncAuth

>>> DMR_SETTINGS = {
...     Settings.auth: [
...         SyncOrAsyncAuth(
...             HttpBasicSyncAuth(),
...             HttpBasicAsyncAuth(),
...         ),
...     ],
... }

wemake-python-styleguide

репозиторий | зеркало

Самый строгий Python линтер ever! Лучший инструмент, чтобы научить вашего агента писать простой и понятный Python код.

За июль вышел новый релиз 1.7.0. В нем мы:

  • Запретили писать array[start:stop:] вместо array[start:stop] и array[start::] вместо array[start:]

  • Улучшили правила для match/case, например, такой код теперь заставят переписывать:

match state:
    case EventType.REJECT:
        user = 'rejected'
    case _:
        user = 'active'

Потому что тут у нас просто if state == EventType.REJECT: ... с else:, а не match/case. Аналогично с другими типами паттернов MatchSequence, MatchMapping, тд.

Одной строкой

Здесь будут ссылки и новости от сообщества. Хотите попасть сюда? Присылайте свои проекты в чат с тегом #opensource. Опубликую в следующем месяце.

Одной строкой:

Заключение

Фух, вроде бы все важное покрыли :) Расскажите, как вам формат? Было ли интересно? Было ли полезно?

Постараюсь выкладывать такие отчеты каждый месяц, если формат вам понравится.

А если вы думаете, что я делаю что-то полезное для мира, то поддержать можно по ссылкам:

Я там ничего не продаю, просто делаю свою работу, если считаете, что я делаю полезное - можно поддержать. Если нет, то я все равно продолжу ее делать.

Подписывайтесь на главный канал сообщества, если вам нравится такое читать. И возможно даже захочется поучаствовать! Буду рад видеть вас частью нашего опенсорс сообщеста :)

Давайте в коммментах обсудим: какой синтаксис для frozendict / frozenset вы бы хотели видеть в Python?

Комментарии (36)


  1. banno
    31.07.2026 13:41

    Стало выглядеть как f строки


    1. sobolevn Автор
      31.07.2026 13:41

      Да, таков наш план. Вот тут было первое голосование https://discuss.python.org/t/frozenset-and-frozendict-comprehensions/101584/48 Люди хотят, чтобы было похоже на f строку :) Я сам был за $


      1. vldmrmlkv
        31.07.2026 13:41

        А если токен будет из двух букв - это нарушает какое-то соглашение в таких случаях или так исторически так сложилось? fo от frozen object тоже нормально и нет путаницы с f-строками.


      1. iroln
        31.07.2026 13:41

        А зачем люди хотят, чтобы было похоже на f-строку? Синтаксический маркер f уже используется в контексте строк. А теперь у этого маркера будут разные синтаксические роли в зависимости от типа литерала. Это по сути контекстно-зависимая семантика. И это выглядит как-то неоднозначно. Я бы так не делал.


        1. sobolevn Автор
          31.07.2026 13:41

          Я не знаю, почему люди так хотят :)

          Как я писал выше, я был за $. Но демократия решила иначе.


      1. danilovmy
        31.07.2026 13:41

        Вот тут было первое голосование https://discuss.python.org/t/frozenset-and-frozendict-comprehensions/101584/48 
        Люди хотят, чтобы было похоже на f строку :) Я сам был за $

        прочел все 75 комментариев. Люди хотят frozenset(...) and frozendict(...) offer much clearer mnemonic value. Зачем вводить f{}, когда возможно очевидное frozenset(iterable) непонятно. кстати предложенный {}.frosen(), кмк, идеальный вариант.

        Но, собственно, где я и где python...


        1. sobolevn Автор
          31.07.2026 13:41

          Но ведь frozenset(iterable) не позволяет нормально оптимизировать код. В обсуждении по ссылке как раз есть примеры, почему так сделать нормально не получится. Сравните опкоды `frozenset({1, 2})` и `frozenset((1, 2))`. В том же обсуждении есть примеры, почему .take_frozenset и .take_frozendict не могут быть единственными дешевыми конструкторами. Например: при многострочных выражениях - непонятно, какой тип будет у объекта до самого конца. И другие важные штуки.


  1. ryann
    31.07.2026 13:41

    It is interesting


  1. danilovmy
    31.07.2026 13:41

    дичь конечно, но как есть:
    почему не упростить синтаксис до mydict.take_frosen() myset.take_frosen(). тогда я могу нормально работать с getattr(obj, "take_frosen")() или по какой то причине необходимо использовать разные названия для одного и того же действия?
    почему не стали использовать что нибудь типа myset as frosen?
    Как будет проверяться что frosen? будет ли if myset is frosen:

    Форматтер строки к сетам/диктам.... о да, это прекрасно нет. Это же очередной выстрел в ногу. Потому что у строки есть/был u'', f'', r'', t'' и теперь f'' будет у dict но он делает не то что вы полумали. а с автодополнением f{1,2,3} в vscode превратится в строку... "1,2,3" и никак не в фрозенсет.


    1. sobolevn Автор
      31.07.2026 13:41

      Я почти ничего не понял :)

      set.take_frozenset и dict.take_frozendict - разные методы для разных типов. их названия точно отражают то, что происходит внутри. Зачем сокращать названия builtin классов - не очень понятно.

      почему не стали использовать что нибудь типа myset as frosen?

      Потому что в питоне нет концепции кастов. as используется для создания имен, а не типов. Добавлять такое - точно не надо :)

      Как будет проверяться что frosen?

      Не очень понял вопрос. Как и сейчас? isinstance(obj, frozenset)

      Форматтер строки к сетам/диктам

      Нет, форматтер строки тут не при чем. Мы добавляем новый токен f{, который называется FLBRACE, по аналогии с { (LBRACE).

      а с автодополнением f{1,2,3} в vscode превратится в строку

      Автодопонение будет работать корректно. Сейчас же `{1, 2, 3}` не превращается в строку. И `f{1, 2, 3}` не будет. Не думаю, что тут могут возникнуть проблемы.


  1. test4354545
    31.07.2026 13:41

    Как будто fdict(ну или frozen_dict) вместо f более явно выражало бы суть. А то как то совсем не по дзену питона


    1. sobolevn Автор
      31.07.2026 13:41

      я не думаю, что тут есть реальная проблема. вы же просто помните, что {} - словарь; вот так же и f{} - иммутабельный словарь.


      1. test4354545
        31.07.2026 13:41

        Дело, конечно, ваше. Скажу лишь, что когда я в первый раз познакомился с Python, я его полюбил как раз за то, что, в отличие от других языков, в нём вещей, которые нужно "просто помнить", было намного меньше и обычно всё упиралось в то, что нужно просто знать англ. язык.


        1. sobolevn Автор
          31.07.2026 13:41

          Как знание английского языка помогает вам понять `{1: 2}`? В чем принципиальное отличие от `f{1: 2}`?


          1. test4354545
            31.07.2026 13:41

            Никак, поэтому и топлю за то чтобы подобного было как можно меньше, а не больше :)


            1. sobolevn Автор
              31.07.2026 13:41

              Спасибо за вашу обратную связь! Вашу позицию не разделяю, но уважаю :)


    1. morijndael
      31.07.2026 13:41

      да, но компилироваться это будет примерно в следующее:

      1. найти имя fdict в локальных/нелокальных/глобальных переменных

      2. проверить, что его вообще можно вызвать

      3. закинуть аргументы на стек

      4. вызвать функцию

      5. функция на C создаст нужный объект дергая внутренности интерпретатора

      А литерал:

      1. закинуть аргументы на стек

      2. создать объект сразу нужным опкодом


      1. sobolevn Автор
        31.07.2026 13:41

        Вы совершенно правы :) Сейчас есть похожая штука: сравните

        » ./python.exe -m dis <<< 'dict(a=1, b=2)'      
          0           RESUME                   0
        
          1           LOAD_NAME                0 (dict)
                      PUSH_NULL
                      LOAD_SMALL_INT           1
                      LOAD_SMALL_INT           2
                      LOAD_CONST               1 (('a', 'b'))
                      CALL_KW                  2
                      POP_TOP
                      LOAD_COMMON_CONSTANT     7 (None)
                      RETURN_VALUE

        И:

        » ./python.exe -m dis <<< '{"a":1, "b": 2}'  
          0           RESUME                   0
        
          1           LOAD_CONST               0 ('a')
                      LOAD_SMALL_INT           1
                      LOAD_CONST               1 ('b')
                      LOAD_SMALL_INT           2
                      BUILD_MAP                2
                      POP_TOP
                      LOAD_COMMON_CONSTANT     7 (None)
                      RETURN_VALUE

        Вроде разница не очень большая, но: BUILD_MAP работает так:

        inst(BUILD_MAP, (values[oparg*2] -- map)) {
            PyObject *map_o = _Py_BuildMap_StackRefSteal(values, oparg);
            DEAD(values);
            ERROR_IF(map_o == NULL);
            map = PyStackRef_FromPyObjectStealMortal(map_o);
        }

        Где _Py_BuildMap_StackRefSteal делает тоже самое, что и простой вызов _PyDict_FromItems. Ну а CALL_KW - один из самых медленных опкодов вообще.


  1. vasily-v-ryabov
    31.07.2026 13:41

    Всё хорошо. Только метод назвал бы не take_frosendict, а as_frosendict. То же самое для frozenset. Так как-то понятнее и короче.

    Ну, или хотя бы to_frozendict. Просто take - взять, а тут вроде отдавать надо. Поэтому и корёжит.


    1. zzzzzzerg
      31.07.2026 13:41

      У take семантика забрать значения, у as оставить на месте и вернуть копию. Можно было бы использовать steal, но от него решили отказаться в пользу существующего прецедента в bytearray.


      1. aero2210
        31.07.2026 13:41

        to_frozen по аналогии с int.to_bytes


        1. sobolevn Автор
          31.07.2026 13:41

          to_bytes не уничтожает существующий int, а просто конвертит его. а тут нужен явный глагол, который бы показывал, что само левое значение обнулится:

          >>> b = bytearray(b'123')
          >>> b
          bytearray(b'123')
          >>> b.take_bytes()
          b'123'
          >>> b
          bytearray(b'')

          В питоне такое назвали take_* :)


          1. morijndael
            31.07.2026 13:41

            надо было назвать yoink_* , с ним сразу понятно что происходит с содержимым :D