Module 3: The inspect Module — Python’s Built-in Endoscope¶
Table of Contents¶
- Introduction
- Core 11:
inspect.signature()— Contracts, Kinds, and Binding - Core 12:
getsource,getfile,getmodule— Provenance Is Best-Effort - Core 13:
getmembersvsgetattr_static— Dynamic Values vs Safe Structure - Core 14: Frames and Stack Introspection — Diagnostics Only
- Capstone: A Safe, Signature-Guided
__repr__Mixin - Glossary (Module 3)
Introduction¶
Modules 1–2 gave you raw building blocks: __code__, dir(), getattr, classification predicates, and static lookup. This module introduces inspect: Python’s structured introspection layer—the one that tooling, debuggers, documentation generators, and many frameworks build on.
We cover four practical areas:
- Core 11: Signatures + argument binding —
inspect.signature,Parameter,BoundArguments. - Core 12: Source + provenance —
getsource,getfile,getmodule(best-effort). - Core 13: Member enumeration —
getmembers(dynamic) vsgetattr_static(safe/static). - Core 14: Stack + frames —
currentframe,stack(diagnostics only).
Risk stance (non-negotiable):
inspect.signatureis library-grade when cached.getsource/getfile/getmoduleare best-effort—never make correctness depend on them.getmembersand frame APIs can be slow and side-effectful—reserve them for debugging/tooling.
Capstone: a safe __repr__ mixin that orders fields using signatures and reads state without evaluating properties or walking the stack.
Core 11: inspect.signature() — Contracts, Kinds, and Binding¶
Canonical definition¶
inspect.signature(callable, *, follow_wrapped=True) returns a Signature object describing how the callable should be invoked.
It synthesizes information from (when available):
__signature__(explicit override)__wrapped__chain (whenfollow_wrapped=True)__code__,__defaults__,__kwdefaults__,__annotations__(Python functions)- implementation-specific metadata (many builtins expose enough)
A Signature exposes:
.parameters: ordered mapping of name →Parameter.return_annotation.bind(*args, **kwargs)and.bind_partial(...).replace(...)
A Parameter has:
.name-
.kindin: -
POSITIONAL_ONLY POSITIONAL_OR_KEYWORDVAR_POSITIONAL(*args)KEYWORD_ONLYVAR_KEYWORD(**kwargs).default(orParameter.empty).annotation(orParameter.empty)
Error model (treat as normal, not exceptional):
TypeError: object is not callable, or has a broken custom__signature__.ValueError: signature cannot be provided (some callables implemented in C/extensions).
Deep dive: why bind() matters¶
If you’re writing a decorator, RPC layer, CLI wrapper, validator, tracer, or adapter: do not reimplement argument matching. sig.bind() is your “call simulator”: it enforces the same rules as the interpreter, including positional-only and keyword-only behavior.
Visual: how signature() and bind() relate¶
Source callable
───────────────
def f(a, /, b, *, c=False, **kw): ...
1) inspect.signature(f)
──────────────────────
┌──────────────────────────────────────────────────┐
│ Signature │
│ parameters: Ordered[name → Parameter] │
│ a: POSITIONAL_ONLY │
│ b: POSITIONAL_OR_KEYWORD │
│ c: KEYWORD_ONLY (default False) │
│ kw: VAR_KEYWORD │
└──────────────────────────────────────────────────┘
2) sig.bind(*args, **kwargs)
────────────────────────────
Call: f(10, 20, d=1)
args=(10, 20), kwargs={"d": 1}
┌──────────────────────────────────────────────────┐
│ BoundArguments │
│ arguments: Ordered │
│ a → 10 │
│ b → 20 │
│ c → <missing until apply_defaults()> │
│ kw → {"d": 1} │
└──────────────────────────────────────────────────┘
3) ba.apply_defaults()
──────────────────────
fills c → False
Examples (all runnable)¶
Basic kinds:
import inspect
def demo(a: int, /, b, *, c: bool = False, **kw):
pass
sig = inspect.signature(demo)
print(sig) # (a: int, /, b, *, c: bool = False, **kw)
print(sig.parameters["a"].kind) # POSITIONAL_ONLY
print(sig.parameters["c"].kind) # KEYWORD_ONLY
Binding (including expected-failure handling):
import inspect
def demo(a: int, /, b, *, c: bool = False, **kw):
pass
sig = inspect.signature(demo)
# OK
ba = sig.bind(10, 20, d=1)
print(ba.arguments) # {'a': 10, 'b': 20, 'kw': {'d': 1}}
# Defaults are not applied unless you ask
ba.apply_defaults()
print(ba.arguments) # includes c=False
# Expected failure: positional-only passed as keyword
try:
sig.bind(a=10, b=20)
except TypeError as e:
print("Expected:", e)
Bound vs unbound methods:
import inspect
class C:
def meth(self, x: int = 0, *, y):
pass
print(inspect.signature(C.meth)) # (self, x: int = 0, *, y)
print(inspect.signature(C().meth)) # (x: int = 0, *, y)
Wrappers and __wrapped__ (why functools.wraps matters):
import inspect
import functools
def deco(f):
@functools.wraps(f)
def wrapper(*args, **kwargs):
return f(*args, **kwargs)
return wrapper
@deco
def f(a, /, b, *, c=0): ...
print(inspect.signature(f)) # follows __wrapped__ by default
Callables with no signature available (never assume which ones):
import inspect
def safe_signature(obj):
try:
return str(inspect.signature(obj))
except (TypeError, ValueError) as e:
return f"<no signature: {type(e).__name__}: {e}>"
print(safe_signature(len)) # may succeed on modern CPython; do not rely
print(safe_signature(object())) # not callable → TypeError
Advanced notes and pitfalls (precise, no myths)¶
- Do not claim caching guarantees.
inspect.signaturecan be expensive; cache it yourself (e.g.,functools.lru_cachekeyed by callable identity). follow_wrapped=Truemeansinspect.signaturewill traverse__wrapped__(set byfunctools.wraps) unless you passfollow_wrapped=False.Signature.bind()raisesTypeErrorwith interpreter-like messages. That’s a feature; surface those messages rather than inventing new ones unless you have a strong reason.
Use / avoid
- Use whenever you forward, validate, log, or document arguments.
- Avoid calling repeatedly in hot paths; cache per callable.
Exercise¶
Write @validate_call that:
- caches
inspect.signature(func)once, - does
ba = sig.bind(*args, **kwargs)andba.apply_defaults(), - validates types using your Module 5
_is_instancesubset, - raises
TypeErrorwith clear, minimal messages.
Core 12: getsource, getfile, getmodule — Provenance Is Best-Effort¶
Canonical definition¶
inspect.getsource(obj)→ source text (function/class/module) or raisesinspect.getfile(obj)→ filename string (or synthetic path) or raisesinspect.getmodule(obj)→ module object (orNone)
These are not “introspection truth.” They are “recover what we can.”
Deep dive: when it fails (and why you must handle it)¶
These APIs depend on how code was loaded and whether source is available:
- REPL/Jupyter
exec/evalcode (co_filenamelike"<string>")- zipimport
- frozen executables
- coverage/instrumentation/AST transforms (line info may shift)
- stripped-source deployments
Visual: provenance success/failure pipeline¶
Object ──► code object / module metadata ──► filename ──► linecache ──► source slice
(may not exist) (may be fake) (may miss) (may raise)
Typical failures:
• interactive: filename "<stdin>" / transient cells → linecache mismatch → OSError
• exec/eval: filename "<string>" → no file → OSError
• frozen/zipimport: file not present → OSError / TypeError
Examples (always safe)¶
import inspect
def demo():
return 42
print(inspect.getmodule(demo)) # often <module '__main__' ...>
try:
print(inspect.getfile(demo))
except TypeError as e:
print("Expected:", e)
try:
print(inspect.getsource(demo))
except (OSError, TypeError) as e:
print("Expected:", type(e).__name__, e)
A library-grade helper:
import inspect
def safe_getsource(obj, fallback="<<source unavailable>>"):
try:
return inspect.getsource(obj)
except (OSError, TypeError):
return fallback
print(safe_getsource(lambda x: x))
Use / avoid
- Use in dev tools, debug UI, documentation tooling you control.
- Avoid making correctness depend on it in libraries/services.
Exercise¶
Implement where_defined(obj) returning (module_name, file_or_none, first_line_or_none) with best-effort behavior and no exceptions escaping.
Core 13: getmembers vs getattr_static — Dynamic Values vs Safe Structure¶
Canonical definition¶
inspect.getmembers(obj, predicate=None):
- uses
dir(obj)to get names, - then calls
getattr(obj, name)for each name, - optionally filters by predicate,
- returns sorted
(name, value)pairs.
This is dynamic lookup and will execute descriptors and properties.
inspect.getattr_static(obj, name) performs static lookup:
- bypasses
__getattribute__,__getattr__, and the descriptor protocol, - returns the raw attribute object (e.g., a
property) without evaluating it.
Why this matters¶
Many codebases accidentally write “introspection” that triggers production side effects:
- properties that lazily connect to a database,
- descriptors that compute/cached-load values,
__getattr__that proxies to remote systems.
If you want structure, use static lookup. If you want values, accept side effects explicitly.
Visual: dynamic vs static attribute resolution¶
Dynamic (normal Python attribute access)
────────────────────────────────────────
getattr(obj, "x") / obj.x
1) data descriptor on type(obj)? -> __get__/__set__ involved
2) obj.__dict__?
3) non-data descriptor / class attr?
4) __getattr__ fallback?
This can execute:
• property fget
• descriptor __get__
• custom __getattribute__/__getattr__
Static (inspect.getattr_static)
───────────────────────────────
inspect.getattr_static(obj, "x")
• returns the raw attribute object
• does NOT invoke descriptors
• does NOT call __getattr__/__getattribute__
Demonstration: getmembers can trigger side effects¶
import inspect
class Example:
@property
def expensive(self):
print("SIDE EFFECT: property executed")
return 123
def meth(self): ...
x = 10
e = Example()
# Dynamic enumeration: may execute expensive (depending on predicate and order)
_ = inspect.getmembers(e) # prints side effect
# Static: returns descriptor object, no evaluation
raw = inspect.getattr_static(Example, "expensive")
print(type(raw).__name__) # property
Safe enumeration pattern for frameworks/tools¶
import inspect
def static_getmembers(obj, predicate=None):
for name in dir(obj):
value = inspect.getattr_static(obj, name)
if predicate is None or predicate(value):
yield name, value
class Example:
@property
def expensive(self):
return 123
def meth(self): ...
print([n for n, v in static_getmembers(Example, inspect.isfunction) if n == "meth"])
Use / avoid
- Use
getmembersfor quick REPL exploration or controlled debugging. - Use
getattr_staticfor any library/framework introspection. - Avoid dynamic enumeration on instances with properties/lazy descriptors.
Exercise¶
Implement list_properties(cls) that returns property names and their docstrings using only getattr_static (no evaluation).
Core 14: Frames and Stack Introspection — Diagnostics Only¶
Canonical definition¶
inspect.currentframe()returns the current frame object orNone.inspect.stack()builds a list ofFrameInfofor the current call stack.
Frames expose:
f_code(code object)f_locals,f_globalsf_back(caller frame)
Deep dive: cost and memory hazards¶
inspect.stack()walks the full Python stack and consultslinecache— expensive.- Frames keep references to locals and back-links; keeping frames alive can retain large object graphs (leak-like behavior).
Visual: frames keep things alive¶
frame ──► f_locals (dict) ──► local objects
│
└──► f_back ──► previous frame ──► previous locals ──► ...
If you store `frame` or `traceback` in a long-lived structure,
you keep the entire chain alive until those references are released.
Example: safe top-of-stack inspection without inspect.stack()¶
import inspect
def top_callers(limit=3):
f = inspect.currentframe()
if f is None:
return []
try:
out = []
cur = f.f_back # skip this helper’s own frame
while cur is not None and len(out) < limit:
out.append(cur.f_code.co_name)
cur = cur.f_back
return out
finally:
del f # break cycles
def outer():
def inner():
print(top_callers(5))
inner()
outer()
Example: snapshot locals safely¶
import inspect
def snapshot_locals(limit=2):
f = inspect.currentframe()
if f is None:
return []
try:
out = []
cur = f.f_back
while cur is not None and len(out) < limit:
out.append(dict(cur.f_locals)) # snapshot
cur = cur.f_back
return out
finally:
del f
def foo():
a = 1
return snapshot_locals(1)
print(foo()[0].get("a")) # 1
Use / avoid
- Use in crash reporting, debug assertions, developer tooling.
- Avoid as normal control-flow, and avoid in hot paths.
Exercise¶
Implement assert_called_from(allowed_prefixes) that checks the immediate caller function/module and raises a clear RuntimeError in debug mode only.
Capstone: A Safe, Signature-Guided __repr__ Mixin¶
Goal: produce a good repr that is:
- stable across regular and slotted classes,
- ordered by
__init__signature when possible, - does not evaluate properties,
- avoids
getmembersand avoids stack/frame inspection.
Key idea: only read state from:
- instance
__dict__(if present), - declared
__slots__names from the MRO, - use
object.__getattribute__to avoid custom__getattribute__traps.
Visual: safe repr data flow (no properties)¶
instance
│
├─► __dict__ (if present) (safe: raw storage)
│
├─► __slots__ names from MRO (safe: slot descriptors)
│ │
│ └─► object.__getattribute__ (avoids custom lookup)
│
└─► order fields using signature(__init__) when possible
import inspect
class ReprMixin:
def __repr__(self):
cls = type(self)
state = {}
# 1) __dict__ (if any), read without invoking custom attribute logic
try:
d = object.__getattribute__(self, "__dict__")
except Exception:
d = None
if isinstance(d, dict):
state.update(d)
# 2) slots (from MRO), excluding private names and duplicates
for base in cls.__mro__:
slots = getattr(base, "__slots__", None)
if not slots:
continue
if isinstance(slots, str):
slots = (slots,)
for name in slots:
if name in ("__dict__", "__weakref__"):
continue
if name.startswith("_") or name in state:
continue
try:
state[name] = object.__getattribute__(self, name)
except AttributeError:
pass
# 3) order using __init__ signature when possible
ordered_items = None
try:
sig = inspect.signature(cls.__init__)
order = [
p.name
for p in sig.parameters.values()
if p.name != "self"
and p.kind not in (inspect.Parameter.VAR_POSITIONAL, inspect.Parameter.VAR_KEYWORD)
]
seen = set()
ordered = []
for n in order:
if n in state:
ordered.append((n, state[n]))
seen.add(n)
extras = [(n, v) for n, v in state.items() if n not in seen]
ordered_items = ordered + sorted(extras, key=lambda t: t[0])
except (TypeError, ValueError):
pass
items = ordered_items if ordered_items is not None else sorted(state.items(), key=lambda t: t[0])
args = ", ".join(f"{k}={v!r}" for k, v in items)
# Optional doc hint (first line only)
doc = inspect.getdoc(cls) or ""
hint = doc.splitlines()[0].strip() if doc else ""
return f"{cls.__name__}({args})" + (f" # {hint}" if hint else "")
Demo (regular + slotted):
class A(ReprMixin):
"""regular"""
def __init__(self, x, y=0):
self.x = x
self.y = y
class B(ReprMixin):
"""slotted"""
__slots__ = ("x", "y")
def __init__(self, x, y=0):
self.x = x
self.y = y
print(A(1)) # A(x=1, y=0) # regular
print(B(2)) # B(x=2, y=0) # slotted
Exercise¶
Add an opt-in flag:
- class attribute
_repr_include = ("a", "b", ...)to include only those names - default remains “include all discovered state”
- ensure it never evaluates properties (do not call
getattron arbitrary names)
Glossary (Module 3)¶
| Term | Definition |
|---|---|
inspect module |
Python’s structured introspection toolkit used by debuggers, doc tools, and frameworks; powerful but not always cheap or side-effect free. |
| Signature | inspect.Signature object describing how a callable should be invoked (parameters + return annotation). |
inspect.signature |
Retrieves a callable’s signature using __signature__, __wrapped__, or function metadata; can raise TypeError/ValueError and can be expensive—cache it. |
__signature__ override |
Explicit signature attached to a callable to control what inspect.signature reports (common in wrappers/frameworks). |
__wrapped__ chain |
Wrapper linkage used by functools.wraps; enables tools to see through decorators when follow_wrapped=True. |
follow_wrapped |
inspect.signature(..., follow_wrapped=True) behavior that traverses __wrapped__ to reach the original callable. |
| Parameter | inspect.Parameter: one formal argument in a signature (name, kind, default, annotation). |
| Parameter kind | One of the five invocation categories: POSITIONAL_ONLY, POSITIONAL_OR_KEYWORD, VAR_POSITIONAL, KEYWORD_ONLY, VAR_KEYWORD. |
Positional-only (/) |
Parameters that must be passed positionally; binding them by keyword is a TypeError. |
Keyword-only (*) |
Parameters that must be passed by keyword; cannot be supplied positionally. |
| Defaults application | BoundArguments.apply_defaults() populates omitted parameters with defaults; binding alone does not. |
| Argument binding | sig.bind(*args, **kwargs) (or bind_partial) simulates interpreter argument matching; the correct way to validate/forward calls. |
| BoundArguments | Result of binding: ordered mapping from parameter names to passed values (plus captured *args/**kwargs). |
bind vs bind_partial |
bind requires all required parameters; bind_partial permits missing required parameters (useful for partial application). |
| Bound vs unbound method signature | inspect.signature(C.meth) includes self; inspect.signature(C().meth) omits it because it’s already bound. |
| Signature availability | Some C-extension/built-in callables may not provide a signature; inspect.signature can raise ValueError—never assume uniform support. |
| Provenance | Best-effort “where did this object come from?” recovery via inspect.getsource, getfile, getmodule; not correctness-grade. |
inspect.getsource |
Attempts to recover source text; may fail in REPL/Jupyter, exec, zipimport, frozen builds, or transformed code—must be exception-safe. |
inspect.getfile |
Best-effort filename lookup; may be synthetic ("<string>") or absent, and can raise TypeError. |
inspect.getmodule |
Best-effort mapping from object to module object; may return None. |
| Linecache dependency | Source recovery relies on cached file lines (linecache); mismatches or missing sources produce OSError/TypeError. |
| Member enumeration | Discovering attributes on an object; can be structural (safe) or value-resolving (side-effectful). |
| Dynamic member enumeration | inspect.getmembers(obj) uses dir() + getattr() per name; executes descriptors/properties and can trigger heavy side effects. |
| Static lookup | inspect.getattr_static(obj, name) returns the raw attribute/descriptor without invoking descriptor protocol or __getattr__. |
| Structural introspection | Inspecting “what is attached” (raw descriptors/attributes) rather than “what would evaluating it do” (executed values). |
| Side-effectful introspection | Any introspection that calls getattr/evaluates properties or triggers proxy logic (common accidental production bug in tooling). |
| Frame | Execution record object (f_code, f_locals, f_globals, f_back) used for diagnostics and debugging. |
inspect.currentframe |
Returns the current frame or None; useful for limited diagnostics but must be handled carefully. |
| Stack inspection | inspect.stack() walks frames and consults linecache; expensive and can retain object graphs—diagnostics only. |
| Frame retention hazard | Holding references to frames/tracebacks can keep locals and caller chains alive, causing leak-like memory retention. |
| Safe repr pattern | Build __repr__ from raw storage (__dict__ + slots) and stable ordering (signature), without evaluating properties or walking the stack. |
object.__getattribute__ escape hatch |
Bypasses custom __getattribute__ overrides to read attributes more predictably (still may trigger slot mechanics, but avoids custom traps). |
Proceed to Module 4: Decorators Level 1 — Function Transformation Basics.