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 чтобы их достать:
typing.get_type_hints(func_or_class)— resolves аннотации к real types (handles forward refs,from __future__ import annotations).inspect.signature(func).parameters— Parameter objects с.annotationfield.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).
В этом уроке:
__annotations__— низкоуровневый storage для type hints (PEP 526).typing.get_type_hints(...)— high-level API resolving forward references.inspect.signature(...)— Parameter introspection.dataclasses.fields(...)— Field introspection (cross-link M04 урок 05 — @dataclass).- Pitfall 11 —
from __future__ import annotationsломает runtime introspection clarity. - 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.py — get_type_hints function (~50 lines, handles forward refs, classes, functions, methods).
inspect.signature(...) — Parameter introspection
inspect.signature(callable) returns a Signature object с .parameters — OrderedDict[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:
| Kind | Meaning |
|---|---|
POSITIONAL_ONLY | До / в signature: def f(x, /, y) — x positional-only |
POSITIONAL_OR_KEYWORD | Default — 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.py — Signature.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 имеет:
| Attribute | Type | Meaning |
|---|---|---|
.name | str | Имя поля |
.type | type или string forward-ref | Annotation |
.default | value or dataclasses.MISSING | Default value |
.default_factory | callable or MISSING | Factory для mutable defaults |
.repr | bool | Включается ли в __repr__ |
.compare | bool | Используется в __eq__? |
.metadata | Mapping[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.py — fields() 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:
__annotations__хранит строки —'int'вместо<class 'int'>.Field.typeтоже string.get_type_hints()всё ещё работает — онeval’ит strings — но performance cost reach 100x для больших modules.- 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 descriptors — default в Python 3.14; PEP 749 — Implementing PEP 649; Python 3.14 What’s New — annotations.
Cross-link M04 урок 05 — @dataclass typed introspection
В 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
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.
Ключевые выводы
__annotations__— низкоуровневый dict хранящий type hints на функциях/классах/модулях (PEP 526). Raw access — не рекомендуется в production.typing.get_type_hints(callable)— high-level API. Resolves forward refs (string annotations) черезeval(). Handles functions, classes, methods. CiteLib/typing.py.inspect.signature(callable).parameters— Parameter objects с.annotation,.default,.kind. Используется pytest, FastAPI, Click. CiteLib/inspect.py.dataclasses.fields(cls)— Field objects с.name,.type,.default,.metadataдля@dataclass. Cross-link M04 урок 05 — frozen + slots + typed = framework basis. CiteLib/dataclasses.py.- Pitfall 11 —
from __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. - Frameworks (Pydantic, FastAPI, attrs, SQLAlchemy ORM) build на этих 3 APIs. Понимая их, вы понимаете как runtime validation works.
- Code challenge
py-m07-04-code-1— расширение@dataclassintrospection: extract list off"{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 блока.