Некоторое время назад я добавил в mypy поддержку
ReadOnly special form для TypedDict: https://github.com/python/mypy/pull/17644PR большой, его будут смотреть еще некоторое время. Но, о самых важных принципах рассказать можно уже сейчас.
1. Что такое
ReadOnly?PEP: https://peps.python.org/pep-0705
По сути, он запрещает такой код:
from typing import ReadOnly, TypedDict
class User(TypedDict):
username: ReadOnly[str] # you cannot change the value of `user['username']`
user: User = {'username': 'sobolevn'}
user['username'] = 'changed' # type error! username is read-only
Крайне полезная вещь для бизнес логики.
2.
ReadOnly был добавлен в Python в версию 3.13 мной некоторое время назад: https://github.com/python/cpython/pull/116350Однако, он был добавлен в
typing_extensions еще раньше: https://github.com/python/typing_extensions/commit/0b0166d649cebcb48e7e208ae5da36cfab5965feТак что пользоваться
typing_extensions.ReadOnly можно будет как только выйдет новая версия mypy с поддержкой данной special form.3. Как устроен
ReadOnly?Основная сложность, что разные special form'ы могут идти вместе:
-
username: ReadOnly[Required[str]]-
age: NotRequired[Annotated[ReadOnly[int], Validate(min=18, max=120)]]- и тд в любых комбинациях
Внутри
TypedDict появились специальные атрибуты: __readonly_keys__ и __mutable_keys__:
>>> from typing import TypedDict, ReadOnly
>>> class User(TypedDict):
... username: ReadOnly[str]
... age: int
...
>>> User.__readonly_keys__
frozenset({'username'})
>>> User.__mutable_keys__
frozenset({'age'})
4. Какие делатали типизации важны?
Помимо очевидного запрета на изменение
ReadOnly ключей, нужно помнить, про отношение подтипов.Пример:
User = TypedDict('User', {'username': ReadOnly[str]})
MutableUser = TypedDict('MutableUser', {'username': str})
def accepts_user(user: User): ...
def accepts_mutable_user(user: MutableUser): ...
ro_user: User
mut_user: MutableUser
# MutableUser является подвидом User, но User не является подвидом MutableUser
accepts_user(mut_user) # ok
accepts_mutable_user(ro_user) # error: expected: MutableUser, given: User
Но почему?
Потому что тело функции
accepts_mutable_user может выглядеть как-то так:
def accepts_mutable_user(user: MutableUser):
user['username'] = ''
Таким образом – мы могли бы допустить ошибку и изменить "неизменяемый" ключ.
Ждём? 🤔