Updated On : Jan-25,2021 Tags inspect, analyze, python-objects
inspect - Analyze Python Objects

inspect - Analyze Python Objects

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.

  • Examine the signature of the function.
  • Find out the type of object.
  • Examine the contents of the class.
  • Examine the contents of the module.
  • Retrieve source code of method/function.
  • Format arguments of the function.
  • Get interpreter stack information.
  • etc.

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.

In [1]:
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.

ismodule()

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__

isclass()

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__

ismethod()

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__

isfunction()

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.

In [10]:
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()
Is random a Module : True
Is addition a Module : False

Is random a Class : False
Is Random a Class : True
Is ChainMap a Class : True

Is addition a Method : False
Is randint a Method : True

Is addition a Function : True
Is randint a Function : False

isgeneratorfunction()

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__

isgenerator()

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.

In [11]:
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)))
Is countGenerator a Generator Function : True
Is addition a Generator Function : False

Is <genexpr> a Generator : True
Is addition a Generator : False

iscoroutinefunction()

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.

iscode()

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.

In [12]:
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)))
Is addition_async a Coroutine Function : True
Is addition a Coroutine Function : False

Is addition.__code__ a Code : True
Is addition a Code : False

istraceback()

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

isframe()

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

isbuiltin()

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__

isroutine()

This method takes as input an object and returns True if it’s of type routine (function/method) else False.

isabstract()

This method takes as input an object and returns True if it’s of type abstract base class else False.

In [13]:
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)))
Is tb a Traceback : True
Is addition a Traceback : False

Is tb a Frame : False
Is tb.tb_frame a Frame : True

Is sum a Builtin : True
Is addition a Builtin : False

Is sum a Routine : True
Is addition a Routine : True

Is sum a Abstract Base Class : False
Is Hashable a Abstract Base Class : True

getmembers()

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.

In [14]:
import random

inspect.getmembers(random, inspect.isclass)
Out[14]:
[('Random', random.Random),
 ('SystemRandom', random.SystemRandom),
 ('_BuiltinMethodType', builtin_function_or_method),
 ('_MethodType', method),
 ('_Sequence', collections.abc.Sequence),
 ('_Set', collections.abc.Set)]
In [15]:
inspect.getmembers(random, inspect.ismodule)
Out[15]:
[('_bisect',
  <module 'bisect' from '/home/sunny/anaconda3/lib/python3.7/bisect.py'>),
 ('_itertools', <module 'itertools' (built-in)>),
 ('_os', <module 'os' from '/home/sunny/anaconda3/lib/python3.7/os.py'>),
 ('_random',
  <module '_random' from '/home/sunny/anaconda3/lib/python3.7/lib-dynload/_random.cpython-37m-x86_64-linux-gnu.so'>)]
In [16]:
inspect.getmembers(random, inspect.ismethod)
Out[16]:
[('betavariate',
  <bound method Random.betavariate of <random.Random object at 0x562178ea1c38>>),
 ('choice',
  <bound method Random.choice of <random.Random object at 0x562178ea1c38>>),
 ('choices',
  <bound method Random.choices of <random.Random object at 0x562178ea1c38>>),
 ('expovariate',
  <bound method Random.expovariate of <random.Random object at 0x562178ea1c38>>),
 ('gammavariate',
  <bound method Random.gammavariate of <random.Random object at 0x562178ea1c38>>),
 ('gauss',
  <bound method Random.gauss of <random.Random object at 0x562178ea1c38>>),
 ('getstate',
  <bound method Random.getstate of <random.Random object at 0x562178ea1c38>>),
 ('lognormvariate',
  <bound method Random.lognormvariate of <random.Random object at 0x562178ea1c38>>),
 ('normalvariate',
  <bound method Random.normalvariate of <random.Random object at 0x562178ea1c38>>),
 ('paretovariate',
  <bound method Random.paretovariate of <random.Random object at 0x562178ea1c38>>),
 ('randint',
  <bound method Random.randint of <random.Random object at 0x562178ea1c38>>),
 ('randrange',
  <bound method Random.randrange of <random.Random object at 0x562178ea1c38>>),
 ('sample',
  <bound method Random.sample of <random.Random object at 0x562178ea1c38>>),
 ('seed',
  <bound method Random.seed of <random.Random object at 0x562178ea1c38>>),
 ('setstate',
  <bound method Random.setstate of <random.Random object at 0x562178ea1c38>>),
 ('shuffle',
  <bound method Random.shuffle of <random.Random object at 0x562178ea1c38>>),
 ('triangular',
  <bound method Random.triangular of <random.Random object at 0x562178ea1c38>>),
 ('uniform',
  <bound method Random.uniform of <random.Random object at 0x562178ea1c38>>),
 ('vonmisesvariate',
  <bound method Random.vonmisesvariate of <random.Random object at 0x562178ea1c38>>),
 ('weibullvariate',
  <bound method Random.weibullvariate of <random.Random object at 0x562178ea1c38>>)]

getmodulename()

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.

In [17]:
inspect.getmodulename("/home/sunny/anaconda3/lib/python3.7/random.py")
Out[17]:
'random'
In [18]:
inspect.getmodulename("/home/sunny/anaconda3/lib/python3.7/collections.py")
Out[18]:
'collections'

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,

getdoc()

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.

In [19]:
import collections

collections_docs = inspect.getdoc(collections)

print(collections_docs)
This module implements specialized container datatypes providing
alternatives to Python's general purpose built-in containers, dict,
list, set, and tuple.

* namedtuple   factory function for creating tuple subclasses with named fields
* deque        list-like container with fast appends and pops on either end
* ChainMap     dict-like class for creating a single view of multiple mappings
* Counter      dict subclass for counting hashable objects
* OrderedDict  dict subclass that remembers the order entries were added
* defaultdict  dict subclass that calls a factory function to supply missing values
* UserDict     wrapper around dictionary objects for easier dict subclassing
* UserList     wrapper around list objects for easier list subclassing
* UserString   wrapper around string objects for easier string subclassing
In [20]:
addition_docs = inspect.getdoc(addition)

print(addition_docs)
Description:
    This function takes as input any number of integer arguments, sum them up and returns results.

Params:

*args: List of Values

getcomments()

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.

In [21]:
defaultdict_comments = inspect.getcomments(collections.defaultdict)

print(defaultdict_comments)
None
In [22]:
addition_comments = inspect.getcomments(addition)

print(addition_comments)
## This is a Generic Addition Function.
## It can take any number of integer arguments and sum them up.

getmodule()

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.

In [12]:
inspect.getmodule(random.randint)
Out[12]:
<module 'random' from '/home/sunny/anaconda3/lib/python3.7/random.py'>
In [13]:
inspect.getmodule(collections.Counter)
Out[13]:
<module 'collections' from '/home/sunny/anaconda3/lib/python3.7/collections/__init__.py'>

getfile()

This function takes an object as input and returns the file name in which that object was defined.

In [14]:
inspect.getfile(random)
Out[14]:
'/home/sunny/anaconda3/lib/python3.7/random.py'
In [15]:
inspect.getfile(collections.abc)
Out[15]:
'/home/sunny/anaconda3/lib/python3.7/collections/abc.py'
In [16]:
inspect.getfile(collections.deque)
Out[16]:
'/home/sunny/anaconda3/lib/python3.7/collections/__init__.py'

getsourcefile()

This function takes as input an object and returns a filename where the source code of the object is present.

In [17]:
inspect.getsourcefile(random)
Out[17]:
'/home/sunny/anaconda3/lib/python3.7/random.py'
In [18]:
inspect.getsourcefile(collections.abc)
Out[18]:
'/home/sunny/anaconda3/lib/python3.7/collections/abc.py'
In [19]:
inspect.getsourcefile(collections.ChainMap)
Out[19]:
'/home/sunny/anaconda3/lib/python3.7/collections/__init__.py'

getsourcelines()

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.

In [24]:
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="")
Return Type of inspect.getsourcelines() :  <class 'tuple'>
Line Number Where Object Starts :  1

async def addition_async(**args):
    return sum(args)
In [21]:
source_lines = inspect.getsourcelines(addition)

for line in source_lines[0]:
    print(line, end="")
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)
In [29]:
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="")
Line Number Where Object Starts :  218

Source Code :

    def randint(self, a, b):
        """Return random integer in range [a, b], including both end points.
        """

        return self.randrange(a, b+1)

getsource()

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.

In [22]:
source = inspect.getsource(addition)

print("Return Type of inspect.getsource() : ", type(source), "\n")
print(source)
Return Type of inspect.getsource() :  <class 'str'>

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)

In [23]:
source = inspect.getsource(random.randint)

print("Return Type of random.randint() : ", type(source), "\n")
print(source)
Return Type of random.randint() :  <class 'str'>

    def randint(self, a, b):
        """Return random integer in range [a, b], including both end points.
        """

        return self.randrange(a, b+1)

cleandoc()

This function takes as input docstrings and cleans them up by removing leading or trailing white spaces.

In [24]:
addition_docs = inspect.getdoc(addition)

print(inspect.cleandoc(addition_docs))
Description:
    This function takes as input any number of integer arguments, sum them up and returns results.

Params:

*args: List of Values

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.

getclasstree()

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.

In [42]:
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])))
[(<class 'object'>, ()),
 [(<class '__main__.A'>, (<class 'object'>,)),
  [(<class '__main__.B'>, (<class '__main__.A'>,)),
   [(<class '__main__.D'>, (<class '__main__.B'>,))],
   (<class '__main__.C'>, (<class '__main__.A'>,)),
   [(<class '__main__.E'>, (<class '__main__.C'>,))]]]]

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.

In [43]:
class F(D,E):
    pass

import pprint

print(pprint.pformat(inspect.getclasstree([A,B,C,D,E,F]), indent=2))
[ (<class 'object'>, ()),
  [ (<class '__main__.A'>, (<class 'object'>,)),
    [ (<class '__main__.B'>, (<class '__main__.A'>,)),
      [ (<class '__main__.D'>, (<class '__main__.B'>,)),
        [(<class '__main__.F'>, (<class '__main__.D'>, <class '__main__.E'>))]],
      (<class '__main__.C'>, (<class '__main__.A'>,)),
      [ (<class '__main__.E'>, (<class '__main__.C'>,)),
        [ ( <class '__main__.F'>,
            (<class '__main__.D'>, <class '__main__.E'>))]]]]]
In [44]:
import pprint

print(pprint.pformat(inspect.getclasstree([A,B,C,D,E,F], unique=True)))
[(<class 'object'>, ()),
 [(<class '__main__.A'>, (<class 'object'>,)),
  [(<class '__main__.B'>, (<class '__main__.A'>,)),
   [(<class '__main__.D'>, (<class '__main__.B'>,)),
    [(<class '__main__.F'>, (<class '__main__.D'>, <class '__main__.E'>))]],
   (<class '__main__.C'>, (<class '__main__.A'>,)),
   [(<class '__main__.E'>, (<class '__main__.C'>,))]]]]

getmro()

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.

In [28]:
inspect.getmro(E)
Out[28]:
(__main__.E, __main__.C, __main__.A, object)
In [29]:
inspect.getmro(F)
Out[29]:
(__main__.F,
 __main__.D,
 __main__.B,
 __main__.E,
 __main__.C,
 __main__.A,
 object)

getfullargspec()

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 - It has a list of positional parameter names.
  • varargs - It has *args parameters.
  • varkw - It has **kwargs parameters.
  • defaults - It has a list of default values for the last n positional parameters.
  • kwonlyargs - It has a list of keyword-only parameter names.
  • kwonlydefaults - It has default values for a list of keyword-only parameters.
  • annotations - It has annotation details as a dictionary. The annotations are information about type if provided.

Below we have tried to explain the usage of the method with simple examples.

In [46]:
inspect.getfullargspec(random.choices)
Out[46]:
FullArgSpec(args=['self', 'population', 'weights'], varargs=None, varkw=None, defaults=(None,), kwonlyargs=['cum_weights', 'k'], kwonlydefaults={'cum_weights': None, 'k': 1}, annotations={})
In [31]:
inspect.getfullargspec(random.randint)
Out[31]:
FullArgSpec(args=['self', 'a', 'b'], varargs=None, varkw=None, defaults=None, kwonlyargs=[], kwonlydefaults=None, annotations={})
In [32]:
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)
In [33]:
arg_spec = inspect.getfullargspec(Addition.addition)

arg_spec
Out[33]:
FullArgSpec(args=['self'], varargs='integers_lst', varkw=None, defaults=None, kwonlyargs=['power'], kwonlydefaults={'power': None}, annotations={'return': <class 'int'>, 'integers_lst': <class 'list'>, 'power': <class 'int'>})
In [34]:
print("Annotations : ", arg_spec.annotations)
print("Var Args    : ", arg_spec.varargs)
Annotations :  {'return': <class 'int'>, 'integers_lst': <class 'list'>, 'power': <class 'int'>}
Var Args    :  integers_lst

getcallargs()

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.

In [35]:
inspect.getcallargs(Addition.addition)
Out[35]:
{'self': __main__.Addition, 'integers_lst': (), 'power': None}
In [36]:
inspect.getcallargs(Addition.addition, 1,2,3,4)
Out[36]:
{'self': __main__.Addition, 'integers_lst': (1, 2, 3, 4), 'power': None}
In [37]:
inspect.getcallargs(Addition.addition, 1,2,3,4, power=2)
Out[37]:
{'self': __main__.Addition, 'integers_lst': (1, 2, 3, 4), 'power': 2}

getattr_static()

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().

In [56]:
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"))
Inside __getattribute__
emp_id is present in e1 :  True
Inside __getattribute__
emp_id is present in e1 :  124

emp_id is present in e1 :  124

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.

signature()

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.

In [57]:
sum_signature = inspect.signature(sum)

sum_signature
Out[57]:
<Signature (iterable, start=0, /)>
In [79]:
def addition(a:int=0, b:int=0) -> int:
    return a + b


addition_signature = inspect.signature(addition)

addition_signature
Out[79]:
<Signature (a: int = 0, b: int = 0) -> int>
In [80]:
choices_signature = inspect.signature(random.choices)

choices_signature
Out[80]:
<Signature (population, weights=None, *, cum_weights=None, k=1)>
In [81]:
print("Sum            Signature : ", sum_signature)
print("Addition       Signature : ", addition_signature)
print("Random.Choices Signature : ", choices_signature)
Sum            Signature :  (iterable, start=0, /)
Addition       Signature :  (a: int = 0, b: int = 0) -> int
Random.Choices Signature :  (population, weights=None, *, cum_weights=None, k=1)

Signature

Signature.from_callable()

The Signature object has a method named from_callable() which also lets us retrieve the signature as well.

In [82]:
#inspect.Signature.from_function(random.choices) Deprectated
inspect.Signature.from_callable(random.choices)
Out[82]:
<Signature (population, weights=None, *, cum_weights=None, k=1)>

Signature.return_annotation

The Signature object has return_annotation attribute which returns annotation of the return type of the function if present.

In [83]:
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)
Sum            Return Annotation :  <class 'inspect._empty'>
Addition       Return Annotation :  <class 'int'>
Random.Choices Return Annotation :  <class 'inspect._empty'>

Signature.replace()

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.

In [84]:
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)
Sum            Signature         :  (iterable, start=0, /) -> float
Addition       Signature         :  (a: int = 0, b: int = 0) -> str
Sum            Return Annotation :  <class 'float'>
Addition       Return Annotation :  <class 'str'>

Signature.parameters

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.

In [85]:
print("Sum            Parameters : ", sum_signature.parameters)
print("Addition       Parameters : ", addition_signature.parameters)
print("Random.Choices Parameters : ", choices_signature.parameters)
Sum            Parameters :  OrderedDict([('iterable', <Parameter "iterable">), ('start', <Parameter "start=0">)])
Addition       Parameters :  OrderedDict([('a', <Parameter "a: int = 0">), ('b', <Parameter "b: int = 0">)])
Random.Choices Parameters :  OrderedDict([('population', <Parameter "population">), ('weights', <Parameter "weights=None">), ('cum_weights', <Parameter "cum_weights=None">), ('k', <Parameter "k=1">)])

Signature.bind()

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.

In [86]:
bounded_args = addition_signature.bind(a=100)

bounded_args
Out[86]:
<BoundArguments (a=100)>

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.

In [87]:
bounded_args.apply_defaults()

bounded_args
Out[87]:
<BoundArguments (a=100, b=0)>
In [88]:
bounded_args.signature
Out[88]:
<Signature (a: int = 0, b: int = 0) -> int>

We can even call the function once we have bind values to the parameters in BoundedArguments instance.

In [89]:
addition(*bounded_args.args, **bounded_args.kwargs)
Out[89]:
100

Signature.bind_partial()

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.

In [90]:
bounded_args = addition_signature.bind_partial(25)

bounded_args
Out[90]:
<BoundArguments (a=25)>
In [91]:
bounded_args.apply_defaults()

bounded_args
Out[91]:
<BoundArguments (a=25, b=0)>
In [92]:
addition(*bounded_args.args, **bounded_args.kwargs)
Out[92]:
25

Parameter

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.

  • name - The name of parameter.
  • default - The default value of the parameter.
  • annotation - The type information about parameter.
  • kind - The type of parameter. It has one of the below values.
    • POSITIONAL_ONLY
    • POSITIONAL_OR_KEYWORD
    • VAR_POSITIONAL
    • KEYWORD_ONLY
    • VAR_KEYWORD

Parameter object is immutable hence we can not modify it.

In [117]:
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))
Name : a, Default Value : 0, Annotation : <class 'int'>, Type : POSITIONAL_OR_KEYWORD
Name : b, Default Value : 0, Annotation : <class 'int'>, Type : POSITIONAL_OR_KEYWORD
In [123]:
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))
Name : population, Default Value : <class 'inspect._empty'>, Annotation : <class 'inspect._empty'>, Type : POSITIONAL_OR_KEYWORD

Name : weights, Default Value : None, Annotation : <class 'inspect._empty'>, Type : POSITIONAL_OR_KEYWORD

Name : cum_weights, Default Value : None, Annotation : <class 'inspect._empty'>, Type : KEYWORD_ONLY

Name : k, Default Value : 1, Annotation : <class 'inspect._empty'>, Type : KEYWORD_ONLY

Parameter.replace()

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.

In [162]:
a_param = addition_signature.parameters["a"]
b_param = addition_signature.parameters["b"]

a_param, b_param
Out[162]:
(<Parameter "a: int = 0">, <Parameter "b: int = 0">)
In [163]:
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
Out[163]:
(<Parameter "param1: float = 0.0">, <Parameter "param2: float = 0.0">)
In [165]:
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)
Addition       Signature         :  (a: int = 0, b: int = 0) -> int
Addition       Signature         :  (param1: float = 0.0, param2: float = 0.0) -> float

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.

In [120]:
def division(a, b):
    return a/b

try:
    out = division(10, 0)
except Exception as e:
    print("Exception : ", e)
    tb = e.__traceback__
Exception :  division by zero

getframeinfo()

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.

In [129]:
traceback_obj = inspect.getframeinfo(tb.tb_frame, context=1)

traceback_obj
Out[129]:
Traceback(filename='<ipython-input-120-76dd2647cde1>', lineno=8, function='<module>', code_context=['    tb = e.__traceback__\n'], index=0)
In [130]:
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="")
Filename     :  <ipython-input-120-76dd2647cde1>
Line No      :  8
Function     :  <module>
Index        :  0
Code Context :

    tb = e.__traceback__

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.

In [139]:
traceback_obj = inspect.getframeinfo(tb.tb_frame, context=8)

traceback_obj
Out[139]:
Traceback(filename='<ipython-input-120-76dd2647cde1>', lineno=8, function='<module>', code_context=['def division(a, b):\n', '    return a/b\n', '\n', 'try:\n', '    out = division(10, 0)\n', 'except Exception as e:\n', '    print("Exception : ", e)\n', '    tb = e.__traceback__\n'], index=7)
In [140]:
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="")
Filename     :  <ipython-input-120-76dd2647cde1>
Line No      :  8
Function     :  <module>
Index        :  7
Code Context :

def division(a, b):
    return a/b

try:
    out = division(10, 0)
except Exception as e:
    print("Exception : ", e)
    tb = e.__traceback__

getouterframes()

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.

In [164]:
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]
empty range for randrange() (-10,-19, -9)
Out[164]:
FrameInfo(frame=<frame at 0x7f9edc07cc18, file '<ipython-input-164-907f404dbc4a>', line 7, code <module>>, filename='<ipython-input-164-907f404dbc4a>', lineno=7, function='<module>', code_context=['try:\n', '    out = random.randint(-10, -20)\n', 'except Exception as e:\n', '    print(e)\n', '    tb = e.__traceback__\n', '\n', 'outer_frames = inspect.getouterframes(tb.tb_frame, context=8)\n', '\n'], index=4)
In [165]:
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="")
Filename   :  <ipython-input-164-907f404dbc4a>
Line No      :  7
Function     :  <module>
Index        :  4
Code Context :

try:
    out = random.randint(-10, -20)
except Exception as e:
    print(e)
    tb = e.__traceback__

outer_frames = inspect.getouterframes(tb.tb_frame, context=8)


Filename   :  /home/sunny/anaconda3/lib/python3.7/site-packages/IPython/core/interactiveshell.py
Line No      :  3438
Function     :  run_code
Index        :  4
Code Context :

                result.error_in_exec = sys.exc_info()[1]
            self.showtraceback(running_compiled_code=True)
        else:
            outflag = False
        return outflag

    # For backwards compatibility
    runcode = run_code

getinnerframes()

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.

In [168]:
inner_frames = inspect.getinnerframes(tb, context=10)

inner_frames[0]
Out[168]:
FrameInfo(frame=<frame at 0x7f9edc07cc18, file '<ipython-input-164-907f404dbc4a>', line 7, code <module>>, filename='<ipython-input-164-907f404dbc4a>', lineno=4, function='<module>', code_context=['import random\n', '\n', 'try:\n', '    out = random.randint(-10, -20)\n', 'except Exception as e:\n', '    print(e)\n', '    tb = e.__traceback__\n', '\n', 'outer_frames = inspect.getouterframes(tb.tb_frame, context=8)\n', '\n'], index=3)
In [169]:
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="")
Filename   :  <ipython-input-164-907f404dbc4a>
Line No      :  4
Function     :  <module>
Index        :  3
Code Context :

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)


Filename   :  /home/sunny/anaconda3/lib/python3.7/random.py
Line No      :  222
Function     :  randint
Index        :  5
Code Context :


    def randint(self, a, b):
        """Return random integer in range [a, b], including both end points.
        """

        return self.randrange(a, b+1)

    def _randbelow(self, n, int=int, maxsize=1<<BPF, type=type,
                   Method=_MethodType, BuiltinMethod=_BuiltinMethodType):
        "Return a random int in the range [0,n).  Raises ValueError if n==0."

Filename   :  /home/sunny/anaconda3/lib/python3.7/random.py
Line No      :  200
Function     :  randrange
Index        :  5
Code Context :

            raise ValueError("non-integer stop for randrange()")
        width = istop - istart
        if step == 1 and width > 0:
            return istart + self._randbelow(width)
        if step == 1:
            raise ValueError("empty range for randrange() (%d,%d, %d)" % (istart, istop, width))

        # Non-unit step argument supplied.
        istep = _int(step)
        if istep != step:

stack()

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.

In [209]:
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]
Out[209]:
FrameInfo(frame=<frame at 0x7f9ec799e6c8, file '<ipython-input-209-03e05403cc4e>', line 2, code caller_final>, filename='<ipython-input-209-03e05403cc4e>', lineno=2, function='caller_final', code_context=['def caller_final():\n', '    return inspect.stack(context=2)\n'], index=1)
In [210]:
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="")
Filename   :  <ipython-input-209-03e05403cc4e>
Line No      :  2
Function     :  caller_final
Index        :  1
Code Context :

def caller_final():
    return inspect.stack(context=2)

Filename   :  <ipython-input-209-03e05403cc4e>
Line No      :  5
Function     :  caller3
Index        :  1
Code Context :

def caller3():
    return caller_final()

Filename   :  <ipython-input-209-03e05403cc4e>
Line No      :  8
Function     :  caller2
Index        :  1
Code Context :

def caller2():
    return caller3()

Filename   :  <ipython-input-209-03e05403cc4e>
Line No      :  11
Function     :  caller1
Index        :  1
Code Context :

def caller1():
    return caller2()

Filename   :  <ipython-input-209-03e05403cc4e>
Line No      :  14
Function     :  main_caller
Index        :  1
Code Context :

def main_caller():
    return caller1()

trace()

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.

In [202]:
try:
    out = random.randint(-10,-20)
except Exception as e:
    print(e)
    trc = inspect.trace(context=5)

trc[0]
empty range for randrange() (-10,-19, -9)
Out[202]:
FrameInfo(frame=<frame at 0x55b543f00218, file '<ipython-input-202-0a2db501ef85>', line 5, code <module>>, filename='<ipython-input-202-0a2db501ef85>', lineno=2, function='<module>', code_context=['try:\n', '    out = random.randint(-10,-20)\n', 'except Exception as e:\n', '    print(e)\n', '    trc = inspect.trace(context=5)\n'], index=1)
In [203]:
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="")
Filename   :  <ipython-input-202-0a2db501ef85>
Line No      :  2
Function     :  <module>
Index        :  1
Code Context :

try:
    out = random.randint(-10,-20)
except Exception as e:
    print(e)
    trc = inspect.trace(context=5)

Filename   :  /home/sunny/anaconda3/lib/python3.7/random.py
Line No      :  222
Function     :  randint
Index        :  2
Code Context :

        """

        return self.randrange(a, b+1)

    def _randbelow(self, n, int=int, maxsize=1<<BPF, type=type,

Filename   :  /home/sunny/anaconda3/lib/python3.7/random.py
Line No      :  200
Function     :  randrange
Index        :  2
Code Context :

            return istart + self._randbelow(width)
        if step == 1:
            raise ValueError("empty range for randrange() (%d,%d, %d)" % (istart, istop, width))

        # Non-unit step argument supplied.

currentframe()

This function returns a frame object which has information about the caller's stack.

In [204]:
frame = inspect.currentframe()

frame
Out[204]:
<frame at 0x55b543eb2778, file '<ipython-input-204-7c2471155cf7>', line 1, code <module>>

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.

References



Sunny Solanki  Sunny Solanki