Introduction
The inspect module is your window into the internals of running Python code. It lets you examine live objects at runtime -- retrieving function signatures, querying object types, walking the call stack, and even reading source code. Whether you are building a plugin system, writing a debugger, or creating a framework, inspect is the foundation of Python introspection.
Key Concepts
inspect.signature()-- Returns aSignatureobject describing a function's parameters, defaults, and return annotation.- Type-checking predicates -- Functions like
isfunction(),isclass(),ismodule(), andisbuiltin()that identify what kind of object you are looking at. inspect.ispackage()-- New in Python 3.14, checks whether a module is a package (has a__path__attribute).- Stack frame introspection -- Functions like
currentframe()let you walk the call stack and inspect caller information. - Source retrieval --
getsource()andgetfile()let you read the actual source code of live objects.
Real World Context
Imagine you are building a web framework that automatically registers route handlers. You need to inspect each handler function to determine its parameter names, types, and defaults so you can wire up request data correctly. The inspect module makes this possible without requiring developers to write boilerplate registration code -- frameworks like FastAPI rely heavily on inspect.signature() to achieve this exact pattern.
Deep Dive
The most common use of the inspect module is retrieving function signatures. The signature() function returns a Signature object that gives you structured access to every parameter, its default value, and its type annotation.
pythonimport inspect def my_func(a: int, b: int = 10) -> int: """Add two numbers.""" return a + b sig = inspect.signature(my_func) print(sig) # (a: int, b: int = 10) -> int print(sig.parameters['b'].default) # 10 print(sig.parameters['b'].annotation) # <class 'int'> print(sig.return_annotation) # <class 'int'>
The Signature object provides a dictionary-like .parameters attribute where each key is the parameter name and each value is a Parameter object with .default, .annotation, and .kind attributes.
Python 3.14 adds a new annotation_format parameter to inspect.signature(). This parameter accepts values from the annotationlib.Format enum and controls whether annotations are returned as evaluated objects or as raw strings.
pythonimport inspect from annotationlib import Format def greet(name: str) -> str: return f"Hello, {name}" # Default: returns evaluated annotation objects sig = inspect.signature(greet, annotation_format=Format.VALUE) print(sig.parameters['name'].annotation) # <class 'str'> # Returns string representations instead sig = inspect.signature(greet, annotation_format=Format.STRING) print(sig.parameters['name'].annotation) # 'str'
This is particularly useful when you want to serialize or display signatures without triggering evaluation of forward references.
Beyond signatures, inspect provides a family of predicate functions for querying what type of object you are dealing with. These are essential when writing code that handles heterogeneous collections of objects.
pythonimport inspect import os print(inspect.isfunction(my_func)) # True print(inspect.isclass(int)) # True print(inspect.ismodule(os)) # True print(inspect.isbuiltin(print)) # True
Each predicate returns a simple boolean, making them ideal for filtering and dispatching logic.
Python 3.14 introduces inspect.ispackage(), which checks whether a module is a package -- that is, whether it has a __path__ attribute. This is a subtle but important distinction: a package is a directory-based module that can contain sub-modules, while a regular module is a single .py or .so file.
pythonimport inspect import os import json import email print(inspect.ispackage(email)) # True (email is a package) print(inspect.ispackage(json)) # True (json is a package) import math print(inspect.ispackage(math)) # False (math is a single module) print(inspect.ispackage(os)) # False (os is a module, not a package)
Notice that os returns False even though it feels like a large standard library module -- it is implemented as a single module file, not a package directory.
Stack frame introspection is one of the more powerful (and delicate) features of inspect. You can access the current execution frame and walk backward through the call stack to discover who called the current function, from which line, and in which file.
pythondef who_called_me(): frame = inspect.currentframe() caller = frame.f_back print(f"Called by {caller.f_code.co_name}") print(f"At line {caller.f_lineno}") print(f"In file {caller.f_code.co_filename}") def main(): who_called_me() main() # Called by main # At line 10 # In file example.py
The f_back attribute points to the caller's frame, and each frame carries a f_code object with metadata about the function, plus f_lineno for the current line number.
Finally, inspect can retrieve the actual source code of any object defined in a .py file. This is invaluable for debugging tools, documentation generators, and interactive notebooks.
pythonimport inspect def example(): return 42 print(inspect.getsource(example)) # def example(): # return 42 print(inspect.getfile(example)) # Path to the file
Note that getsource() only works for objects defined in Python source files -- it will raise an OSError for built-in functions or objects defined in C extensions.
Common Pitfalls
- Holding references to stack frames -- Storing frame objects (from
inspect.currentframe()) can create reference cycles that prevent garbage collection. Always delete frame references when you are done with them, or usetry/finallyblocks. - Calling
getsource()on built-ins -- Functions likeprintorlenare implemented in C, not Python. Callinginspect.getsource(print)raisesOSError. Always check withinspect.isbuiltin()first. - Confusing
ispackage()withismodule()-- Every package is a module, but not every module is a package.ismodule(email)returnsTrueANDispackage(email)returnsTrue, butismodule(math)returnsTruewhileispackage(math)returnsFalse.
Best Practices
- Use
inspect.signature()instead of the deprecatedgetfullargspec()-- TheSignatureAPI is more powerful, handles all parameter kinds correctly, and in Python 3.14 supports theannotation_formatparameter. - Prefer
inspectpredicates over manualisinstancechecks -- Usinginspect.isclass(obj)is clearer and more robust thanisinstance(obj, type), and communicates intent better. - Clean up frame references immediately -- If you use
currentframe()orstack(), wrap the usage in atry/finallyblock anddelthe frame variable to avoid reference cycles.
Summary
inspect.signature()gives you structured access to any function's parameters, defaults, and annotations -- and in Python 3.14, supportsannotation_formatfor controlling how annotations are returned.- Type-checking predicates (
isfunction,isclass,ismodule,isbuiltin) let you query what kind of object you are working with. - Python 3.14 adds
inspect.ispackage()to distinguish packages (likeemail) from plain modules (likemathandos). - Stack frame introspection via
currentframe()andf_backlets you walk the call stack, but you must clean up frame references to avoid memory leaks. getsource()retrieves the source code of Python-defined objects, but does not work on C-level built-ins.
Code Examples
import inspect
def who_called_me():
frame = inspect.currentframe()
caller = frame.f_back
print(f"Called by {caller.f_code.co_name}")
def main():
who_called_me()
main() # Called by mainimport inspect
def process(a: int, b: str = 'hello', *args, **kwargs) -> bool:
pass
sig = inspect.signature(process)
for name, param in sig.parameters.items():
print(f"{name}: kind={param.kind.name}, "
f"default={param.default}, "
f"annotation={param.annotation}")
# a: kind=POSITIONAL_OR_KEYWORD, default=<class 'inspect._empty'>, annotation=<class 'int'>
# b: kind=POSITIONAL_OR_KEYWORD, default=hello, annotation=<class 'str'>
# args: kind=VAR_POSITIONAL, ...
# kwargs: kind=VAR_KEYWORD, ...