Last Updated: June 01, 2026
- inspect Quick Example
- What Is the inspect Module?
- Getting Function Signatures with signature()
- Binding Arguments with bind() and bind_partial()
- Reading Source Code with getsource() and getsourcelines()
- Class Introspection: Members, MRO, Inheritance
- Stack Frames: Knowing Who Called You
- A Real Example: Auto-Generating CLI Args from a Function
- Common Pitfalls
- FAQ
- Wrapping Up
- Related Python Tutorials
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.
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.
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.
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.
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()raisesTypeError. Check withinspect.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 wayinspect.signature()can read. You'll seeValueError: no signature found. Passfollow_wrapped=Falseor fall back to docstring parsing. - Frame leaks.
inspect.currentframe()returns a reference to the frame object. If you store that reference (or anything fromframe.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. Usetyping.get_type_hints(func)instead of readingparam.annotationdirectly — 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 — otherwiseinspectsees 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.
Related Python Tutorials
Continue Learning Python
Tutorials you might also find useful: