TypedDict в Python: типизированные словари

17 июля 2026
Автор

Антон Ларичев

Что такое TypedDict

Python-словари (dict) по умолчанию не несут никакой информации о том, какие ключи в них хранятся и какого типа их значения. Статические анализаторы вроде mypy или pyright не могут проверить корректность обращения к таким словарям, а IDE не подскажет автодополнение.

TypedDict — механизм из модуля typingtyping_extensions для старых версий Python), который позволяет описать схему словаря: какие ключи обязательны, какие необязательны, и какого типа каждое значение. На уровне интерпретатора такой словарь остаётся обычным dict — никакой дополнительной памяти или overhead-а. Вся проверка происходит только статически.

from typing import TypedDict

class User(TypedDict):
    id: int
    name: str
    email: str

Теперь mypy знает, что user["id"] — это int, а попытка обратиться к user["phone"] будет помечена как ошибка.

Синтаксис объявления

Есть два равнозначных способа создать TypedDict.

Класс-наследник TypedDict

from typing import TypedDict

class Product(TypedDict):
    sku: str
    price: float
    in_stock: bool

Этот стиль предпочтителен: он хорошо читается, поддерживает наследование и работает с большинством инструментов.

Функциональный синтаксис

from typing import TypedDict

Product = TypedDict("Product", {"sku": str, "price": float, "in_stock": bool})
# или через keyword-аргументы
Product = TypedDict("Product", sku=str, price=float, in_stock=bool)

Функциональный синтаксис нужен, когда имя ключа совпадает с зарезервированным словом Python (например, class или return). В остальных случаях класс-наследник чище.

Использование TypedDict

from typing import TypedDict

class Point(TypedDict):
    x: float
    y: float

def distance(p: Point) -> float:
    return (p["x"] ** 2 + p["y"] ** 2) ** 0.5

origin: Point = {"x": 0.0, "y": 0.0}
print(distance(origin))  # 0.0

point: Point = {"x": 3.0, "y": 4.0}
print(distance(point))  # 5.0

Можно передавать TypedDict туда, где ожидается dict[str, Any] — обратная совместимость полная. Но dict[str, Any] нельзя передать туда, где ожидается конкретный TypedDict без явного приведения типа.

Обязательные и необязательные поля

По умолчанию все поля TypedDict считаются обязательными. Если при создании словаря пропустить хотя бы одно — mypy выдаст ошибку.

total=False: все поля необязательные

from typing import TypedDict

class UserPatch(TypedDict, total=False):
    name: str
    email: str
    age: int

patch: UserPatch = {"email": "new@example.com"}  # OK

Параметр total=False делает все поля необязательными сразу. Это удобно для PATCH-запросов, настроек с дефолтами и любых частичных обновлений.

Смешивание обязательных и необязательных полей

Чтобы иметь и обязательные, и необязательные поля в одном TypedDict, используют наследование:

from typing import TypedDict

class UserBase(TypedDict):
    id: int
    name: str

class UserOptional(TypedDict, total=False):
    age: int
    bio: str

class User(UserBase, UserOptional):
    pass

user: User = {"id": 1, "name": "Alice"}                  # OK
user_full: User = {"id": 2, "name": "Bob", "age": 30}    # OK

Начиная с Python 3.11 появился аннотатор Required / NotRequired, который позволяет указывать обязательность прямо на уровне поля:

from typing import TypedDict, NotRequired, Required

class Config(TypedDict, total=False):
    host: Required[str]   # обязательно, несмотря на total=False
    port: int             # необязательно
    debug: NotRequired[bool]  # явно необязательно

Это самый выразительный способ — не нужно разбивать описание на несколько классов.

Наследование TypedDict

TypedDict поддерживает одиночное и множественное наследование от других TypedDict:

from typing import TypedDict

class Address(TypedDict):
    street: str
    city: str
    country: str

class Company(TypedDict):
    name: str
    inn: str

class CompanyWithAddress(Company, Address):
    pass

contractor: CompanyWithAddress = {
    "name": "Рога и копыта",
    "inn": "1234567890",
    "street": "ул. Пушкина",
    "city": "Москва",
    "country": "Россия",
}

При наследовании дочерний тип содержит все поля родительских. Нельзя переопределить поле с несовместимым типом — mypy это запрещает.

Вложенные TypedDict

Python не ограничивает глубину вложенности:

from typing import TypedDict

class Coordinates(TypedDict):
    lat: float
    lon: float

class City(TypedDict):
    name: str
    population: int
    center: Coordinates

city: City = {
    "name": "Москва",
    "population": 12_500_000,
    "center": {"lat": 55.7558, "lon": 37.6173},
}

print(city["center"]["lat"])  # 55.7558

Mypy знает типы на каждом уровне вложенности и проверит их все.

Работа с TypedDict в функциях

Передача и возврат

from typing import TypedDict

class Order(TypedDict):
    order_id: str
    amount: float
    currency: str

def format_order(order: Order) -> str:
    return f"#{order['order_id']}: {order['amount']} {order['currency']}"

def make_order(order_id: str, amount: float) -> Order:
    return {"order_id": order_id, "amount": amount, "currency": "RUB"}

order = make_order("ORD-001", 1500.0)
print(format_order(order))  # #ORD-001: 1500.0 RUB

Работа с get() и pop()

Обращение через get() возвращает Optional от типа поля. Mypy отслеживает это корректно:

from typing import TypedDict

class Config(TypedDict, total=False):
    timeout: int
    retries: int

def apply_config(cfg: Config) -> None:
    timeout = cfg.get("timeout", 30)  # тип: int
    retries = cfg.get("retries", 3)   # тип: int
    print(f"timeout={timeout}, retries={retries}")

isinstance-проверки не работают

Поскольку TypedDict — это только аннотация, isinstance(data, User) вызовет TypeError в runtime. Для runtime-валидации используют библиотеки вроде pydantic или пишут явную валидацию вручную.

# Это вызовет ошибку:
# isinstance(data, User)  # TypeError: Subscripted generics cannot be used with class and instance checks

# Правильно — проверять наличие ключей вручную:
def is_valid_user(data: dict) -> bool:
    return (
        isinstance(data.get("id"), int)
        and isinstance(data.get("name"), str)
    )

TypedDict и JSON

Один из самых частых сценариев — описание структуры JSON-ответов API:

import json
from typing import TypedDict

class GithubRepo(TypedDict):
    id: int
    name: str
    full_name: str
    private: bool
    html_url: str
    description: str | None
    stargazers_count: int
    forks_count: int

def parse_repo(raw: str) -> GithubRepo:
    data: GithubRepo = json.loads(raw)
    return data

raw_json = '{"id": 1, "name": "my-repo", "full_name": "user/my-repo", "private": false, "html_url": "https://github.com/user/my-repo", "description": null, "stargazers_count": 42, "forks_count": 5}'
repo = parse_repo(raw_json)
print(repo["stargazers_count"])  # 42

Mypy проверит все обращения к полям, но не проверит реальное содержимое JSON — это зона ответственности разработчика или runtime-валидатора.

TypedDict и списки

from typing import TypedDict

class Tag(TypedDict):
    id: int
    label: str

class Article(TypedDict):
    title: str
    body: str
    tags: list[Tag]

article: Article = {
    "title": "TypedDict в Python",
    "body": "...",
    "tags": [
        {"id": 1, "label": "python"},
        {"id": 2, "label": "typing"},
    ],
}

for tag in article["tags"]:
    print(tag["label"])  # mypy знает, что label — str

Сравнение с dataclass и NamedTuple

Критерий TypedDict dataclass NamedTuple
Runtime-тип dict кастомный класс кортеж
Совместимость с dict API полная нет нет
Атрибутный доступ нет (obj["key"]) да (obj.key) да (obj.key)
Runtime-валидация нет нет (по умолчанию) нет
Сериализация в JSON тривиальная нужен конвертер нужен конвертер
Изменяемость изменяемый изменяемый неизменяемый
Наследование да да ограниченное

TypedDict побеждает там, где данные уже приходят в виде словарей — из JSON, из конфигов, из БД. Если данные создаются программно и нужен полноценный ООП-объект — лучше dataclass.

Практический пример: типизация конфига приложения

from typing import TypedDict, NotRequired
import json
import os

class DatabaseConfig(TypedDict):
    host: str
    port: int
    name: str
    user: str
    password: str

class LoggingConfig(TypedDict):
    level: str
    format: NotRequired[str]

class AppConfig(TypedDict):
    debug: bool
    database: DatabaseConfig
    logging: LoggingConfig
    allowed_hosts: list[str]

def load_config(path: str) -> AppConfig:
    with open(path) as f:
        cfg: AppConfig = json.load(f)
    return cfg

def get_db_url(cfg: AppConfig) -> str:
    db = cfg["database"]
    return f"postgresql://{db['user']}:{db['password']}@{db['host']}:{db['port']}/{db['name']}"

# Пример конфига (config.json):
# {
#   "debug": true,
#   "database": {
#     "host": "localhost",
#     "port": 5432,
#     "name": "myapp",
#     "user": "admin",
#     "password": "secret"
#   },
#   "logging": {"level": "INFO"},
#   "allowed_hosts": ["localhost", "127.0.0.1"]
# }

Совместимость версий Python

  • Python 3.8+: TypedDict доступен из typing
  • Python 3.9+: можно использовать list[str] вместо List[str] в аннотациях
  • Python 3.10+: поддерживается синтаксис str | None вместо Optional[str]
  • Python 3.11+: добавлены Required и NotRequired

Для Python 3.7 и ниже используйте typing_extensions:

try:
    from typing import TypedDict, NotRequired, Required
except ImportError:
    from typing_extensions import TypedDict, NotRequired, Required

Проверка типов с mypy

Для запуска проверки:

pip install mypy
mypy your_file.py

Пример ошибок, которые поймает mypy:

from typing import TypedDict

class User(TypedDict):
    id: int
    name: str

user: User = {"id": "not_an_int", "name": "Alice"}  # error: str не int
user2: User = {"id": 1}                              # error: пропущен ключ name
print(user["phone"])                                 # error: ключ phone не существует

Все три ошибки mypy поймает до запуска программы, что существенно снижает количество дефектов в production.

Итоги

TypedDict — минималистичный инструмент для добавления статической типизации к словарям без изменения их runtime-поведения. Он особенно полезен при работе с JSON API, конфигурационными файлами и любыми данными, которые уже представлены в виде dict. Используйте total=False и NotRequired для необязательных полей, наследование для переиспользования схем, и mypy для автоматической проверки корректности.

Чтобы глубже разобраться с типизацией и другими возможностями Python, приходите на курс Python на PurpleSchool.

Python collections — Counter, defaultdict, OrderedDictСтрелочка вправо

Постройте личный план изучения Python до уровня Middle — бесплатно!

Python — часть карты развития Backend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Все гайды по Python

Как отправлять запросы с помощью requests в PythonПочему Python выводит значение без команды printКак работает команда print в PythonВозможности Python для автоматизации задачРабота с JSON в Python на примерахPython get — методы получения данныхКак находить и исправлять ошибки в PythonРабота с данными через API и внешние сервисыСтруктура и оформление кода PythonОсновы Django с PythonПолезные приёмы в Python для повседневной работыИспользование locals в Python для отладкиИнтеграция PHP и PythonКак выполнять HTTPS-запросы в PythonFastAPI Python — быстрый старт: создание REST API с нуляКак работать с API в Python
Ввод целого числа в PythonВедение логов в PythonУдаление данных в Python с помощью removeОбработка исключений с помощью try/except в PythonФункция super() в Python — как вызвать метод родителяСоздание собственных контекстных менеджеров в PythonРабота с символами программирования PythonРабота с переменной X в PythonРабота с классами в PythonКак скачать Python на компьютерПростая программа на Python для начинающихОсновы Python для тех, кто начинаетЧто нового в Python 3Поддерживается ли Python 2 и стоит ли его использоватьPython 1 — с чего начиналась история языкаКоманда python print - полное руководство по выводу данныхПравила именования переменных в PythonПользовательские исключения в PythonОсновы Python coreОписание объектов PythonНаследование классов в Python — основы и примерыМножественное наследование в Python — примеры и MROКонтекстный менеджер with в Python — как работает и зачем нуженКомментарии в Python — однострочные, многострочные и docstringКакой Python выбрать для установкиКак вывести целое число с помощью print в PythonКак установить Python на Windows macOS и LinuxКак пользоваться консолью PythonКак получить последний элемент в PythonКак найти значение в PythonКак настроить PythonКак использовать print для строк в PythonКак работает интерпретатор PythonИнструкция по работе с PythonБлок finally в обработке исключений PythonЦелые числа в PythonАбстрактные классы в Python — ABC и abstractmethod
Pydantic: валидация данных в Python
Загрузка данных PythonУправление проектами на GitHub с PythonСоздание веб-приложений на Flask PythonСоздание бота на PythonСоздание интерфейсов Python QTСоздание игр с PygameСоздание GUI в PythonКак работать со словарями в PythonРабота с библиотеками через Python PackagingРабота со временем в Python при помощи модуля timePython name — особенности переменнойМатематические операции в Python с модулем mathPython listing — что это и как использоватьPytest — тестирование на Python: полное руководствоОбработка изображений с OpenCV PythonNumPy в Python — основы и применение в задачахМашинное обучение с PythonИспользование Anaconda с PythonМодуль contextlib в Python — утилиты для контекстных менеджеровБиблиотеки Python и их применение в проектах
Возврат значений из функции в PythonВложенные функции в PythonСоздание собственных декораторов в PythonРабота с функцией map в PythonЦикл while в Python и примеры использованияОбработка чисел, введённых через input в PythonОсновные операторы в Python с примерамиУсловные выражения if else в Python для начинающихКак выполняется вызов функций call в PythonПродвинутые генераторы в Python — send, throw, close и корутиныПозиционные и именованные аргументы в PythonОбъявление переменных и управление областью видимости в PythonПередача аргументов по ссылке и по значению в PythonПередача аргументов через args и kwargs в PythonОсновные методы Python и примеры их использованияОператор match/case в Python 3.10+ — основы структурного сопоставленияПаттерны match/case в Python — деструктуризация, guard и вложенные шаблоныПрактические примеры match/case в Python — реальные сценарии примененияЛокальные и глобальные переменные в PythonЧасто используемые команды PythonКлючевые слова global и nonlocal в PythonКак создавать функции в PythonКак работает сборщик мусора в PythonКак работает область видимости переменных в PythonКак работает функция callable в PythonКак работает функция any и all в PythonКак проверить тип переменной в PythonКак передать функцию как аргумент в PythonКак использовать функцию isinstance в PythonКак использовать функцию filter в PythonКак использовать функцию filter в PythonКак использовать функцию eval безопасно в PythonКак использовать декораторы в PythonИзменяемые и неизменяемые типы данных в PythonГенераторы и yield в Python — как создавать и использоватьГенераторные выражения в Python — синтаксис и примерыФункции в Python и способы их вызоваФункции как объекты в PythonЧто такое замыкания в PythonЧто делает функция reduce в PythonЧто делает функция id в PythonАргументы по умолчанию в PythonАнонимные функции и lambda в PythonАлгоритмы на Python — примеры и объяснение
Запись данных в PythonУстановка pip в PythonУправление зависимостями requirement в PythonУправление библиотеками с помощью Python PackagingУдаление пробелов с помощью strip в PythonСтруктурирование кода в PythonСоздание исполняемого файла Python в exeРазбор traceback в модуле PythonРазбор site-packages в PythonРазбор Program Files в PythonРабота с Unicode кодировками в PythonРабота с системными функциями Python sysРабота с папкой AppData в PythonРабота с модулем logging в PythonРабота с каталогами в PythonРабота с CSV в PythonВиртуальная среда venv в Python — создание и настройкаКак создать простое приложение на PythonИспользование pip в Python для установки пакетовМодули в Python и организация кода в проектеИмпорт модулей в Python и правила подключенияРабота с файлами в Python пошаговоЧто делает компилятор Python и как он работаетПолучение строки из модуля PythonПодключение файлов в Python с includeПеременные среды в PythonСборка проекта с помощью packaging в PythonНастройка Python сервераИспользование Python на UbuntuИспользование консоли PythonИспользование кодировок в PythonИнициализация пакетов PythonИмпорт модулей PythonИмпорт имен в PythonСреда IDLE Python и базовые возможностиЧтение и запись TXT в PythonЧтение файлов в Python с помощью open file
Удаление элементов из списка PythonТипы данных в Python — обзор и рекомендацииОсновные операции со строками в PythonМетоды str в Python и обработка текстаСписки в Python и их ключевые методыСоздание списков данных в PythonРабота со строками и символами в PythonРабота со столбцами в PythonРабота со списком значений в PythonРабота с таблицами в Python с помощью DataFrameРабота с RFR в PythonРабота с пробелами в PythonРабота с массивами в PythonРабота с кортежами tuple PythonРабота с координатами X и Y в PythonРабота с ключами в PythonРабота с элементами данных PythonРабота с двоичными числами PythonРабота с данными в PythonРабота с данными NumPy PythonРабота с большими числами в PythonРабота с битами в PythonРабота с байтами в PythonЧто такое значение в Python и как его определитьМножества в Python и операции с нимиИспользование range в Python для цикловПроверка на четность в PythonПроверка числа в PythonПреобразование типов в PythonПреобразование списка в строку PythonПреобразование числа в строку в PythonПостроение графиков в PythonОпределение индекса элемента в PythonОкругление чисел в PythonОбъединение списков в Python с помощью zipМножества в PythonМассивы в Python и отличие от списковМассив чисел в PythonКортежи данных в PythonКак вычислить сумму чисел в PythonКак получить остаток от деления в PythonКак найти следующее число в PythonИспользование Unicode в PythonТип int в Python и его особенностиИндекс списка в PythonФункции для работы со строками в PythonЭлементы Python и способы доступа к нимДоступ к элементам массива в PythonДеление чисел в PythonРабота с данными в Python на практикеКак работать с числами в Python
Открыть базу знаний

Лучшие курсы по теме

Иконка молнииНовый
изображение курса

Основы Python

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Nest.js с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.6
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

Docker и Ansible

Антон Ларичев
AI-тренажерыAI-тренажеры
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий