53. `inspect`

inspect.getmembers, signature objects, frame introspection, and source retrieval via linecache.

53. inspect

The inspect module exposes structured runtime introspection. It lets Python code examine objects, functions, classes, methods, frames, tracebacks, source files, signatures, annotations, generators, coroutines, and descriptors.

Where sys exposes low-level interpreter state, inspect builds higher-level views on top of that state.

53.1 The Role of inspect

inspect answers questions about live Python objects:

Question Example API
What kind of object is this? inspect.isfunction()
What parameters does this callable accept? inspect.signature()
Where was this object defined? inspect.getsourcefile()
What source code created this object? inspect.getsource()
What frame is currently executing? inspect.currentframe()
What local variables exist in a frame? inspect.getargvalues()
What is the state of this generator? inspect.getgeneratorstate()

The module is heavily used by debuggers, test frameworks, documentation generators, dependency injection frameworks, RPC systems, decorators, serializers, CLI tools, and metaprogramming libraries.

Conceptually:

runtime object
    ↓
inspect
    ↓
metadata, source, signature, frame, or state

53.2 Introspection vs Reflection

Introspection means examining program structure at runtime.

Reflection means modifying or invoking program structure dynamically.

inspect is mostly introspective. It reads object metadata, but usually does not mutate interpreter structures.

Example:

import inspect

def add(a, b):
    return a + b

print(inspect.isfunction(add))
print(inspect.signature(add))
print(inspect.getsource(add))

Output shape:

True
(a, b)
def add(a, b):
    return a + b

The source result depends on the function being defined in a real source file.

53.3 Object Classification

The simplest part of inspect classifies objects.

Examples:

import inspect

class User:
    def save(self):
        pass

def build():
    pass

print(inspect.isclass(User))
print(inspect.isfunction(build))
print(inspect.ismethod(User().save))

Important classification helpers include:

Function Meaning
isclass() Object is a class
isfunction() Object is a Python function
isbuiltin() Object is a built-in function or method
ismethod() Object is a bound method
ismodule() Object is a module
isgenerator() Object is a generator object
iscoroutine() Object is a coroutine object
isawaitable() Object can be awaited
isframe() Object is a frame
istraceback() Object is a traceback

These functions usually check type relationships and internal attributes. They are thin classification layers over the object model.

53.4 Functions and Code Objects

A Python function stores a code object in __code__.

Example:

def f(x, y=10):
    z = x + y
    return z

print(f.__code__)
print(f.__defaults__)
print(f.__globals__)

inspect uses this structure to recover information about the function.

A function object contains:

Attribute Meaning
__code__ Compiled code object
__defaults__ Positional default values
__kwdefaults__ Keyword-only defaults
__globals__ Global namespace
__closure__ Closure cells
__annotations__ Type annotations
__dict__ Custom function attributes
__module__ Defining module
__qualname__ Qualified name

The code object contains lower-level compiler output:

Code object field Meaning
co_argcount Positional argument count
co_posonlyargcount Positional-only argument count
co_kwonlyargcount Keyword-only argument count
co_varnames Local variable names
co_consts Constants
co_names Referenced global and attribute names
co_freevars Free variables
co_cellvars Cell variables
co_filename Source filename
co_firstlineno First source line
co_flags Function flags
co_code Raw bytecode bytes

inspect combines function-level and code-object-level metadata into a user-facing view.

53.5 Signatures

inspect.signature() is one of the most important APIs in the module.

Example:

import inspect

def connect(host, port=5432, *, timeout=30, ssl=False):
    pass

sig = inspect.signature(connect)
print(sig)

Output:

(host, port=5432, *, timeout=30, ssl=False)

The result is a Signature object.

A Signature contains ordered Parameter objects.

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

Parameter kinds include:

Kind Python syntax
POSITIONAL_ONLY def f(x, /)
POSITIONAL_OR_KEYWORD def f(x)
VAR_POSITIONAL def f(*args)
KEYWORD_ONLY def f(*, x)
VAR_KEYWORD def f(**kwargs)

This mirrors CPython’s function calling model.

53.6 Binding Arguments

A signature can bind call arguments without actually calling the function.

Example:

import inspect

def f(a, b=2, *, c=3):
    pass

sig = inspect.signature(f)

bound = sig.bind(1, c=10)
print(bound.arguments)

Output:

{'a': 1, 'c': 10}

Defaults are not inserted automatically unless requested:

bound.apply_defaults()
print(bound.arguments)

Output:

{'a': 1, 'b': 2, 'c': 10}

This is useful for frameworks that need to validate or transform calls:

CLI command dispatch
HTTP endpoint binding
RPC argument validation
dependency injection
decorator wrappers
test fixture injection

53.7 Callable Introspection

Not every callable is a plain Python function.

Callable forms include:

def f():
    pass

class C:
    def __call__(self):
        pass

len
C()

inspect.signature() handles many callable types:

Callable Source of signature
Python function __code__, defaults, annotations
Bound method Function signature minus bound self
Class __call__, __new__, or __init__
Built-in function __text_signature__ when available
Callable object type(obj).__call__
Decorated function __wrapped__ chain

For built-ins and extension functions, there may be no Python code object. CPython may provide a text signature string instead.

Example:

import inspect

print(inspect.signature(len))

Output shape:

(obj, /)

The slash means the parameter is positional-only.

53.8 Decorators and __wrapped__

Decorators often replace one function with another.

Example:

def log(fn):
    def wrapper(*args, **kwargs):
        print("calling")
        return fn(*args, **kwargs)
    return wrapper

Without care, introspection sees wrapper, not the original function.

The standard solution is functools.wraps():

import functools
import inspect

def log(fn):
    @functools.wraps(fn)
    def wrapper(*args, **kwargs):
        print("calling")
        return fn(*args, **kwargs)
    return wrapper

@log
def add(a, b):
    return a + b

print(add.__wrapped__)
print(inspect.signature(add))

functools.wraps() sets __wrapped__.

inspect.signature() follows the __wrapped__ chain by default.

This is why well-written decorators preserve signatures, documentation tools work better, and frameworks can still infer parameters.

53.9 Members

inspect.getmembers() returns named attributes of an object.

Example:

import inspect

class User:
    kind = "user"

    def save(self):
        pass

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

getmembers() roughly performs:

walk object attributes
resolve descriptors
sort by name
return list of pairs

It may invoke descriptors. That means it can execute user code indirectly.

Safer alternative:

inspect.getmembers_static(obj)

getmembers_static() avoids dynamic descriptor lookup where possible. This matters for tools that inspect hostile or side-effect-heavy objects.

53.10 Descriptors and Static Lookup

Normal attribute access in Python may call descriptor methods:

__get__
__set__
__delete__

Example:

class C:
    @property
    def value(self):
        print("property executed")
        return 42

obj = C()
print(obj.value)

Inspecting obj.value normally executes the property.

inspect.getattr_static() avoids normal dynamic lookup:

import inspect

raw = inspect.getattr_static(obj, "value")
print(raw)

This returns the property object rather than calling it.

This feature is important for:

documentation tools
debuggers
security-sensitive inspection
object browsers
framework metadata collection

53.11 Source Code Retrieval

inspect.getsource() tries to retrieve source text for an object.

Example:

import inspect

def f(x):
    return x + 1

print(inspect.getsource(f))

This depends on several pieces of metadata:

object
    ↓
code object
    ↓
co_filename
    ↓
linecache
    ↓
source lines

For a function, CPython stores the filename and starting line number in the code object:

print(f.__code__.co_filename)
print(f.__code__.co_firstlineno)

inspect uses this information to find and slice the original source file.

Source retrieval can fail when code comes from:

interactive shell
eval()
exec()
generated code
deleted files
zip imports without source access
compiled-only modules
native extension modules

In those cases, inspect.getsource() raises an error.

53.12 Linecache

The linecache module supports source retrieval.

inspect uses it to read source lines by filename.

Conceptually:

filename
    ↓
linecache
    ↓
source lines
    ↓
inspect block extraction

linecache caches file contents and also supports synthetic sources registered by interactive environments.

This is why notebooks, REPLs, and debuggers often integrate with linecache to make inspection work for generated cells.

53.13 Frames

inspect.currentframe() returns the current frame.

Example:

import inspect

frame = inspect.currentframe()
print(frame.f_code.co_name)
print(frame.f_locals)

A frame exposes active execution state:

Attribute Meaning
f_code Executing code object
f_locals Local namespace view
f_globals Global namespace
f_builtins Builtins namespace
f_back Caller frame
f_lineno Current line number
f_trace Trace function

Frames are powerful and dangerous. Holding references to frames can keep entire call stacks alive.

Example risk:

import inspect

saved = inspect.currentframe()

The frame refers to locals. Locals may refer to large objects. The frame also points to the previous frame through f_back.

This can create memory retention bugs.

A safer pattern:

frame = inspect.currentframe()
try:
    print(frame.f_code.co_name)
finally:
    del frame

53.14 Stack Inspection

inspect.stack() returns records for the current call stack.

Example:

import inspect

def a():
    b()

def b():
    for frameinfo in inspect.stack():
        print(frameinfo.function, frameinfo.lineno)

a()

Each frame record contains:

Field Meaning
frame Frame object
filename Source filename
lineno Line number
function Function name
code_context Nearby source text
index Position in context

Stack inspection is expensive. It walks frames and often reads source lines.

It should be avoided in hot paths unless absolutely necessary.

53.15 Tracebacks

A traceback is a linked list of execution frames captured during an exception.

Example:

import inspect

try:
    1 / 0
except Exception as exc:
    tb = exc.__traceback__
    print(inspect.istraceback(tb))

Traceback nodes contain:

Attribute Meaning
tb_frame Frame at this stack level
tb_lineno Line number
tb_lasti Last bytecode instruction offset
tb_next Next traceback node

inspect can classify and traverse traceback objects, but formatting is usually handled by the traceback module.

53.16 Generators

Generators expose execution state.

Example:

import inspect

def gen():
    yield 1
    yield 2

g = gen()

print(inspect.getgeneratorstate(g))
next(g)
print(inspect.getgeneratorstate(g))

Possible states:

State Meaning
GEN_CREATED Created, not yet started
GEN_RUNNING Currently executing
GEN_SUSPENDED Paused at yield
GEN_CLOSED Finished or closed

A generator object stores:

Attribute Meaning
gi_code Code object
gi_frame Suspended frame
gi_running Running flag
gi_yieldfrom Delegated iterator

Generators are resumable frames packaged as objects.

53.17 Coroutines

Native coroutines are also inspectable.

Example:

import inspect

async def fetch():
    return 1

coro = fetch()

print(inspect.iscoroutine(coro))
print(inspect.getcoroutinestate(coro))

coro.close()

Coroutine states include:

State Meaning
CORO_CREATED Created, not yet awaited
CORO_RUNNING Currently executing
CORO_SUSPENDED Awaiting something
CORO_CLOSED Finished or closed

Coroutine objects are similar to generators but participate in the await protocol.

53.18 Async Generators

Async generators combine async execution with yielded values.

Example:

import inspect

async def agen():
    yield 1

obj = agen()

print(inspect.isasyncgen(obj))

They have their own object type and execution state. inspect provides helpers to classify them and understand async code structure.

53.19 Classes and MRO Inspection

inspect can examine classes.

Example:

import inspect

class A:
    pass

class B(A):
    pass

print(inspect.getmro(B))

Output:

(<class '__main__.B'>, <class '__main__.A'>, <class 'object'>)

The method resolution order is stored by the type object and controls attribute lookup.

inspect exposes this without requiring direct access to CPython’s PyTypeObject.

53.20 Closure Inspection

Closures store references to variables from outer scopes.

Example:

def outer(x):
    def inner():
        return x
    return inner

fn = outer(10)

print(fn.__closure__)

Each closure cell contains a referenced object.

inspect.getclosurevars() gives a structured view:

import inspect

vars = inspect.getclosurevars(fn)
print(vars.nonlocals)
print(vars.globals)
print(vars.builtins)
print(vars.unbound)

This is useful for understanding how nested functions capture state.

53.21 Annotations

inspect works with function annotations and signatures.

Example:

import inspect

def f(x: int, y: str = "a") -> bool:
    return True

sig = inspect.signature(f)
print(sig.return_annotation)

for param in sig.parameters.values():
    print(param.name, param.annotation)

Annotations may be actual objects or strings, depending on how the module was compiled and which future imports are active.

Modern annotation handling often involves annotationlib or typing utilities, but inspect remains central for callable metadata.

53.22 Internal Dependencies

inspect depends on several lower-level runtime features:

Runtime feature Used for
Code objects Source location and function metadata
Frames Stack inspection
Tracebacks Exception inspection
Function attributes Signatures and closures
Descriptor protocol Member lookup
Import system Module and source resolution
linecache Source retrieval
types Runtime type classification

So inspect is less a primitive than a composition layer. It turns raw CPython metadata into stable Python APIs.

53.23 Cost Model

inspect can be expensive.

Cheap operations:

isfunction()
isclass()
ismodule()
signature() on cached/simple functions

Potentially expensive operations:

stack()
getsource()
getmembers()
getmembers_static()
signature() on complex decorated callables

Expensive reasons include:

walking frames
reading files
resolving descriptors
following wrapper chains
parsing text signatures
allocating metadata objects

Avoid heavy inspection inside tight loops.

53.24 Common Failure Modes

Inspection can fail or mislead.

Case Behavior
Built-in function lacks signature metadata ValueError
Source file unavailable OSError
Decorator hides original function Wrong signature
Dynamic __getattr__ has side effects Inspection may execute code
Frame retained too long Memory retention
Generated code uses fake filenames Source lookup may fail

Robust tools must handle these failures.

Example:

import inspect

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

53.25 Why inspect Matters for CPython Internals

inspect matters because it exposes CPython’s execution structures in a controlled Python-level form.

It gives access to:

code objects
function metadata
frames
tracebacks
signatures
closures
generator state
coroutine state
class hierarchy
source mapping

These are the same concepts used internally by the compiler, interpreter, debugger hooks, import system, exception machinery, and object model.

Understanding inspect makes later internals easier because it connects visible Python objects to the hidden runtime structures beneath them.

53.26 Chapter Summary

The inspect module is CPython’s high-level introspection layer. It builds on objects, functions, code objects, frames, tracebacks, descriptors, closures, source files, and signatures.

Unlike sys, which exposes raw runtime state, inspect organizes that state into reusable tools. It is the module that turns CPython’s internal execution model into something Python programs can query directly.