Skip to content

53. `inspect`

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

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:

QuestionExample 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:

FunctionMeaning
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:

AttributeMeaning
__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 fieldMeaning
co_argcountPositional argument count
co_posonlyargcountPositional-only argument count
co_kwonlyargcountKeyword-only argument count
co_varnamesLocal variable names
co_constsConstants
co_namesReferenced global and attribute names
co_freevarsFree variables
co_cellvarsCell variables
co_filenameSource filename
co_firstlinenoFirst source line
co_flagsFunction flags
co_codeRaw 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:

KindPython syntax
POSITIONAL_ONLYdef f(x, /)
POSITIONAL_OR_KEYWORDdef f(x)
VAR_POSITIONALdef f(*args)
KEYWORD_ONLYdef f(*, x)
VAR_KEYWORDdef 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:

CallableSource of signature
Python function__code__, defaults, annotations
Bound methodFunction signature minus bound self
Class__call__, __new__, or __init__
Built-in function__text_signature__ when available
Callable objecttype(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:

AttributeMeaning
f_codeExecuting code object
f_localsLocal namespace view
f_globalsGlobal namespace
f_builtinsBuiltins namespace
f_backCaller frame
f_linenoCurrent line number
f_traceTrace 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:

FieldMeaning
frameFrame object
filenameSource filename
linenoLine number
functionFunction name
code_contextNearby source text
indexPosition 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:

AttributeMeaning
tb_frameFrame at this stack level
tb_linenoLine number
tb_lastiLast bytecode instruction offset
tb_nextNext 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:

StateMeaning
GEN_CREATEDCreated, not yet started
GEN_RUNNINGCurrently executing
GEN_SUSPENDEDPaused at yield
GEN_CLOSEDFinished or closed

A generator object stores:

AttributeMeaning
gi_codeCode object
gi_frameSuspended frame
gi_runningRunning flag
gi_yieldfromDelegated 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:

StateMeaning
CORO_CREATEDCreated, not yet awaited
CORO_RUNNINGCurrently executing
CORO_SUSPENDEDAwaiting something
CORO_CLOSEDFinished 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 featureUsed for
Code objectsSource location and function metadata
FramesStack inspection
TracebacksException inspection
Function attributesSignatures and closures
Descriptor protocolMember lookup
Import systemModule and source resolution
linecacheSource retrieval
typesRuntime 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.

CaseBehavior
Built-in function lacks signature metadataValueError
Source file unavailableOSError
Decorator hides original functionWrong signature
Dynamic __getattr__ has side effectsInspection may execute code
Frame retained too longMemory retention
Generated code uses fake filenamesSource 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.