In Python, everything is an object by default (classes, functions, tracebacks, methods, modules, etc). We can create a class object by using the special type() method without using class keyword. Sometimes we need to analyze these objects to understand them better. Python has provided us with a module named inspect for this purpose. It provides a list of methods that can let us inspect python objects to understand them better. It let us perform a list of the below-mentioned functions and many more on the objects.
The information provided by inspect module can be used for type checking, analyzing source code of objects, analyzing classes and functions, and interpreting interpreter stack information.
We have divided our tutorial into sections and each section has a list of methods that are generally used for the same purpose.
We'll be explaining the list of methods in each section with simple examples to demonstrate the usage of those methods. We'll start by importing inspect module.
import inspect
As a part of this section, we'll introduce methods that let us find out the type of the object given that we have no information about it. We'll introduce methods that let us know whether the object that we are analyzing is class, module, method, function, traceback, generator, code, coroutine, built-in, etc.
This method takes as input object and returns True if it’s of type module else False. It determines whether the object is a module or not based on the presence of the below attributes in the object.
__cached__
__doc__
__file__
This method takes as input object and returns True if it’s of type class else False. It determines whether the object is a class or not based on the presence of the below attributes in the object.
__doc__
__module__
This method takes as input object and returns True if it’s of type method else False. It determines whether an object is a method or not based on the presence of the below attributes in the object.
__doc__
__name__
__func__
__self__
This method takes as input object and returns True if it is of type function else False. It determines whether an object is a function or not based on the presence of the below attributes in the object.
__doc__
__name__
__code__
__defaults__
__globals__
__annotations__
__kwdefaults__
Below we have explained the usage of the methods with simple examples. We have created a function named addition for testing methods related to function objects.
import random
import collections
## This is a Generic Addition Function.
## It can take any number of integer arguments and sum them up.
def addition(**args):
'''
Description:
This function takes as input any number of integer arguments, sum them up and returns results.
Params:
*args: List of Values
'''
return sum(args)
print("Is {} a Module : {}".format(random.__name__, inspect.ismodule(random)))
print("Is {} a Module : {}".format(addition.__name__, inspect.ismodule(addition)))
print()
print("Is {} a Class : {}".format(random.__name__, inspect.isclass(random)))
print("Is {} a Class : {}".format(random.Random.__name__, inspect.isclass(random.Random)))
print("Is {} a Class : {}".format(collections.ChainMap.__name__, inspect.isclass(collections.ChainMap)))
print()
print("Is {} a Method : {}".format(addition.__name__, inspect.ismethod(addition)))
print("Is {} a Method : {}".format(random.randint.__name__, inspect.ismethod(random.randint)))
print()
print("Is {} a Function : {}".format(addition.__name__, inspect.isfunction(addition)))
print("Is {} a Function : {}".format(random.randint.__name__, inspect.isfunction(random.randint)))
print()
This method takes as input object and returns True if it’s of type generator function else False. It determines whether an object is a generator function or not based on the presence of the below attributes in the object.
__doc__
__name__
__code__
__defaults__
__globals__
__annotations__
__kwdefaults__
This method takes as input object and returns True if it’s of type generator else False. It determines whether an object is a generator or not based on the presence of the below attributes in the object.
__iter__
close
gi_code
gi_frame
gi_running
next
send
throw
Below we have created two generators (one using a function and another using parenthesis notation) for testing these functions.
def countGenerator():
for i in range(100):
yield i
countGen = (i for i in range(100))
print("Is {} a Generator Function : {}".format(countGenerator.__name__, inspect.isgeneratorfunction(countGenerator)))
print("Is {} a Generator Function : {}".format(addition.__name__, inspect.isgeneratorfunction(addition)))
print()
print("Is {} a Generator : {}".format(countGen.__name__, inspect.isgenerator(countGen)))
print("Is {} a Generator : {}".format(addition.__name__, inspect.isgenerator(addition)))
This method takes as input object and returns True if it’s of type coroutine function else False. It determines whether an object is a coroutine function or not based on the presence of attributes same as the one present in the function.
This method takes as input an object and returns True if it’s of type code else False. It determines whether an object is a code or not based on the presence of the below attributes in the object.
co_argcount
co_code
co_cellvars
co_consts
co_filename
co_firstlineno
co_flags
co_freevars
co_kwonlyargcount
co_lnotab
co_name
co_names
co_nlocals
co_stacksize
co_varnames
Below we have explained how we can use these methods. We have created another version of the addition method with async keyword prefix to indicate that it’s a coroutine. We can access the code of a function with the help of __code__
attribute of the function object.
async def addition_async(**args):
return sum(args)
print("Is {} a Coroutine Function : {}".format(addition_async.__name__, inspect.iscoroutinefunction(addition_async)))
print("Is {} a Coroutine Function : {}".format(addition.__name__, inspect.iscoroutinefunction(addition)))
print()
print("Is {} a Code : {}".format("addition.__code__", inspect.iscode(addition.__code__)))
print("Is {} a Code : {}".format(addition.__name__, inspect.iscode(addition)))
This method takes as input an object and returns True if it’s of type traceback else False. It determines whether an object is a traceback or not based on the presence of the below attributes in the object.
tb_frame
tb_lasti
tb_lineno
tb_next
This method takes as input an object and returns True if it’s of type frame else False. It determines whether an object is a frame or not based on the presence of the below attributes in the object.
f_back
f_builtins
f_code
f_globals
f_lasti
f_lineno
f_locals
f_trace
This method takes as input an object and returns True if it’s of type builtin keyword else False. It determines whether an object is a built-in keyword or not based on the presence of the below attributes in the object.
__doc__
__name__
__self__
This method takes as input an object and returns True if it’s of type routine (function/method) else False.
This method takes as input an object and returns True if it’s of type abstract base class else False.
tb = None
try:
a = 10/0
except Exception as e:
tb = e.__traceback__
print("Is {} a Traceback : {}".format("tb", inspect.istraceback(tb)))
print("Is {} a Traceback : {}".format(addition.__name__, inspect.istraceback(addition)))
print()
print("Is {} a Frame : {}".format("tb", inspect.isframe(tb)))
print("Is {} a Frame : {}".format("tb.tb_frame", inspect.isframe(tb.tb_frame)))
print()
print("Is {} a Builtin : {}".format(sum.__name__, inspect.isbuiltin(sum)))
print("Is {} a Builtin : {}".format(addition.__name__, inspect.isbuiltin(addition)))
print()
print("Is {} a Routine : {}".format(sum.__name__, inspect.isroutine(sum)))
print("Is {} a Routine : {}".format(addition.__name__, inspect.isroutine(addition)))
print()
print("Is {} a Abstract Base Class : {}".format(sum.__name__, inspect.isabstract(random.Random)))
print("Is {} a Abstract Base Class : {}".format(collections.abc.Hashable.__name__, inspect.isabstract(collections.abc.Hashable)))
This function takes as input object and returns a list of members of the object. Each entry is a tuple of two values (member name and member value). The member can be method, class, attribute, coroutine, etc.
It has a second parameter named predicate which accepts a reference to one of the above-mentioned is*() method as input. It then returns a list with members that return True for that method.
Below we have called the method on random module and passed isclass method as a predicate. It returns only members of a random module which are class. We have then called the method again two times with different predicates.
import random
inspect.getmembers(random, inspect.isclass)
inspect.getmembers(random, inspect.ismodule)
inspect.getmembers(random, inspect.ismethod)
This function accepts the path to the module and returns the module name without including package names. It's based on importlib module of python.
inspect.getmodulename("/home/sunny/anaconda3/lib/python3.7/random.py")
inspect.getmodulename("/home/sunny/anaconda3/lib/python3.7/collections.py")
As a part of this section, we'll introduce methods that can let us know details about the source code of the object like code, comments, docs, source file name, etc. We'll explain the usage of these methods with a simple example to demonstrate how to use them,
This function accepts an object as its first parameter and returns the documentation string for that object. If the provided object is a class, a method, or property and doc are not present then it tries to retrieve doc from the inheritance hierarchy.
We have printed documentation string for collections module and addition method below using this method.
import collections
collections_docs = inspect.getdoc(collections)
print(collections_docs)
addition_docs = inspect.getdoc(addition)
print(addition_docs)
This function takes as input object and returns a list of comments which are present before the source code of that object if its class or method. If the passed object is a module then it tries to retrieve comments from the beginning of the file. If there are no comments present then it returns None.
defaultdict_comments = inspect.getcomments(collections.defaultdict)
print(defaultdict_comments)
addition_comments = inspect.getcomments(addition)
print(addition_comments)
This function takes an object as input and returns a reference to the module in which it was defined. We have called the method with randint function and Counter class from random and collections modules respectively to retrieve both modules below.
inspect.getmodule(random.randint)
inspect.getmodule(collections.Counter)
This function takes an object as input and returns the file name in which that object was defined.
inspect.getfile(random)
inspect.getfile(collections.abc)
inspect.getfile(collections.deque)
This function takes as input an object and returns a filename where the source code of the object is present.
inspect.getsourcefile(random)
inspect.getsourcefile(collections.abc)
inspect.getsourcefile(collections.ChainMap)
This function takes an object as input and returns a tuple of length two. The value in the tuple is the source code of the object as a list of strings and the second value is the line number in the file in which the code starts. It can take an object of a type class, method, function, coroutine, traceback, frame, code, etc.
source_lines = inspect.getsourcelines(addition_async)
print("Return Type of inspect.getsourcelines() : ", type(source_lines))
print("Line Number Where Object Starts : ", source_lines[1], "\n")
for line in source_lines[0]:
print(line, end="")
source_lines = inspect.getsourcelines(addition)
for line in source_lines[0]:
print(line, end="")
source_lines = inspect.getsourcelines(random.randint)
print("Line Number Where Object Starts : ", source_lines[1], "\n")
print("Source Code : \n")
for line in source_lines[0]:
print(line, end="")
This function takes as input an object and returns the source code of the object as a string. It accepts an object of a type class, method, function, coroutine, traceback, etc as input.
source = inspect.getsource(addition)
print("Return Type of inspect.getsource() : ", type(source), "\n")
print(source)
source = inspect.getsource(random.randint)
print("Return Type of random.randint() : ", type(source), "\n")
print(source)
This function takes as input docstrings and cleans them up by removing leading or trailing white spaces.
addition_docs = inspect.getdoc(addition)
print(inspect.cleandoc(addition_docs))
As a part of this section, we'll introduce methods that can help us analyze class and functions better. It has methods that can help us understand class hierarchy, method resolution order, function arguments, function argument values, etc. We'll explain the usage of these methods with simple examples.
This function takes as input a list of classes and returns classes organized in a hierarchical structure based on their inheritance. It returns a list of 2 value tuples as output. The first value in the tuple is the class itself and the second value is a list of base classes of that class. Each entry in the list precedes a list of classes from which the current entry inherits.
It has a parameter named unique which is False by default hence entries for the class can be present more than once if it’s inherited by multiple classes. We can prevent repeated entries by setting this parameter to True.
Below we have created a class hierarchy where class B and C extends class A, class D extends B, and class E extends class C. We have then passed a list of these classes in arbitrary order to this method. It organizes classes in the order in which inheritance follows through them.
The first entry is for an object which does not have a superclass. Each entry has a superclass present and an entry before that entry has an entry for that superclass.
class A:
pass
class B(A):
pass
class C(A):
pass
class D(B):
pass
class E(C):
pass
import pprint
print(pprint.pformat(inspect.getclasstree([E,D,C,B,A])))
Below we have created another class named F which extends class D and class E both. We have then printed the hierarchy again. We have also printed hierarchy by setting unique parameter to True to avoid repeated entries.
class F(D,E):
pass
import pprint
print(pprint.pformat(inspect.getclasstree([A,B,C,D,E,F]), indent=2))
import pprint
print(pprint.pformat(inspect.getclasstree([A,B,C,D,E,F], unique=True)))
This function takes as input class object and returns a list of classes which forms method resolution order for this class. These are the classes in which an interpreter looks for a method in this order when it does not found the method in the original class.
inspect.getmro(E)
inspect.getmro(F)
This function takes as input function object and returns a named tuple that has information about function arguments and default values. The returned named tuple has below mentioned parameters.
*args
parameters.**kwargs
parameters.Below we have tried to explain the usage of the method with simple examples.
inspect.getfullargspec(random.choices)
inspect.getfullargspec(random.randint)
class Addition:
def __init__(self):
pass
@classmethod
def addition(self, *integers_lst: list, power: int=None) -> int:
return sum(*integers_lst)**power if power else sum(*integers_lst)
arg_spec = inspect.getfullargspec(Addition.addition)
arg_spec
print("Annotations : ", arg_spec.annotations)
print("Var Args : ", arg_spec.varargs)
This function has the form getcallargs(func, *args, **args)
which lets us provide function object and values of its parameters when calling it. It'll then return a dictionary where each parameter of the function is mapped to their values.
inspect.getcallargs(Addition.addition)
inspect.getcallargs(Addition.addition, 1,2,3,4)
inspect.getcallargs(Addition.addition, 1,2,3,4, power=2)
This method can be used to retrieve attribute values when we don't want code execution for retrieving the attribute. Python interpreter can call __getattribute__
or __getattr__
methods when we try to retrieve or check existence of an attribute using getattr() or hasattr() methods. If we don't want these methods to be executed to retrieve attribute values then we should use getattr_static() method.
Below we have explained with simple example the difference between calling getattr() and getattr_static().
class Employee:
def __init__(self, emp_name, emp_id):
self.emp_name = emp_name
self.emp_id = emp_id
def __getattr__(self, attr):
print("Inside __getattr__")
return getattr(self, attr)
def __getattribute__(self, attr):
print("Inside __getattribute__")
return super().__getattribute__(attr)
e1 = Employee("Sunny", 124)
e1.employee_email = "sunny.2309@yahoo.in"
print("emp_id is present in e1 : ", hasattr(e1, "emp_id"))
print("emp_id is present in e1 : ", getattr(e1, "emp_id"))
print("\nemp_id is present in e1 : ", inspect.getattr_static(e1, "emp_id"))
As a part of this section, we'll introduce methods that let us analyze the signature of the callable objects. It let us retrieve parameter names, their default values, their annotations, etc.
This function takes as input function object and returns Signature object which has information about the signature of the function. It even holds information about the annotation of the function.
Below we have retrieved signatures of few functions and printed them.
sum_signature = inspect.signature(sum)
sum_signature
def addition(a:int=0, b:int=0) -> int:
return a + b
addition_signature = inspect.signature(addition)
addition_signature
choices_signature = inspect.signature(random.choices)
choices_signature
print("Sum Signature : ", sum_signature)
print("Addition Signature : ", addition_signature)
print("Random.Choices Signature : ", choices_signature)
The Signature object has a method named from_callable() which also lets us retrieve the signature as well.
#inspect.Signature.from_function(random.choices) Deprectated
inspect.Signature.from_callable(random.choices)
The Signature object has return_annotation attribute which returns annotation of the return type of the function if present.
print("Sum Return Annotation : ", sum_signature.return_annotation)
print("Addition Return Annotation : ", addition_signature.return_annotation)
print("Random.Choices Return Annotation : ", choices_signature.return_annotation)
This function lets us modify various attributes of the Signature object. Below we are modifying the return annotation of the addition function. We can even modify the parameters of the function by providing a list of Parameter objects to parameters parameter in the call to this method.
new_sum_signature = sum_signature.replace(return_annotation = float)
new_addition_signature = addition_signature.replace(return_annotation = str)
print("Sum Signature : ", new_sum_signature)
print("Addition Signature : ", new_addition_signature)
print("Sum Return Annotation : ", new_sum_signature.return_annotation)
print("Addition Return Annotation : ", new_addition_signature.return_annotation)
The parameters attribute returns an ordered dictionary of parameters of the function. It orders parameters in the order in which they are present in the signature. Each value in the dictionary is an object of type Parameter which has information about a particular parameter of that signature.
print("Sum Parameters : ", sum_signature.parameters)
print("Addition Parameters : ", addition_signature.parameters)
print("Random.Choices Parameters : ", choices_signature.parameters)
This function takes as input values of the parameter and returns BoundedArguments object which is binding from parameter and their values. It has the signature of the form bind(*args, **args)
which lets us bind values to the parameter in different ways (directly giving them as a list or giving them as a dictionary where the key is parameter name and value is parameter value).
This function will fail if we don't provide all the required arguments by the function.
bounded_args = addition_signature.bind(a=100)
bounded_args
The BoundedArguments object has method named apply_default() which maps default values to parameters for which value is not provided when creating this object using bind() method.
bounded_args.apply_defaults()
bounded_args
bounded_args.signature
We can even call the function once we have bind values to the parameters in BoundedArguments instance.
addition(*bounded_args.args, **bounded_args.kwargs)
This function works exactly the same way as bind() function with the only difference that it let us create BoundedArguments object without providing some required arguments.
This function won't fail if we don't provide required arguments but when we call our actual function using BoundedArguments, it'll fail if required arguments are not present.
bounded_args = addition_signature.bind_partial(25)
bounded_args
bounded_args.apply_defaults()
bounded_args
addition(*bounded_args.args, **bounded_args.kwargs)
The parameter information about callable is held in Parameter object which is present in parameters attribute of Signature object. Below we have listed down important attributes of the Parameter object.
Parameter object is immutable hence we can not modify it.
for param_name, param in addition_signature.parameters.items():
print("Name : %s, Default Value : %s, Annotation : %s, Type : %s"%(param.name, param.default, param.annotation, param.kind))
for param_name, param in choices_signature.parameters.items():
print("\nName : %s, Default Value : %s, Annotation : %s, Type : %s"%(param.name, param.default, param.annotation, param.kind))
This method lets us create a new parameter object by replacing the value of the original parameter object. We had mentioned earlier that parameter object is immutable hence this method creates a copy of it with attribute values replaced.
Below we are changing parameter name, annotation, and the default value for both parameters of the addition method to create a new parameter object for both parameters. We are then changing the signature object by using its replace() method to create a new signature object using these newly created parameter objects.
a_param = addition_signature.parameters["a"]
b_param = addition_signature.parameters["b"]
a_param, b_param
new_a_param = a_param.replace(name="param1", annotation=float, default=0.0)
new_b_param = b_param.replace(name="param2", annotation=float, default=0.0)
new_a_param, new_b_param
new_addition_signature = addition_signature.replace(return_annotation = float, parameters=[new_a_param, new_b_param])
print("Addition Signature : ", addition_signature)
print("Addition Signature : ", new_addition_signature)
As a part of this section, we'll introduce methods that can help us understand the stack trace of the python interpreter when exceptions occur. We'll explain the usage of the methods with simple examples.
Below we have created a simple method that performs division. We have then called the division method with parameters 10 and 0 in the try-except statement to catch division by zero exception. We have also recorded traceback objects which we'll use later when explaining the methods of this section.
If you are interested in learning about how to capture, format, and print traceback of exceptions then please feel free to check our tutorial on the same.
def division(a, b):
return a/b
try:
out = division(10, 0)
except Exception as e:
print("Exception : ", e)
tb = e.__traceback__
This function takes as input frame object and returns information about the traceback. It returns named tuple Traceback which has information like filename, line number, calling function, index, and code context. Below we have retrieved traceback using the frame available from the traceback instance which we captured earlier. We have also printed formatted traceback.
Please make a note that all the methods of this section have an argument named context which accepts an integer and returns that many lines of context.
traceback_obj = inspect.getframeinfo(tb.tb_frame, context=1)
traceback_obj
print("Filename : ", traceback_obj.filename)
print("Line No : ", traceback_obj.lineno)
print("Function : ", traceback_obj.function)
print("Index : ", traceback_obj.index)
print("Code Context : \n")
for line in traceback_obj.code_context:
print(line, end="")
Below we have called getframeinfo() function again but this time with a context value of 8 which will add a number of lines around the frame that we passed.
traceback_obj = inspect.getframeinfo(tb.tb_frame, context=8)
traceback_obj
print("Filename : ", traceback_obj.filename)
print("Line No : ", traceback_obj.lineno)
print("Function : ", traceback_obj.function)
print("Index : ", traceback_obj.index)
print("Code Context : \n")
for line in traceback_obj.code_context:
print(line, end="")
This function takes as input frame object and returns all outer frames including the frame given as a parameter. It includes frames which have information on calls that resulted in exception. It returns a list of named tuple object FrameInfo objects which have information like filename, line number, function, index, and code context. The first frame info object has information about the frame which we passed as input and the last frame has information about the outermost call on that frame's total stack.
Below we have tried to retrieve all outer frames of the frame from our failure traceback. We have then printed all frame info objects. We have called randint function of the random module with negative arguments so that it fails and we can capture a trace of it for explaining function usage.
Please make a note that the second frame info which is printed in formatted output has code that was executed by jupyter notebook to execute code that is present in the cell. This tutorial was generated by running code examples in a jupyter notebook.
import random
try:
out = random.randint(-10, -20)
except Exception as e:
print(e)
tb = e.__traceback__
outer_frames = inspect.getouterframes(tb.tb_frame, context=8)
outer_frames[0]
for frame in outer_frames:
print("\nFilename : ", frame.filename)
print("Line No : ", frame.lineno)
print("Function : ", frame.function)
print("Index : ", frame.index)
print("Code Context : \n")
for line in frame.code_context:
print(line, end="")
This function takes as input traceback instance and returns a list of named tuple FrameInfo objects which has information about traceback's frame and all inner frames. This list has information about frames which has information about calls that were made when the exception happened.
We can see below from the formatted output that how many functions were called in order to retrieve random integers.
inner_frames = inspect.getinnerframes(tb, context=10)
inner_frames[0]
for frame in inner_frames:
print("\nFilename : ", frame.filename)
print("Line No : ", frame.lineno)
print("Function : ", frame.function)
print("Index : ", frame.index)
print("Code Context : \n")
for line in frame.code_context:
print(line, end="")
This method returns a list of named tuple FrameInfo objects which have information about the caller's stack.
We have below created methods that calls one another and the last method calls stack() function. It captures all calls that happened. Please check the output to see the stack trace of calls.
Please make a note that we are printing only 5 entries. The reason behind this is that we have run the code in jupyter notebook which calls lots of functions to execute this cell hence the stack has a lot of unnecessary frames that won't be present when code is run as a script. We have excluded those frames from printing output.
def caller_final():
return inspect.stack(context=2)
def caller3():
return caller_final()
def caller2():
return caller3()
def caller1():
return caller2()
def main_caller():
return caller1()
stack = main_caller()
stack[0]
for frame in stack[:5]:
print("\nFilename : ", frame.filename)
print("Line No : ", frame.lineno)
print("Function : ", frame.function)
print("Index : ", frame.index)
print("Code Context : \n")
for line in frame.code_context:
print(line, end="")
This function returns a list of named tuple FrameInfo objects which has information about the stack between the current frame and the frame in which the exception was raised.
Below we have called randint() function with negative arguments so that it fails and we can capture trace to explain the usage of the method. We can see that the actual exception happened in randrange where it got handled. We can see all the frames between calls from the below cell till that frame.
try:
out = random.randint(-10,-20)
except Exception as e:
print(e)
trc = inspect.trace(context=5)
trc[0]
for frame in trc:
print("\nFilename : ", frame.filename)
print("Line No : ", frame.lineno)
print("Function : ", frame.function)
print("Index : ", frame.index)
print("Code Context : \n")
for line in frame.code_context:
print(line, end="")
This function returns a frame object which has information about the caller's stack.
frame = inspect.currentframe()
frame
This ends our small tutorial explaining usage of the API of inspect module. Please feel free to let us know your views in the comments section.
If you are more comfortable learning through video tutorials then we would recommend that you subscribe to our YouTube channel.
When going through coding examples, it's quite common to have doubts and errors.
If you have doubts about some code examples or are stuck somewhere when trying our code, send us an email at coderzcolumn07@gmail.com. We'll help you or point you in the direction where you can find a solution to your problem.
You can even send us a mail if you are trying something new and need guidance regarding coding. We'll try to respond as soon as possible.
If you want to