Skip to content

Inspect Module for Introspection#

The inspect module is Python's standard-library toolkit for examining live objects: functions, classes, coroutines, frames, source code, and call signatures. It powers debuggers, documentation generators, test frameworks, and dependency-injection libraries.

How to use this page

Use after Classes and OOP Basics and Dunder or Magic Methods. Pairs with Metaclasses (class construction) and Callable Objects (what inspect recognizes as callable).

At a glance
Track Python Advanced → Meta Programming
Sections 11 major topics
Outline Use the right-hand TOC to jump

Topics: What introspection means in Python · inspect.signature — the workhorse (Python 3.3+) · inspect.getmembers — bulk attribute scan · Classes and the MRO · Source code and stack frames · Coroutines and async inspection · inspect.getclosurevars — closure analysis · inspect.classify_class_attrs · … (+3 more)

  1. What introspection means in Python
  2. inspect.signature — the workhorse (Python 3.3+)
  3. inspect.getmembers — bulk attribute scan
  4. Classes and the MRO
  5. Source code and stack frames
  6. Coroutines and async inspection
  7. inspect.getclosurevars — closure analysis
  8. inspect.classify_class_attrs
  9. inspect.unwrap — following decorator chains
  10. Safe boundaries for introspection
  11. Practical recipe: CLI from function signature

What introspection means in Python#

Introspection is reading structure and metadata from objects at runtime without executing their business logic.

import inspect

def add(a: int, b: int) -> int:
    """Return the sum."""
    return a + b

inspect.isfunction(add)       # True
inspect.isbuiltin(print)      # True
inspect.ismethod(add)         # False — not bound yet
Question inspect tool
Is it callable? inspect.isroutine, callable
What are the parameters? inspect.signature
Where is it defined? inspect.getfile, getmodule
What is the source? inspect.getsource
Is it a coroutine? inspect.iscoroutinefunction

inspect.signature — the workhorse (Python 3.3+)#

import inspect
from typing import Optional

def connect(host: str, port: int = 5432, *, ssl: bool = True) -> None:
    ...

sig = inspect.signature(connect)
for name, param in sig.parameters.items():
    print(name, param.kind, param.default, param.annotation)

Output (conceptually):

Name Kind Default Annotation
host POSITIONAL_OR_KEYWORD empty str
port POSITIONAL_OR_KEYWORD 5432 int
ssl KEYWORD_ONLY True empty

Parameter.kind values#

Kind Meaning
POSITIONAL_ONLY Before / (Python 3.8+)
POSITIONAL_OR_KEYWORD Normal positional or keyword
KEYWORD_ONLY After * or *args
VAR_POSITIONAL *args
VAR_KEYWORD **kwargs
def demo(a, /, b, *, c, **kwargs): ...

[p.kind for p in inspect.signature(demo).parameters.values()]

Framework pattern — auto-wiring

Web frameworks and CLI libraries bind route paths or flags to function parameters by reading inspect.signature — no duplicate configuration.

def bind_call(func, **kwargs):
    sig = inspect.signature(func)
    bound = sig.bind_partial(**kwargs)
    bound.apply_defaults()
    return func(*bound.args, **bound.kwargs)

inspect.getmembers — bulk attribute scan#

import inspect

class Service:
    VERSION = "1.0"

    def run(self) -> None:
        pass

    @staticmethod
    def helper() -> int:
        return 42

# predicate filters what you get back
methods = inspect.getmembers(Service, predicate=inspect.isfunction)
# [('run', <function ...>), ('helper', <function ...>)]

all_members = inspect.getmembers(Service)
Function Use
getmembers(obj) All attributes (unless obj.__dir__ customized)
getmembers(obj, predicate) Filtered list of (name, value) pairs
getmembers_static(obj) Python 3.11+ — avoids descriptor execution

Descriptors can run code

getmembers may trigger @property getters and other descriptors. Use getmembers_static (3.11+) when auditing untrusted objects.


Classes and the MRO#

class A:
    def f(self): pass

class B(A):
    pass

inspect.getmro(B)
# (<class 'B'>, <class 'A'>, <class 'object'>)

inspect.isclass(B)          # True
inspect.ismethod(B.f)       # False — unbound function on class
inspect.ismethod(B().f)     # True — bound method

Compare with Method Resolution Order for C3 linearization theory.


Source code and stack frames#

import inspect

def outer():
    def inner():
        return inspect.currentframe()
    return inner()

frame = outer()
inspect.getframeinfo(frame)
# FrameInfo(filename='...', lineno=..., function='inner', ...)

inspect.stack()   # list of FrameInfo from current frame up
inspect.trace()   # stack of current exception (if any)
API Returns
getsource(func) Source text as string
getsourcelines(func) (lines, start_lineno)
getfile(obj) Path to defining file
getmodule(obj) Module object or None
getdoc(obj) Cleaned docstring

Fails for builtins and dynamically compiled code

getsource raises OSError for C builtins, REPL one-liners, and exec()-generated functions.


Coroutines and async inspection#

import asyncio
import inspect

async def fetch():
    await asyncio.sleep(0)

inspect.iscoroutinefunction(fetch)  # True
inspect.iscoroutine(fetch())        # True — the object returned by calling fetch

def sync(): pass
inspect.iscoroutinefunction(sync)   # False
Function Detects
iscoroutinefunction async def functions
iscoroutine Coroutine objects
isawaitable Coroutines + objects with __await__
isasyncgenfunction async def with yield

inspect.getclosurevars — closure analysis#

def make_multiplier(factor: int):
    def multiply(x: int) -> int:
        return x * factor
    return multiply

times3 = make_multiplier(3)
inspect.getclosurevars(times3)
# ClosureVars(nonlocals={'factor': 3}, globals={...}, builtins={...}, unbound=())

Pairs with Closures and the late-binding loop trap documented there.


inspect.classify_class_attrs#

Groups class attributes by how they are defined:

import inspect

class Base:
    class_attr = 1

    @property
    def name(self) -> str:
        return "base"

class Child(Base):
    def method(self) -> None:
        pass

for attr in inspect.classify_class_attrs(Child):
    print(attr.name, attr.kind)
# class_attr class variable
# name      property
# method    method
kind Meaning
method Function in class, unbound
class method @classmethod
static method @staticmethod
property @property
data / data descriptor Plain class attribute or descriptor

inspect.unwrap — following decorator chains#

from functools import wraps

def logged(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        return func(*args, **kwargs)
    return wrapper

@logged
def compute(x: int) -> int:
    return x * 2

inspect.unwrap(compute) is compute          # False — wrapper in between
inspect.unwrap(compute).__name__            # 'compute' after peeling wrappers

Essential when decorators stack and you need the original function's signature.


Safe boundaries for introspection#

Scenario Guidance
Logging / debugging getsource, signature — generally safe
Plugin discovery Whitelist modules; don't import arbitrary paths
Auditing user code Avoid getmembers without getmembers_static
Production hot paths Cache signature results — reflection has cost

Import side effects

inspect.getmodule may import packages to resolve dotted names. Never introspect untrusted module paths in security-sensitive contexts.


Practical recipe: CLI from function signature#

import inspect
import sys

def greet(name: str, loud: bool = False) -> str:
    msg = f"Hello, {name}!"
    return msg.upper() if loud else msg

def main(argv: list[str]) -> None:
    sig = inspect.signature(greet)
    kwargs = {}
    for arg, param in zip(argv, sig.parameters.values()):
        if param.annotation is bool:
            kwargs[param.name] = arg.lower() in ("true", "1", "yes")
        else:
            kwargs[param.name] = arg
    print(greet(**kwargs))

# python script.py Ada true  →  HELLO, ADA!

Production CLIs use argparse, click, or typer — but all rely on the same introspection primitives under the hood.


Interview traps (quick reference)#

Trap What goes wrong Safe approach
inspect.ismethod on class attribute False for function on class Call on instance.method or check inspect.isfunction
getsource on lambda OSError Use inspect.getsource only on named defs
Assuming defaults covers all Keyword-only defaults live on Parameter.default Iterate signature().parameters
getmembers triggers properties Side effects / exceptions getmembers_static (3.11+)
unwrap without follow_wrapped=False Stops at first __wrapped__ Know when to stop unwrapping

Mental model checklist#

  1. What does inspect.signature return and how do you read parameter kinds?
  2. When is inspect.ismethod true vs false?
  3. What is the difference between getmembers and getmembers_static?
  4. How does inspect.unwrap help with decorated functions?
  5. Why might getsource fail on a valid Python function?

What's next#

Topic Page
Metaclasses Metaclasses
Callable objects Callable Objects
Type hints Function Annotations and Type Hints
Monkey patching Monkey Patching
Unit testing (mock introspection) Unit Testing