login
Holden Web
What you'll need to know tomorrow

Uses of Introspection

This lesson includes the following topics:

The Meaning of 'Introspection'

The word "introspection" means "looking inside." Introspective people are ones who think about themselves, usually to increase self-understanding. In Python, introspection is a way that your programs can learn about the environment in which they operate and the properties of the modules they import.

You have already learned about several of Python's introspection mechanisms. The built-in dir() function, for example, attempts to return (to quote from the documentation) "an interesting set of names"—meaning the names of attributes accessible from the object passed as an argument. If no argument is passed, it returns the attributes found in the current local namespace.

dir() in Python 3.x has a hook that looks for a __dir__() method on its argument. If such a method is present, it is called and dir() returns what the method returns. This allows you to determine what users see about your object, and this can be useful if you are using "virtual" attributes (that is, if your objects handle access to methods that do not appear in the class's __dict__). If no __dir__() method is found, dir() uses a standard mechanism to compose its result after examining its argument.

Some Simple Introspection Examples

x.__class__.__name__ will tell you the name of an object's class (and is much more reliable than trying to analyze a repr() string):

The Right and Wrong Way to Extract a Class Name
>>> class Something:
...     pass
...
>>> s = Something()
>>> s
<__main__.Something object at 0x...>
>>> repr(s)[1:-1].split()[0].split(".")[1] # WRONG!
'Something'
>>> s.__class__.__name__ # RIGHT (AND SO MUCH EASIER)
'Something'
>>> repr(4)[1:-1].split()[0].split(".")[1] # Fail
Traceback (most recent call last):
  File "<console>", line 1, in <module>
IndexError: list index out of range
>>> 4.__class__.__name__
  File "<string>", line 1
SyntaxError: invalid decimal literal
>>> (4).__class__.__name__ # SUCCEED
'int'
>>> str(type(4))[1:-1].split()[1][1:-1] # Way too complex
'int'
>>> str(type(s))[1:-1].split()[1][1:-1] # And only give same result for built-ins (see s above)
'__main__.Something'

The failed attempt to extract the class name from the integer 4's repr() string shows just how fragile the "wrong" method is: it applies only to objects with a very specific representation. When handed an int instance it explodes, raising an exception. The syntax error occurred because the interpreter took the period (".") to be part of a number, and then could not understand why it was followed by an identifier. Putting the (4) in parentheses allows the lexical analysis routines to parse things correctly, and we see that the class name is available from built-in classes just as it is on self-declared ones. If you find yourself writing code like the first and last examples, you should question whether there isn't a better way: Python is designed to avoid the need for such contortions.

some_object.__doc__ can be useful, but if things are properly written, you'll get better presentation from help(some_object), which is designed to print necessary documentation in a legible way.

Modern Python In Python 3.14 the SyntaxError message for 4.__class__.__name__ reads invalid decimal literal rather than the original invalid syntax. The reason is the same: the lexer takes the . as part of a numeric literal. The fix—wrapping the integer in parentheses as (4).__class__.__name__—is unchanged. Object repr addresses vary with every run; they are shown here as 0x....
Attribute Handling Functions

If you took earlier courses in this Certificate Series (or otherwise possibly from private study) you've encountered the getattr(obj), setattr(obj), and delattr(obj) functions, and learned that they result in a call to their argument obj's __getattr__(), __setattr__(), and __delattr__() methods. There is also the hasattr() predicate, which can be used to determine whether or not a given attribute is present in an object. There is, however, no corresponding __hasattr__() method. You might wonder what hasattr() does to find out what value to return, and the answer to that question is complex enough to have received the attention of some of the best minds in Python.

Without going too deeply into the internals, it is fairly easy for you to determine whether or not __getattr__() gets called by hasattr() under at least some circumstances. You simply write a class whose instances report calls of their __getattr__() method, and then call hasattr() on an instance:

INTERACTIVE SESSION:
>>> class X:
...     def __getattr__(self, name):
...         print("getattr", name)
...         return 0
...
>>> x = X()
>>> hasattr(x, "thing")
getattr thing
True
>>>

hasattr(obj, "__call__") can be used to tell you whether or not obj can be called like a function. Older versions of Python provide a callable() built-in function, which should have been removed in Python 3.0 because the given test is now all that is required— everything callable has a __call__ attribute. Its deletion was omitted in error for the 3.0 release, with the result that callable is available for that release. It was then removed from 3.1 (the version in use when this course was being written), but has returned in 3.2 because the above test turns out not to be quite as specific as the version that can be written in C with full access to the object structures. Being able to determine the presence or absence of a particular attribute is occasionally useful in other contexts.

NoteYou should avoid writing code where "too much" (a judgment call) of the logic depends on the presence or absence of specific attributes, unless you are writing deliberately introspective code as part of a framework or library.

Of course you can implement whole "virtual namespaces" within your own objects by using getattr() and setattr(), but remember that these functions can also be used (assuming you can gain access to the required namespaces) to modify your current environment. Understand that doing so in this way is not recommended except in rather extreme cases, because it results in "magical" changes—changes whose origin is difficult or impossible to discern by reading the program code:

'Magical' changes to the module's namespace
>>> import sys
>>> __name__
'__main__'
>>> module = sys.modules[__name__]
>>> a
Traceback (most recent call last):
  File "<console>", line 1, in <module>
NameError: name 'a' is not defined
>>> setattr(module, "a", 42)
>>> a
42
>>>

Before the setattr() call, there was no "a" defined in the module's namespace. Since all imported modules are available under their natural names from sys.modules, you can access the current module's namespace by looking it up.

If it were possible to subclass the module object to change its attribute access methods, we could be faced with some extremely hard-to-understand code! Fortunately this is not something you need to worry about in practice. Most of the code you will encounter does not use such tricks (indeed, the Django framework mentioned earlier had a period in its development devoted to "magic removal" to make the code easier for Python programmers and beginners to understand, and provide a framework that was less brittle).

What Use is Introspection?

Frameworks use introspection frequently, to discover the capabilities of objects the user has passed; for example, "does this object's class have a something() method? If so, call the object's do_something(); otherwise call the do_something_similar() framework function with the object as an argument." Some built-in functions also do this kind of introspection. The dir() built-in mentioned above returns the result of the argument object's __dir__() method if it has one; otherwise it uses built-in functionality to provide an "interesting" set of names (the result is not defined more clearly than that anywhere in the code).

NoteA framework is an environment that provides a wealth of facilities to programmers. You can think of it as being like an "operating system for a particular type of programming task." The users of frameworks are generally application programmers, using the framework (for example, Django or Tkinter) to build a particular type of application (in Django's case, they would be web applications; in Tkinter's case, they would be windowed applications).
The Inspect module

This module allows you to dig as deep as you ever need to in terms of introspection. It provides many functions by which you can determine the properties of objects, including sixteen predicates that allow you to easily determine whether an object is of a particular type.

The getmembers() Function

inspect.getmembers(obj[, predicate]) returns a list of two-element (name, value) tuples. If you provide a second argument, it is called with the value as its only argument and the item only appears in the resulting list if the result is True. This makes the predicates mentioned in the last paragraph very useful if you are only interested in objects of a particular type. Following are some special attributes especially worth knowing about (columns to the right explain which attributes you can expect to see on five given types of object).

AttributePurposeModuleClassMethodFunctionBuilt-in
__doc__Documentation string
__file__Path to the file from which the object was loaded    
__module__Name of module in which the object was imported   
__name__Name of object  
__func__The implementation of the method    
__self__Instance to which this method is bound (or None)   
__code__Code object containing function's bytecode    
__defaults__Documentation string 
__globals__Documentation string 

The predicates that you can use with getmembers() are:

Predicate namePurpose
ismodule(x)Returns True if x is a module.
isclass(x)Returns True if x is a class, whether built-in or user-defined.
ismethod(x)Returns True if x is a bound method written in Python.
isfunction(x)Returns True if x is a function (including functions created by lambda expressions).
isgeneratorfunction(x)Returns True if x is a Python generator function.
isgenerator(x)Returns True if x is a generator.
istraceback(x)Returns True if x is a traceback object (created when an exception is handled).
isframe(x)Returns True if x is a stack frame (can be used to debug code interactively).
iscode(x)Returns True if x is a code object.
isbuiltin(x)Returns True if x is a built-in function or a bound built-in method.
isroutine(x)Returns True if x is a user-defined or built-in function or method.
isabstract(x)Returns True if x is an abstract base class (one meant to be inherited from rather than instantiated).
ismethoddescriptor(x)Returns True if x is a method descriptor unless ismethod(x), isclass(x), isfunction(x) or isbuiltin(x) is True.
isdatadescriptor(x)Returns True if x is a data descriptor (has both a __get__() and a __set__() method).
isgetsetdescriptor(x)Returns True if x is a getsetdescriptor—these are used in extension modules.
ismemberdescriptor(x)Returns True if x is a member descriptor—these are used in extension modules.

The second argument to inspect.getmembers() allows you to access members of a particular type easily:

Experimenting with getmembers()
>>> import inspect
>>> from smtplib import SMTP
>>> from pprint import pprint
>>> pprint(inspect.getmembers(SMTP))
[('__class__', <class 'type'>),
 ('__delattr__', <slot wrapper '__delattr__' of 'object' objects>),
 ('__dict__', mappingproxy({...})),
 ('__doc__',
  "This class manages a connection to an SMTP or ESMTP server.\n
    SMTP Objects:\n
        SMTP objects have the following attributes:\n
            helo_resp\n
                This is the message given by the server in response to the\n
                most recent HELO command.\n\n
            ehlo_resp\n
                This is the message given by the server in response to the\n
                most recent EHLO command. This is usually multiline.\n\n
            does_esmtp\n
                This is a True value _after you do an EHLO command_, if the\n
                server supports ESMTP.\n\n
            esmtp_features\n
                This is a dictionary, which, if the server supports ESMTP,\n
                will _after you do an EHLO command_, contain the names of the\n
                SMTP service extensions this server supports, and their\n
                parameters (if any).\n\n
                Note, all extension names are mapped to lower case in the\n
                dictionary.\n\n
        See each method's docstrings for details.  In general, there is a\n
        method of the same name to perform each SMTP command.  There is also a\n
        method called 'sendmail' that will do an entire mail transaction.\n        "),
 ('__eq__', <slot wrapper '__eq__' of 'object' objects>),
    ...
 ('__weakref__', <attribute '__weakref__' of 'SMTP' objects>),
 ('_get_socket', <function SMTP._get_socket at 0x...>),
 ('close', <function SMTP.close at 0x...>),
    ...
 ('verify', <function SMTP.verify at 0x...>),
 ('vrfy', <function SMTP.verify at 0x...>)]
>>>
>>> pprint(inspect.getmembers(SMTP, inspect.ismethod))
[]
>>> pprint(inspect.getmembers(SMTP, inspect.isfunction))
[('__enter__', <function SMTP.__enter__ at 0x...>),
 ('__exit__', <function SMTP.__exit__ at 0x...>),
 ('__init__', <function SMTP.__init__ at 0x...>),
    ...
 ('verify', <function SMTP.verify at 0x...>),
 ('vrfy', <function SMTP.verify at 0x...>)]
>>> smtp = SMTP()
>>> pprint(inspect.getmembers(smtp, inspect.ismethod))
[('__enter__',
  <bound method SMTP.__enter__ of <smtplib.SMTP object at 0x...>>),
 ('__exit__',
  <bound method SMTP.__exit__ of <smtplib.SMTP object at 0x...>>),
 ('__init__',
  <bound method SMTP.__init__ of <smtplib.SMTP object at 0x...>>),
    ...
 ('verify',
  <bound method SMTP.verify of <smtplib.SMTP object at 0x...>>),
 ('vrfy', <bound method SMTP.verify of <smtplib.SMTP object at 0x...>>)]
>>>
Modern Python This output has been regenerated on Python 3.14 and differs from the original in two visible ways. First, __dict__ is now shown as a mappingproxy rather than the old dict_proxy type name. Second, inspect.getmembers(SMTP, inspect.isfunction) returns more entries than the original showed—Python 3 exposes __enter__, __exit__, _print_debug, set_debuglevel, and others alongside __init__. The key behaviours— ismethod returning an empty list for the class, and a full list of bound methods for an instance—are unchanged. All memory addresses vary with every run and are shown as 0x....

You will get rather more output than we showed here, and the docstring has been reformatted to make it easier to read in the listing, but there is no reason to list everything that is output. The detail presented is sufficient to demonstrate that the SMTP class has many member attributes, including the standard "dunder" names, many of them inherited from the object type.

Asking for the methods of the class (using the ismethod() predicate as a second argument to getmembers()) changes it to return the empty list. This is not too surprising, as the predicate is documented as returning True only for bound methods—methods associated with a particular instance. The isfunction() predicate used in the third example returns the methods that are specifically defined on the class, but not those inherited from superclasses (which in practice means the object type). Creating an instance of the SMTP class and querying that for methods gives a much more interesting result.

Introspecting Functions

There are various attributes of a code object that can be used to discover information about the function to which it belongs. The inspect module provides some convenience functions to avoid the need to use them under most circumstances, however.

inspect.getfullargspec(f) returns a named tuple FullArgSpec(args, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations) containing information pertaining to the function argument f:

  • args is a list of the names of the standard (positional and keyword) arguments.
  • The defaults member contains the default values for the arguments specified by keyword (which always follow the positionals).
  • varargs and varkw are the names of the * and ** arguments, if present. The value None is used when there are no such arguments.
  • kwonlyargs is a list of the arguments that must be provided as keyword arguments
  • kwonlydefaults is the list of default values of those arguments.
  • annotations is a dict that maps argument names to annotations (which will usually be empty, because we will not cover function annotations in this course)

inspect.formatargspec(args[, varargs, varkw, defaults, kwonlyargs, kwonlydefaults, annotations]) takes the output from getfullargspec() and re-creates the arguments part of the function signature.

Here is a little example to show you how they work.

Function introspection
>>> import inspect
>>> def f(a, b, c=1, d="one", *args, **kw):
...     print('a', a, 'b', b, 'c', c, 'd', d, 'args', args, 'kw', kw)
...
>>> inspect.getfullargspec(f)
FullArgSpec(args=['a', 'b', 'c', 'd'], varargs='args', varkw='kw', defaults=(1, 'one'), kwonlyargs=[], kwonlydefaults=None, annotations={})
>>> inspect.formatargspec(*inspect.getfullargspec(f))
"(a, b, c=1, d='one', *args, **kw)"
>>>
Modern Python inspect.formatargspec() was removed in Python 3.11. Use str(inspect.signature(f)) instead, which produces identical output and works in all current Python versions:
>>> str(inspect.signature(f))
"(a, b, c=1, d='one', *args, **kw)"
The higher-level inspect.signature() (available since Python 3.3) is now the preferred API for callable introspection—it handles decorated functions, __wrapped__ chains, and __signature__ overrides that getfullargspec() does not.

As you can see, formatargspec() produces a parenthesized list of argument specifications that can easily be translated back into the original format (or something equivalent to it) using the formatargspec() function.

There are other facilities that come as part of the inspect module, and you can read the documentation for that module when you feel the need to learn more. Using the features you have learned about in this lesson, however, you should be able to discover what your program needs to know about the code that surrounds it.