Learning Platform
Глоссарий Troubleshooting
Урок 08.04 · 22 мин
Средний
Runtime introspectionget_type_hintsinspect.signaturedataclasses.fieldsPEP 526Pitfall 11Frameworks

Runtime introspection: get_type_hints, inspect.signature, dataclasses.fields

Type hints runtime-introspectable — это main отличие от чисто-static type systems (TypeScript, Flow). Python хранит аннотации как obj.__annotations__ dictionary, и stdlib даёт 3 канонических API чтобы их достать:

  1. typing.get_type_hints(func_or_class) — resolves аннотации к real types (handles forward refs, from __future__ import annotations).
  2. inspect.signature(func).parameters — Parameter objects с .annotation field.
  3. dataclasses.fields(cls) — Field objects с .name / .type для @dataclass.

Эти три API — основа для frameworks типа FastAPI (validates request body против type hints), Pydantic (runtime model construction), SQLAlchemy ORM (declarative columns), pytest (fixture resolution через signature).

В этом уроке:

  1. __annotations__ — низкоуровневый storage для type hints (PEP 526).
  2. typing.get_type_hints(...) — high-level API resolving forward references.
  3. inspect.signature(...) — Parameter introspection.
  4. dataclasses.fields(...) — Field introspection (cross-link M04 урок 05 — @dataclass).
  5. Pitfall 11from __future__ import annotations ломает runtime introspection clarity.
  6. Code challenge py-m07-04-code-1 — extract typed metadata из @dataclass.

__annotations__ — низкоуровневый storage

Python хранит type hints в __annotations__ атрибуте функций, классов и модулей (PEP 526). Это обычный dict:

def parse_age(s: str) -> int | None:
    return int(s) if s.isdigit() else None


print(parse_age.__annotations__)
# {'s': <class 'str'>, 'return': int | None}

Класс с annotations:

class User:
    id: int
    name: str
    email: str

    def greet(self, prefix: str = 'Hi') -> str:
        return f'{prefix}, {self.name}!'


print(User.__annotations__)
# {'id': <class 'int'>, 'name': <class 'str'>, 'email': <class 'str'>}

print(User.greet.__annotations__)
# {'prefix': <class 'str'>, 'return': <class 'str'>}

__annotations__ хранит raw runtime values (типы как объекты). Это работает для immediate типов: int, str, list[int]. Для forward references (типы упомянутые до definition) — нужен get_type_hints().

Cite: PEP 526 — Syntax for Variable Annotations; Lib/inspect.py reads __annotations__ для signature.


typing.get_type_hints(...) — resolve forward refs

Если type hint — строка (forward reference), __annotations__ хранит её как str — нужно eval’ить чтобы получить real type:

class Node:
    """Linked list node."""
    value: int
    next: 'Node | None'      # forward ref — Node не определён ещё


# raw __annotations__:
print(Node.__annotations__)
# {'value': <class 'int'>, 'next': 'Node | None'}    ← string!

Forward ref — 'Node | None' хранится как строка. Чтобы получить real type — typing.get_type_hints:

from typing import get_type_hints

resolved = get_type_hints(Node)
print(resolved)
# {'value': <class 'int'>, 'next': __main__.Node | None}    ← real types

get_type_hints использует eval() под капотом для resolution forward refs — поэтому передавайте класс или функцию (она знает свой __globals__ namespace), не raw __annotations__ dict.

from typing import get_type_hints


def fetch(uid: int) -> 'list[User]':       # forward ref via string
    return []


hints = get_type_hints(fetch)
print(hints)                               # {'uid': <class 'int'>, 'return': list[User]}

Cite: Lib/typing.pyget_type_hints function (~50 lines, handles forward refs, classes, functions, methods).


inspect.signature(...) — Parameter introspection

inspect.signature(callable) returns a Signature object с .parametersOrderedDict[str, Parameter]. Каждый Parameter имеет .annotation, .default, .kind:

import inspect


def query(uid: int, *, name: str | None = None, limit: int = 10) -> list[dict]:
    return []


sig = inspect.signature(query)
print(sig)
# (uid: int, *, name: str | None = None, limit: int = 10) -> list[dict]


for name, param in sig.parameters.items():
    print(f'  {name}: annotation={param.annotation}, '
          f'default={param.default!r}, kind={param.kind}')

# Output:
#   uid: annotation=<class 'int'>, default=<inspect._empty>, kind=POSITIONAL_OR_KEYWORD
#   name: annotation=str | None, default=None, kind=KEYWORD_ONLY
#   limit: annotation=<class 'int'>, default=10, kind=KEYWORD_ONLY


# Return annotation:
print(sig.return_annotation)        # list[dict]

Parameter.kind — enum описывающий callability:

KindMeaning
POSITIONAL_ONLYДо / в signature: def f(x, /, y)x positional-only
POSITIONAL_OR_KEYWORDDefault — def f(x) — может быть positional или keyword
VAR_POSITIONAL*args
KEYWORD_ONLYПосле * в signature: def f(*, x)x keyword-only
VAR_KEYWORD**kwargs

Это даёт полный introspection: имя, default, kind, аннотация для каждого параметра. Используется pytest (для fixture resolution), FastAPI (для DI / request parsing), Click (для CLI argument generation).

Cite: Lib/inspect.pySignature.from_callable builds signature from __annotations__ + function __defaults__.


dataclasses.fields(cls) — Field introspection

@dataclass (M04 урок 05) генерирует __init__/__repr__/__eq__ через codegen. Он также делает класс introspectable через dataclasses.fields(cls):

from dataclasses import dataclass, fields


@dataclass(frozen=True, slots=True)
class User:
    id: int
    name: str
    age: int | None = None
    tags: list[str] | None = None


# fields() returns tuple[Field, ...]
for f in fields(User):
    print(f'  {f.name}: type={f.type}, default={f.default}')

# Output:
#   id: type=<class 'int'>, default=<factory ...>
#   name: type=<class 'str'>, default=<factory ...>
#   age: type=int | None, default=None
#   tags: type=list[str] | None, default=None

Field object имеет:

AttributeTypeMeaning
.namestrИмя поля
.typetype или string forward-refAnnotation
.defaultvalue or dataclasses.MISSINGDefault value
.default_factorycallable or MISSINGFactory для mutable defaults
.reprboolВключается ли в __repr__
.compareboolИспользуется в __eq__?
.metadataMapping[Any, Any]User-attached metadata (immutable proxy)

Cross-link M04 урок 05: @dataclass(frozen=True, slots=True) — frozen для immutability + hashable, slots для memory efficiency. Введя type hints — dataclasses.fields(User) enumeratable. Это делает @dataclass основой для frameworks (Pydantic, attrs наследуют похожий API):

import json


def to_dict(obj) -> dict:
    """Serialize @dataclass instance to dict (introspection через fields())."""
    return {f.name: getattr(obj, f.name) for f in fields(obj)}


user = User(id=1, name='alice', age=30, tags=['admin', 'verified'])
print(to_dict(user))
# {'id': 1, 'name': 'alice', 'age': 30, 'tags': ['admin', 'verified']}

print(json.dumps(to_dict(user)))
# {"id": 1, "name": "alice", "age": 30, "tags": ["admin", "verified"]}

Это 8 строк вместо 50-line custom serializer. Это и есть power runtime introspection — frameworks build на этих 3 APIs.

Cite: Lib/dataclasses.pyfields() function returns tuple(getattr(class_or_instance, _FIELDS).values()) — internal _FIELDS dict accumulated through @dataclass decorator.


Pitfall 11 — from __future__ import annotations

PEP 563 предложил postponed evaluation annotations: from __future__ import annotations делает все type hints строками (не evaluating). Это улучшает import-time performance и solves forward-ref problems syntactically:

from __future__ import annotations

class Node:
    value: int
    next: Node | None         # OK — Node ещё не определён, но это string


print(Node.__annotations__)
# {'value': 'int', 'next': 'Node | None'}      ← все strings!

Trade-off — runtime introspection становится pain:

  1. __annotations__ хранит строки'int' вместо <class 'int'>. Field.type тоже string.
  2. get_type_hints() всё ещё работает — он eval’ит strings — но performance cost reach 100x для больших modules.
  3. Frameworks полагающиеся на Field.type типа object ломаются — Pydantic, FastAPI обходят это специальной логикой (recursive eval’ом).

Pitfall 11: from __future__ import annotations выглядит красиво (forward-refs everywhere, no quoting), но breaks runtime introspection clarity. В M07 курс рекомендуется не использовать — Python 3.13 отказался от PEP 563 как default; PEP 649 (deferred evaluation of annotations using descriptors) — default в Python 3.14 (released Oct 7, 2025): annotations evaluated lazily через __annotate__ функции и annotationlib module, без превращения в строки. Это решает forward-ref problem без потери runtime introspection clarity.

# WITHOUT from __future__ — runtime types accessible
from dataclasses import dataclass, fields

@dataclass
class User:
    id: int
    name: str

print(fields(User)[0].type)          # <class 'int'>          ← real type


# WITH from __future__ — types as strings (Pitfall 11)
# from __future__ import annotations
# @dataclass
# class User:
#     id: int
#     name: str
# print(fields(User)[0].type)        # 'int'                   ← string

Pragmatic rule в M07/M08: не пишите from __future__ import annotations. Используйте:

  • PEP 604 unions (int | None) — no need for forward refs во многих случаях.
  • String forward refs только для circular refs ('Node | None' quoted) — explicit и readable.
  • get_type_hints() для resolution когда нужно.

Cite: PEP 563 — Postponed Evaluation of Annotations; PEP 649 — Deferred evaluation of annotations using descriptorsdefault в Python 3.14; PEP 749 — Implementing PEP 649; Python 3.14 What’s New — annotations.


В M04 урок 05 мы установили: @dataclass генерирует __init__/__repr__/__eq__ через codegen. Type hints — обязательная часть API. Сейчас connects fields():

from dataclasses import dataclass, fields


@dataclass(frozen=True, slots=True)
class Config:
    """M04 урок 05 pattern + type hints — fully introspectable."""
    host: str
    port: int
    debug: bool = False
    tags: tuple[str, ...] = ()


# Production introspection — построить configuration validator:
def validate(cfg: Config) -> list[str]:
    """Validate Config invariants. Return list of error messages."""
    errors = []
    for f in fields(cfg):
        value = getattr(cfg, f.name)
        if f.type is int and not isinstance(value, int):
            errors.append(f'{f.name}: expected int, got {type(value).__name__}')
        if f.type is str and not isinstance(value, str):
            errors.append(f'{f.name}: expected str, got {type(value).__name__}')
    return errors


cfg = Config(host='localhost', port=8080, debug=True, tags=('prod', 'eu'))
print(validate(cfg))                       # [] — all OK


# Hashable благодаря frozen — можно использовать как dict key:
configs: dict[Config, str] = {cfg: 'production'}
print(configs[cfg])                        # 'production'

@dataclass(frozen=True, slots=True) (M04 урок 05) + type hints (M07 урок 01) + introspection (M07 урок 04) — basis для frameworks. Так Pydantic и attrs работают.

Cite: Lib/dataclasses.py — fields() и Field class; PEP 557 — Data Classes.


Diagram: 3 introspection APIs

Runtime introspection — три инструмента
get_type_hints(callable)resolved annotationstyping.get_type_hints — high-level API. Returns dict[str, type]. Resolves forward refs через eval(). Handles function annotations, class annotations, methods. Standard для frameworks (Pydantic, FastAPI). Cite Lib/typing.py
inspect.signature(fn).parametersParameter objectsinspect.signature — обёртка над __annotations__ + __defaults__ + __code__ flags. Каждый Parameter имеет .annotation, .default, .kind (POSITIONAL_OR_KEYWORD / KEYWORD_ONLY / VAR_KEYWORD / etc). Используется pytest fixture resolution, FastAPI DI. Cite Lib/inspect.py
dataclasses.fields(cls)Field objectsdataclasses.fields — для @dataclass классов. Returns tuple[Field, ...] с .name, .type, .default, .metadata. M04 урок 05 cross-link: @dataclass(frozen=True, slots=True) + type hints + fields() = framework basis. Cite Lib/dataclasses.py

Recipe: typed configuration validator (full pattern)

End-to-end production pattern — combine all three APIs:

from dataclasses import dataclass, fields
from typing import get_type_hints
import inspect


@dataclass
class APIConfig:
    base_url: str
    timeout_seconds: int = 30
    retry_count: int = 3
    debug: bool = False


def describe_config(cls) -> dict[str, dict[str, str]]:
    """Extract metadata: name, type, default per field."""
    result = {}
    hints = get_type_hints(cls)
    for f in fields(cls):
        result[f.name] = {
            'type': str(hints[f.name]),
            'default': repr(f.default) if f.default is not inspect.Parameter.empty else 'required',
        }
    return result


# Use:
import json
print(json.dumps(describe_config(APIConfig), indent=2))
# {
#   "base_url": {"type": "<class 'str'>", "default": "<factory ...>"},
#   "timeout_seconds": {"type": "<class 'int'>", "default": "30"},
#   "retry_count": {"type": "<class 'int'>", "default": "3"},
#   "debug": {"type": "<class 'bool'>", "default": "False"}
# }

Это 6-line introspector — основа для doc generation, CLI auto-flags, validation. Frameworks типа click, attrs, Pydantic строят свои public APIs на этих 3 stdlib utilities.


Ключевые выводы

  1. __annotations__ — низкоуровневый dict хранящий type hints на функциях/классах/модулях (PEP 526). Raw access — не рекомендуется в production.
  2. typing.get_type_hints(callable) — high-level API. Resolves forward refs (string annotations) через eval(). Handles functions, classes, methods. Cite Lib/typing.py.
  3. inspect.signature(callable).parameters — Parameter objects с .annotation, .default, .kind. Используется pytest, FastAPI, Click. Cite Lib/inspect.py.
  4. dataclasses.fields(cls) — Field objects с .name, .type, .default, .metadata для @dataclass. Cross-link M04 урок 05 — frozen + slots + typed = framework basis. Cite Lib/dataclasses.py.
  5. Pitfall 11from __future__ import annotations (PEP 563) делает все аннотации строками. Breaks runtime introspection clarity — Field.type будет 'int' (string), не <class 'int'>. Не используйте в M07/M08; Python 3.13 не делает default; PEP 649 — descriptor-based alternative.
  6. Frameworks (Pydantic, FastAPI, attrs, SQLAlchemy ORM) build на этих 3 APIs. Понимая их, вы понимаете как runtime validation works.
  7. Code challenge py-m07-04-code-1 — расширение @dataclass introspection: extract list of f"{f.name}: {f.type}" для каждого field. Pattern 1 из 67-RESEARCH lines 636-657.

Дальше — урок 05 — type narrowing (isinstance, TypeGuard, assert_type) — как mypy “сужает” union types до specific type внутри if блока.

Проверьте понимание

Результат: 0 из 0
Концептуальный
Вопрос 1 из 4. Какой stdlib API canonical для **resolution forward references** в type hints (e.g., `'Node | None'` quoted string)?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 7