Uses of Introspection
This lesson includes the following topics:
- The Meaning of 'Introspection'
- Attribute Handling Functions
- What Use is Introspection?
- The Inspect Module
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.
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):
>>> 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.... |
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:
>>> 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.
| Note | You 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:
>>> 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).
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).
| Note | A 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). |
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.
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).
| Attribute | Purpose | Module | Class | Method | Function | Built-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 name | Purpose |
|---|---|
| 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:
>>> 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.
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.
>>> 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.
