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 executedThe 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_ONLYbefore/;POSITIONAL_OR_KEYWORDfor regular parameters;VAR_POSITIONALfor*args;KEYWORD_ONLYafter*;VAR_KEYWORDfor**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 framecurrentframe() 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__withwraps(). - 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.







