Standardized Reflection Objects for Python Callables

Python callable introspection currently feels surprisingly fragmented.

Extracting structured metadata from a function often requires combining APIs from many different places:

inspect.signature()
typing.get_type_hints()

func.__annotations__
func.__defaults__
func.__kwdefaults__
func.__code__.co_flags

inspect.iscoroutinefunction()
inspect.getsource()

Some information lives in:

  • inspect
  • typing
  • types
  • function dunder attributes
  • code objects
  • various constants/flags

This works, but it feels fairly low-level and inconsistent.

Modern Python tooling increasingly depends on callable metadata:

  • FastAPI-like frameworks
  • CLI generators
  • RPC systems
  • dependency injection
  • schema generation
  • IDE tooling
  • LLM/agent frameworks

Most of these projects seem to end up building their own normalization layer over existing APIs.

I’m wondering whether Python could benefit from a more structured high-level callable introspection API, perhaps something conceptually like:

r = inspect.reflect(func)

r.signature
r.parameters
r.return_type
r.capabilities.async_callable
r.doc
r.source

instead of directly interacting with many separate APIs and implementation details.

Importantly, I’m not suggesting runtime mutation/reflection features or AST rewriting. This would mainly be a structured introspection layer over existing metadata.

Has something like this already been explored before?

Would this make sense as an enhancement to inspect, or is the current API surface considered sufficient?

inspect.signature provides most of this already, and it would be the natural place to extend if something is missing.

4 Likes

Speaking about standardization and missing features - in my older post (Unified way of introspecting callables) I wrote about the inspect module is not treating all callables uniformly and I proposed that a minimal common base should be defined. There should be a clearly defined set of special attributes like __name__, __qualname__, __docs__, __module__, __annotations__ that can be safely accessed no matter what subtype of a callable you have.

That’s true.
So why not we make it returns a FunctionDescriptor which contains anything about the function’s metadata.

sure but you have to do something like this:

if isinstance(obj, type):
    obj.__qualname__
else:
    obj.__class__.__qualname__

That would break code expecting to e.g. filter out builtin objects by not being able to access certain attributes. If that issue can be prevented though, this would be a quite good solution imo.

It’s not so much that the information “lives” in inspect. The information is in __code__ as the co_argcount attribute and friends, and inspect contains logic that makes it much more intuitively accessible.

“Descriptor” is unfortunately ambiguous. It refers to things with __get__, or (less commonly) with __set_name__. A better name would possibly be FunctionInfo, which has precedents in inspect (the ArgInfo named tuple and FrameInfo returned by getargvalues and get*frame* functions respectively).

but did the inspect mod fixed/not fixed this problem?
in fact … i mean that this extension should work exactly like the builtin func but just make it more organized.

ok i mean a dataclass or something alike.
just like the reflection objects in java/kotlin or something, it returns a organizaed information set, instead of making you access many unrelated objects just for a func’s metadata.
nobody want to access 10 different objects in 10 modules just for a func.name

yes i mean FunctionInfo.
sorry for that .

I saw your older post and it seems we’re thinking about the same direction. Would you be open to collaborating on a more concrete proposal?

I’m wondering that whether this structure is enough for the metadata:

class FunctionInfo:
    name: str
    qualname: str
    signature: inspect.Signature
    annotations: dict[str, Any]
    defaults: tuple[Any, ...]
    kwdefaults: dict[str, Any] | None
    is_coroutine: bool
    is_async_generator: bool
    doc: str | None
    source: str | None

waiting for suggestions.

I’m planning to put together a more complete proposal. Would it be appropriate to move this to a PEP once I have a reference implementation and a draft spec?

This is a PoC of Standardized Reflection Objects for Python Callables.

Where should this be placed in?

Reference to the Jelle Zijlstra’s suggestion,
this can be inspect.mirror.

Usage

# maybe it's too long... just a part :)
from functools import partial
def add(a: int, b: int = 1) -> int:
    """add two numbers."""
    return a + b

partial_add = partial(add, 10)
print("\n-- partial --")
p = mirror(partial_add)
print(f"name        = {p.name!r}") # 'partial'
print(f"signature   = {p.signature}") # (b: int = 1) -> int
print(f"annotations = {p.annotations}") # {}
print(f"defaults    = {p.defaults}") # ()

Code

import inspect
from typing import Callable, Any
from dataclasses import dataclass


@dataclass
class FunctionInfo:
    name: str
    qualname: str
    signature: inspect.Signature | None
    annotations: dict[str, Any]
    defaults: tuple[Any, ...]
    kwdefaults: dict[str, Any] | None
    is_coroutine: bool
    is_async_generator: bool
    doc: str | None
    source: str | None


def mirror(func: Callable) -> FunctionInfo:
    def _attr(obj: Any, *names: str) -> Any:
        for n in names:
            if (val := getattr(obj, n, None)) is not None: return val
        return None

    name = _attr(func, "__name__", "__qualname__") or type(func).__name__
    qualname = _attr(func, "__qualname__", "__name__") or name

    try:
        signature = inspect.signature(func)
    except (ValueError, TypeError):
        signature = None

    annotations = _attr(func, "__annotations__") or {}

    defaults = _attr(func, "__defaults__")
    if defaults is None: defaults = ()
    kwdefaults = _attr(func, "__kwdefaults__")
    is_coroutine = inspect.iscoroutinefunction(func)
    is_async_generator = inspect.isasyncgenfunction(func)

    try:
        doc = inspect.getdoc(func)
    except (ValueError, TypeError):
        doc = None

    try:
        source = inspect.getsource(func)
    except (OSError, TypeError):
        source = None

    return FunctionInfo(
        name=name,
        qualname=qualname,
        signature=signature,
        annotations=annotations,
        defaults=defaults,
        kwdefaults=kwdefaults,
        is_coroutine=is_coroutine,
        is_async_generator=is_async_generator,
        doc=doc,
        source=source,
    )

Test

Coverage 26/27, 96%.
Cases is at UnknownUser03393/PoC_Python_inspect_mirror.

PS: I’m 13 and I wrote this in my spare time after school. If this is dumb, tell me why. If it’s not, tell me how to make it better.
I want to work this forward.

You should either pull the _attr function outside or drop the obj argument since func is always passed. Not a major issue though.

This looks quite nice as a structure. The one question I’d have is how often do you need all of this information at once? The advantage of having individual functions is that you can just get precisely what you want, and not pay the cost for getting things you don’t care about.

Optimising too early is often a bad idea, but in this case you’re competing with an existing API that’s faster.

Maybe you could make the individual attributes into properties, and calculate them on demand?

sounds like a good idea.
it’s like lazy loading? i’ll try it.

that’s right. i’m going to update.