# 53. `inspect`

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

```text
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:

```python
import inspect

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

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

Output shape:

```text
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:

```python
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:

```python
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:

```python
import inspect

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

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

Output:

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

The result is a `Signature` object.

A `Signature` contains ordered `Parameter` objects.

```python
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:

```python
import inspect

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

sig = inspect.signature(f)

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

Output:

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

Defaults are not inserted automatically unless requested:

```python
bound.apply_defaults()
print(bound.arguments)
```

Output:

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

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

```text
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:

```python
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:

```python
import inspect

print(inspect.signature(len))
```

Output shape:

```text
(obj, /)
```

The slash means the parameter is positional-only.

## 53.8 Decorators and `__wrapped__`

Decorators often replace one function with another.

Example:

```python
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()`:

```python
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:

```python
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:

```text
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:

```python
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:

```text
__get__
__set__
__delete__
```

Example:

```python
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:

```python
import inspect

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

This returns the property object rather than calling it.

This feature is important for:

```text
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:

```python
import inspect

def f(x):
    return x + 1

print(inspect.getsource(f))
```

This depends on several pieces of metadata:

```text
object
    ↓
code object
    ↓
co_filename
    ↓
linecache
    ↓
source lines
```

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

```python
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:

```text
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:

```text
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
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:

```python
import inspect

class A:
    pass

class B(A):
    pass

print(inspect.getmro(B))
```

Output:

```text
(<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:

```python
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:

```python
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:

```python
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:

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

Potentially expensive operations:

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

Expensive reasons include:

```text
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:

```python
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:

```text
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.
