Last Updated: June 01, 2026

Intermediate

You’re debugging a 3 AM production issue, staring at a stack trace from a function whose source file is unfamiliar. You wonder: what arguments does it actually accept? Which module did Python import it from? What does its docstring say? You could grep through the repo or open the file in your editor — or you could just ask Python directly with the inspect module.

inspect is Python’s reflection toolkit: it answers questions about live objects without leaving the interpreter. This guide covers the practical bits — getting function signatures, source code, callers, type hints, and class hierarchies — plus how those features power test frameworks, ORMs, and CLI argument parsers under the hood.

Pubs - Python How To Program
Written by Pubs

Python developer and educator with 15+ years building production systems across data engineering, web APIs, and AI tooling. Founder of Python How To Program — 270+ in-depth tutorials covering the modern Python stack.

View all tutorials by Pubs →

inspect Quick Example

Five lines that show why inspect is useful:

# File: quick.py
import inspect
import requests

print(inspect.signature(requests.get))
print(inspect.getsource(requests.get)[:200])
print(inspect.getfile(requests.get))
print(inspect.getdoc(requests.get).split("\n")[0])

Output:

(url, params=None, **kwargs)
def get(url, params=None, **kwargs):
    r"""Sends a GET request.

    :param url: URL for the new :class:`Request` object.
/Users/you/.venv/lib/python3.12/site-packages/requests/api.py
Sends a GET request.

You just asked Python: what’s the signature of requests.get? Show me its source. Tell me where it lives. Read its docstring. No browsing through GitHub or installing source maps — Python answers from its own runtime.

What Is the inspect Module?

Every Python object carries metadata at runtime — its type, its attributes, its source location, the values of its closure variables, the signature of its methods. inspect is the official way to read that metadata without touching the private double-underscore attributes directly.

It’s the foundation of dozens of frameworks you already use:

  • FastAPI inspects route handler signatures to build OpenAPI docs and validate input.
  • pytest inspects fixture signatures to figure out what each test needs.
  • argparse alternatives like Click and Typer inspect your CLI function signatures to generate help text.
  • SQLAlchemy inspects class attributes to map them to database columns.
  • Debuggers and IDEs use inspect to render local variables and frames during stepping.

You’ll reach for it directly when writing tools that work with arbitrary user code: decorators that need to know what they’re wrapping, test utilities that need argument names, or debugging helpers that need a clean view of the current frame.

Reflection: ask Python about itself.
Reflection: ask Python about itself.

Getting Function Signatures with signature()

inspect.signature() returns a Signature object describing a function’s parameters: their names, defaults, kinds (positional, keyword-only, *args, **kwargs), and type annotations:

# File: signatures.py
import inspect

def transfer(from_account: str, to_account: str, amount: float, *, currency: str = "USD", note: str | None = None) -> dict:
    \"\"\"Transfer funds between accounts.\"\"\"
    return {"status": "ok"}

sig = inspect.signature(transfer)
print(f"Signature: {sig}")
print(f"Return type: {sig.return_annotation}")

for name, param in sig.parameters.items():
    kind = param.kind.name
    annot = param.annotation if param.annotation is not inspect.Parameter.empty else "(no annotation)"
    default = param.default if param.default is not inspect.Parameter.empty else "(required)"
    print(f"  {name:<14} kind={kind:<22} type={annot} default={default}")

Output:

Signature: (from_account: str, to_account: str, amount: float, *, currency: str = 'USD', note: str | None = None) -> dict
Return type: <class 'dict'>
  from_account   kind=POSITIONAL_OR_KEYWORD  type=<class 'str'>   default=(required)
  to_account     kind=POSITIONAL_OR_KEYWORD  type=<class 'str'>   default=(required)
  amount         kind=POSITIONAL_OR_KEYWORD  type=<class 'float'> default=(required)
  currency       kind=KEYWORD_ONLY           type=<class 'str'>   default=USD
  note           kind=KEYWORD_ONLY           type=str | None      default=None

This is the same data CLI generators and validators use. kind tells you whether a parameter can be passed positionally, by keyword only, or as part of *args/**kwargs. annotation gives you the type hint, ready for runtime validation. default distinguishes required from optional arguments.

Binding Arguments with bind() and bind_partial()

Signature.bind() is the runtime equivalent of "would this function call work?". It takes positional/keyword arguments and either returns a BoundArguments object — showing exactly which parameter got which value — or raises a clean TypeError:

import inspect

def render_email(to: str, subject: str, body: str, cc: list[str] | None = None) -> str:
    return f"To: {to}\\nSubject: {subject}\\n\\n{body}"

sig = inspect.signature(render_email)

# Successful bind — figure out what each value maps to
bound = sig.bind("alice@example.com", "Hello", body="Welcome to our platform")
print(bound.arguments)
# {'to': 'alice@example.com', 'subject': 'Hello', 'body': 'Welcome to our platform'}

# Apply defaults explicitly
bound.apply_defaults()
print(bound.arguments)
# {'to': '...', 'subject': '...', 'body': '...', 'cc': None}

# Failed bind raises TypeError with a clear message
try:
    sig.bind("alice@example.com")  # missing subject + body
except TypeError as e:
    print(f"Bind failed: {e}")
# Bind failed: missing a required argument: 'subject'

Bind is how middleware and decorators answer "did the caller provide all the required arguments?" without actually invoking the wrapped function. It's also useful for testing — pre-validate calls before running them.

signature(): the runtime version of go-to-definition.
signature(): the runtime version of go-to-definition.

Reading Source Code with getsource() and getsourcelines()

When you want the actual Python text behind an object — function, class, or module — getsource() reads it from the file the object was loaded from:

import inspect
import json

# Get a function's source
print(inspect.getsource(json.dumps))

# Get source with line numbers (useful for showing context)
lines, start_line = inspect.getsourcelines(json.dumps)
print(f"\\nStarts at line {start_line}")
for i, line in enumerate(lines[:5], start=start_line):
    print(f"  {i}: {line.rstrip()}")

# Just the file path
print(f"\\nFile: {inspect.getfile(json.dumps)}")

This is how IDE "Jump to Definition" works on objects whose source is available. It fails on built-in C functions (len, print) because they have no Python source — wrap the call in a try/except TypeError.

Class Introspection: Members, MRO, Inheritance

For classes, inspect gives you the method resolution order (MRO), the list of members, and predicates to filter them:

import inspect

class Animal:
    def speak(self): ...
    def eat(self): ...

class Dog(Animal):
    def speak(self):
        return "Woof"
    def fetch(self):
        return "Got the ball!"

# Method resolution order — Python's inheritance chain
print(inspect.getmro(Dog))
# (<class 'Dog'>, <class 'Animal'>, <class 'object'>)

# All methods defined on the class (and inherited ones)
methods = inspect.getmembers(Dog, predicate=inspect.isfunction)
print("Methods:", [name for name, _ in methods])
# Methods: ['eat', 'fetch', 'speak']

# Just the ones defined directly on Dog (not inherited)
own_methods = [name for name, fn in methods if fn.__qualname__.startswith("Dog.")]
print("Own methods:", own_methods)
# Own methods: ['fetch', 'speak']

# Check if a class is a subclass — works on uninstantiated classes
print(inspect.isclass(Dog), inspect.isclass(Dog()))    # True, False
print(issubclass(Dog, Animal))                          # True

The inspect.is* predicates (isfunction, isclass, ismethod, isgenerator, iscoroutinefunction) are how frameworks decide what to do with each attribute they encounter — register coroutines as async routes, treat regular functions as sync routes, mount classes as resource handlers, etc.

FastAPI inspects your handler signatures to build OpenAPI for free.
FastAPI inspects your handler signatures to build OpenAPI for free.

Stack Frames: Knowing Who Called You

Sometimes a function needs to know who called it — for logging, for security checks, for context-aware behavior. inspect.stack() returns the chain of frames leading to the current call:

import inspect

def caller_info():
    frame = inspect.stack()[1]  # [0] is us, [1] is our caller
    return {
        "function": frame.function,
        "filename": frame.filename,
        "lineno": frame.lineno,
        "code": frame.code_context[0].strip() if frame.code_context else None,
    }

def buggy():
    info = caller_info()
    print(f"Called from {info['function']} at {info['filename']}:{info['lineno']}")
    print(f"  Line: {info['code']}")

def my_handler():
    buggy()

my_handler()
# Called from my_handler at example.py:14
#   Line: buggy()

This is the mechanism deprecation warnings use to point at the caller's line, not the deprecated function's own line. Use it sparingly — frame inspection is slow and creates a tight coupling between functions.

A Real Example: Auto-Generating CLI Args from a Function

Combining signature inspection with argparse gives you a 30-line decorator that turns any function into a CLI:

# File: autoargs.py
import argparse
import inspect
from typing import get_type_hints

def cli(func):
    \"\"\"Run func as a CLI, deriving arguments from its signature.\"\"\"
    parser = argparse.ArgumentParser(description=inspect.getdoc(func))
    sig = inspect.signature(func)
    hints = get_type_hints(func)

    for name, param in sig.parameters.items():
        param_type = hints.get(name, str)
        if param.default is inspect.Parameter.empty:
            parser.add_argument(name, type=param_type)
        else:
            parser.add_argument(f"--{name}", type=param_type, default=param.default)

    args = parser.parse_args()
    return func(**vars(args))

@cli
def greet(name: str, *, greeting: str = "Hello", times: int = 1):
    \"\"\"Greet someone, possibly multiple times.\"\"\"
    for _ in range(times):
        print(f"{greeting}, {name}!")

# Run: python autoargs.py Pubs --greeting Howdy --times 3

This is exactly how Typer and Fire work internally. You annotate types on your function, the framework inspects them at decoration time, builds an argparse parser, and dispatches to your function. No boilerplate.

Common Pitfalls

  • getsource() on built-ins. C-implemented functions (len, dict methods, most of itertools) have no Python source — getsource() raises TypeError. Check with inspect.isbuiltin() first, or catch the exception.
  • Signature on overloaded C functions. Some C-implemented callables (open(), print()) don't expose their parameter information in a way inspect.signature() can read. You'll see ValueError: no signature found. Pass follow_wrapped=False or fall back to docstring parsing.
  • Frame leaks. inspect.currentframe() returns a reference to the frame object. If you store that reference (or anything from frame.f_locals) past the function's return, you keep the frame's local variables alive — a memory leak. Always copy out only the bits you need before returning.
  • Type hints as strings. If a module uses from __future__ import annotations (PEP 563), all annotations are stored as strings, not actual types. Use typing.get_type_hints(func) instead of reading param.annotation directly — it evaluates the strings into real types.
  • Decorator transparency. A decorated function's signature is the decorator's signature unless the decorator uses @functools.wraps. Always wrap your decorators — otherwise inspect sees the wrong thing.

FAQ

Q: How is inspect different from dir() and vars()?
A: dir() returns a list of attribute names; vars() returns the __dict__. inspect gives you semantically rich data: parameter kinds and defaults, source location, type annotations, MRO. For listing attributes use dir; for understanding what they ARE use inspect.

Q: Can inspect read the source of code typed interactively in a REPL?
A: No — interactively-defined functions have no source file backing them, so getsource() raises OSError. Their signatures still work (inspect.signature(f)), but the source text was never persisted to disk for Python to recover.

Q: How do I introspect an async function?
A: Use inspect.iscoroutinefunction() to detect that it's async, and inspect.signature() to read its signature — both work the same as for sync functions. To inspect an awaited coroutine object (not the function), use inspect.iscoroutine() and inspect.getcoroutinestate().

Q: Is it safe to use inspect.stack() in production?
A: Yes for occasional use (logging, deprecation warnings), no for hot paths. stack() walks all frames and creates frame objects — it's measurably slow. If you need caller info often, prefer logging contextvars or pass an explicit identifier.

Q: What about typing? When do I use inspect vs typing.get_type_hints()?
A: They complement each other. inspect.signature() gives you the structural shape (which parameters exist, which are required). typing.get_type_hints() resolves the type annotations, including forward references in from __future__ import annotations code. For full introspection of a typed function, use both.

Wrapping Up

The inspect module is one of those tools that quietly powers most of the Python ecosystem. You may never use it directly until you're writing a framework, a debugger, or an admin tool — and then it's indispensable. Start with signature(), getsource(), and getmembers(): those three cover the bulk of practical reflection work. Add stack() and the is* predicates as you encounter advanced cases.

The full inspect module documentation has the complete reference, including the lesser-known utilities like getclosurevars, BoundArguments.arguments, and the AST-level helpers.

Continue Learning Python

Tutorials you might also find useful: