Python inspect: Object Introspection

Published on: August 2, 2026
Reading time: 6 minutes
Software analysis representing object introspection with Python inspect

Frameworks, debuggers, plugin systems, documentation generators, and testing tools often need to discover how a Python object was defined. They check whether something is a function, class, generator, or coroutine, list members, read signatures, retrieve source code, and examine the execution stack. The Python inspect module brings these introspection operations together in the standard library.

This guide covers getmembers(), is* predicates, signature(), Signature.bind(), getsource(), unwrap(), getattr_static(), and frame functions. It complements our articles about Python descriptors, singledispatch, object copying, memory management, and contextvars.

What introspection means

Introspection is the ability of a program to examine live objects. Python functions, classes, modules, and methods carry metadata such as names, modules, documentation, annotations, and references to compiled code.

def calculate_total(value: float, rate: float = 0.1) -> float:
    """Calculate a total with a rate."""
    return value * (1 + rate)

print(calculate_total.__name__)
print(calculate_total.__doc__)
print(calculate_total.__annotations__)

inspect provides a more uniform API and tools that work with several object categories.

Identify object categories

Predicates such as isfunction(), ismethod(), isclass(), and ismodule() make intent explicit.

import inspect

class Service:
    def run(self):
        return "ok"

service = Service()

print(inspect.isclass(Service))
print(inspect.isfunction(Service.run))
print(inspect.ismethod(service.run))
print(inspect.isroutine(service.run))

Access through the class returns the defined function, while access through an instance produces a bound method. This distinction matters in registries and dependency-injection frameworks.

Generators, coroutines, and async generators

The module distinguishes functions that create asynchronous objects from the created objects themselves.

import inspect

async def fetch():
    return 42

async def events():
    yield "start"

def numbers():
    yield 1

print(inspect.iscoroutinefunction(fetch))
print(inspect.isasyncgenfunction(events))
print(inspect.isgeneratorfunction(numbers))

After calling them, use iscoroutine(), isasyncgen(), isgenerator(), or isawaitable(). Await or close created objects in tests to avoid warnings.

List members with getmembers()

getmembers() returns sorted (name, value) pairs.

class Product:
    category = "general"

    def __init__(self, name):
        self.name = name

    def summary(self):
        return self.name

for name, value in inspect.getmembers(Product):
    if not name.startswith("__"):
        print(name, value)

An optional predicate filters the results:

methods = inspect.getmembers(Product, inspect.isfunction)
print([name for name, _ in methods])

This pattern is useful for discovering handlers, commands, and tests.

getmembers() can execute code

Normal attribute lookup triggers descriptors, properties, __getattr__(), and __getattribute__(). Inspecting an object may therefore execute logic.

class Example:
    @property
    def dangerous(self):
        print("property executed")
        return 10

inspect.getmembers(Example())

This can create side effects in documentation tools and when inspecting untrusted objects.

Passive inspection with getmembers_static()

Since Python 3.11, getmembers_static() avoids dynamic attribute resolution.

members = inspect.getmembers_static(Example())
for name, value in members:
    if name == "dangerous":
        print(value)  # property descriptor, getter not executed

The result may contain a descriptor rather than its value and may omit dynamically generated members. Choose according to whether you need runtime behavior or static structure.

getattr_static()

getattr_static() retrieves one attribute without invoking descriptors, __getattr__, or __getattribute__.

descriptor = inspect.getattr_static(Example, "dangerous")
print(type(descriptor))

It is appropriate for analyzers and debuggers. Resolving an arbitrary descriptor manually can still execute code.

Documentation with getdoc()

getdoc() cleans indentation and can inherit documentation from classes, methods, properties, and descriptors when a subclass does not override it.

print(inspect.getdoc(calculate_total))

cleandoc() can normalize a standalone documentation string.

Find files and modules

getfile(), getsourcefile(), and getmodule() support development tools.

print(inspect.getfile(calculate_total))
print(inspect.getsourcefile(calculate_total))
print(inspect.getmodule(calculate_total))

Built-ins and C extensions may not have Python source files. Catch TypeError and accept None.

Retrieve source code

getsource() returns source text for supported modules, classes, functions, methods, frames, tracebacks, and code objects.

try:
    source = inspect.getsource(calculate_total)
    print(source)
except (OSError, TypeError):
    print("Source is unavailable")

The official inspect documentation states that OSError is raised when source cannot be retrieved and TypeError for many built-ins. Interactive, dynamically executed, and packaged code may also lack accessible source.

getsourcelines() and comments

getsourcelines() returns source lines plus the starting line number.

lines, start = inspect.getsourcelines(calculate_total)
print(start)
print("".join(lines))

getcomments() looks for comments immediately preceding a definition. Prefer docstrings and explicit metadata for stable contracts.

Function signatures

signature() is the recommended API for callable parameters.

from inspect import signature

sig = signature(calculate_total)
print(sig)
print(sig.return_annotation)

for name, parameter in sig.parameters.items():
    print(name, parameter.kind, parameter.default, parameter.annotation)

It understands functions, classes, methods, functools.partial, and many callable objects.

Parameter kinds

Each parameter has a kind:

  • POSITIONAL_ONLY before /;
  • POSITIONAL_OR_KEYWORD for regular parameters;
  • VAR_POSITIONAL for *args;
  • KEYWORD_ONLY after *;
  • VAR_KEYWORD for **kwargs.
def example(a, /, b=2, *args, c, **kwargs):
    pass

for param in inspect.signature(example).parameters.values():
    print(param.name, param.kind.description)

Frameworks can use this information to generate forms, validators, and adapted calls.

Missing value versus None

Parameter.empty means no default or annotation. It differs from an explicit default of None.

param = inspect.signature(calculate_total).parameters["rate"]
if param.default is inspect.Parameter.empty:
    print("Required")

Compare the marker by identity.

Validate calls with Signature.bind()

bind() maps arguments to parameters as a real call would and raises TypeError for an invalid combination.

sig = inspect.signature(calculate_total)

try:
    bound = sig.bind(100, rate=0.2)
    print(bound.arguments)
except TypeError as error:
    print("Invalid arguments:", error)

This lets plugin and route systems validate a call before execution.

bind_partial() and defaults

bind_partial() allows required arguments to be omitted, mirroring functools.partial().

partial_args = sig.bind_partial(rate=0.15)
print(partial_args.arguments)

BoundArguments.apply_defaults() fills declared defaults, an empty tuple for *args, and an empty dictionary for **kwargs.

bound = sig.bind(100)
bound.apply_defaults()
print(bound.arguments)

Call through BoundArguments

The args and kwargs properties support execution after validation or conversion.

bound = sig.bind("100", rate="0.2")
bound.arguments["value"] = float(bound.arguments["value"])
bound.arguments["rate"] = float(bound.arguments["rate"])
result = calculate_total(*bound.args, **bound.kwargs)

Do not automatically use annotations as converters without a policy because annotations can be arbitrary objects.

Annotations and evaluation risk

signature() can return annotations as strings or resolve them. The eval_str and annotation-format options control this behavior. Evaluating annotation strings can execute arbitrary code.

For untrusted content, keep eval_str=False and prefer a string representation. Python 3.14 adds annotation_format integration with annotationlib.Format.

Decorators and __wrapped__

Decorators can hide the original name, docs, and signature. functools.wraps() copies metadata and creates __wrapped__.

from functools import wraps


def log_call(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        print("Calling", func.__name__)
        return func(*args, **kwargs)
    return wrapper

@log_call
def add(a: int, b: int = 0) -> int:
    return a + b

print(inspect.signature(add))

The official functools documentation explains how wraps preserves metadata and original-function access.

unwrap()

inspect.unwrap() follows the __wrapped__ chain.

original = inspect.unwrap(add)
print(original.__name__)

It detects cycles and raises ValueError. A stop callback can halt at a selected wrapper.

follow_wrapped

signature() follows wrappers by default. Set follow_wrapped=False to inspect the wrapper itself.

print(inspect.signature(add, follow_wrapped=True))
print(inspect.signature(add, follow_wrapped=False))

This helps debug decorators that add behavior or parameters.

Modify Signature and Parameter objects

These objects are immutable. Use replace() or copy.replace().

sig = inspect.signature(add)
new_sig = sig.replace(return_annotation="number")
print(new_sig)

Changing displayed metadata does not change callable behavior. Framework contracts and implementations must stay synchronized.

Inheritance and method resolution

getmro() returns the method-resolution order.

class A: pass
class B(A): pass
class C(A): pass
class D(B, C): pass

print(inspect.getmro(D))

This explains which implementation multiple inheritance selects. getclasstree() organizes classes into a hierarchy.

Closures and external variables

getclosurevars() reports nonlocal, global, built-in, and unresolved names.

factor = 10

def create():
    extra = 2
    def calculate(value):
        return value * factor + extra
    return calculate

print(inspect.getclosurevars(create()))

This aids debugging but exposes live references. Do not log secrets.

Generator and coroutine state

getgeneratorstate() reports created, running, suspended, or closed states.

def counter():
    yield 1
    yield 2

gen = counter()
print(inspect.getgeneratorstate(gen))
next(gen)
print(inspect.getgeneratorstate(gen))

Equivalent functions exist for coroutines and asynchronous generators. They are useful in scheduler tests and diagnostics.

Live locals

getgeneratorlocals(), getcoroutinelocals(), and getasyncgenlocals() retrieve local state while an associated frame exists.

These features depend on interpreter details and may return empty mappings elsewhere. Do not build business rules around them.

Frames and the execution stack

currentframe(), stack(), trace(), getouterframes(), and getinnerframes() support debuggers and error reports.

frame = inspect.currentframe()
try:
    if frame is not None:
        info = inspect.getframeinfo(frame)
        print(info.filename, info.lineno, info.function)
finally:
    del frame

currentframe() may return None on implementations without Python frame support.

Frames can retain memory

Frames reference local variables and outer frames. Keeping one can create cycles and extend the lifetime of a large object graph.

Delete frame references in finally. If a frame must be retained temporarily, call frame.clear() after use. This matters in long-running servers.

Introspection is not automatically a public API

Finding an attribute does not make it stable or supported. Underscore names, code-object fields, and frame internals can be implementation details.

Plugin systems should define explicit interfaces and versions. Use introspection for adaptation and diagnostics rather than replacing specifications.

Cross-implementation compatibility

Some CPython built-ins lack complete signature metadata. Descriptors and code objects can vary between interpreters.

Catch TypeError, ValueError, and OSError, provide fallbacks, and test every supported interpreter.

Example: handler registry

def register_handler(func):
    if not inspect.isfunction(func) and not inspect.ismethod(func):
        raise TypeError("Handler must be a function or method")

    sig = inspect.signature(func)
    parameters = list(sig.parameters.values())
    if not parameters:
        raise TypeError("Handler must receive an event")

    return {
        "callable": func,
        "signature": sig,
        "documentation": inspect.getdoc(func) or "",
    }

Before execution, call bind() with the available event and dependencies to surface configuration errors early.

Common mistakes

  • Calling getmembers() on untrusted objects without considering properties.
  • Assuming every callable has source and signature metadata.
  • Evaluating string annotations without a security review.
  • Writing decorators without functools.wraps().
  • Confusing a class function with a bound method.
  • Retaining frames and creating reference cycles.
  • Depending on CPython-only details.
  • Using introspection instead of a plugin contract.

Best practices

  • Use is* predicates to express intent.
  • Prefer signature() over legacy argument APIs.
  • Use getmembers_static() for passive inspection.
  • Treat missing source and signatures as normal cases.
  • Preserve __wrapped__ with wraps().
  • Avoid evaluating untrusted annotations.
  • Release frame references in finally.
  • Test all supported Python implementations.

Conclusion

The Python inspect module examines live objects, classes, functions, signatures, source code, closures, generators, and interpreter stacks. It enables adaptive documentation tools, registries, debuggers, and validators.

Introspection requires care because attribute access can execute descriptors, annotations may involve evaluation, and frames can retain large object graphs. By choosing static APIs when appropriate, tolerating unavailable metadata, preserving wrappers, and clearing frames, you can use Python’s dynamic capabilities without turning diagnostics into side effects or memory leaks.

Share:

Facebook
WhatsApp
Twitter
LinkedIn

Article content

    Related articles

    Memory module representing weak references and caches in Python
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python weakref: Weak References

    Learn Python weakref to build weak references, automatic caches, observer registries, and finalizers without retaining objects in memory.

    Ler mais

    Tempo de leitura: 8 minutos
    28/07/2026
    ZIP archive icon for a Python zipfile article
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python zipfile: Safe ZIP Archives

    Learn how to create, read, validate, and extract ZIP archives with Python zipfile in a predictable and secure workflow.

    Ler mais

    Tempo de leitura: 4 minutos
    27/07/2026
    Software dependency graph and Python task workflow
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python graphlib: Topological Sorting

    Learn Python graphlib to order dependencies, detect cycles, and coordinate independent tasks safely in parallel.

    Ler mais

    Tempo de leitura: 5 minutos
    27/07/2026
    Python code for safe context in asynchronous applications
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python contextvars for Safe Context

    Learn how Python contextvars isolates request data across asyncio tasks, logs, threads, and tests without unsafe global variables.

    Ler mais

    Tempo de leitura: 7 minutos
    26/07/2026
    Python code using cached_property to store expensive calculations
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python cached_property: Cache Object Values

    Learn Python cached_property to store expensive calculations, invalidate values, and prevent stale object caches.

    Ler mais

    Tempo de leitura: 6 minutos
    25/07/2026
    Python code with type-based function dispatch
    Advanced Python
    Foto de perfil de Leandro Hirt da Academify

    Python singledispatch: Type-Based Functions

    Learn Python singledispatch to build type-based functions, reduce isinstance chains, and organize extensible polymorphism with clear examples.

    Ler mais

    Tempo de leitura: 6 minutos
    25/07/2026