70.1 Introspection: dir(), vars(), hasattr(), getattr(), inspect Module
Right, let’s get our hands dirty. You’re about to learn how Python lets Python look at Python. It’s a bit meta, like a snake eating its own tail, but far more useful and less… messy. This isn’t just academic navel-gazing; introspection is how you write flexible, powerful code that can adapt to its environment, inspect libraries you didn’t write, and build frameworks that feel like magic.
We’ll start with the blunt instruments and work our way up to the surgical tools.
The Basics: dir(), vars(), and the attr Gang
First up, dir(). This is your first stop for any object investigation. It returns a list of names—a rough, unsorted inventory of what’s accessible on an object. I say “rough” because it includes methods, attributes, and other things you might not care about. Think of it as the “what’s in the box?” function.
class SuspiciouslyEmptyBox:
def __init__(self):
self.public_secret = "I'm here!"
self._private_secret = "I'm *really* here!"
def a_method(self):
pass
def __a_magic_method__(self):
pass
box = SuspiciouslyEmptyBox()
# Let's see what's in the box
print(dir(box))
# Output will be a long list including: ['__a_magic_method__', '_private_secret', 'a_method', 'public_secret', ...]
See? It shows everything, from our public attributes to our “private” ones (hint: in Python, _single_leading_underscore is just a polite suggestion, not an access modifier) and all the double-underscore “magic” methods. It’s great for a quick glance, but it’s just a list of names. It tells you what is there, not what it is.
To actually get the value of one of those things, you use getattr(). Its partner in crime, hasattr(), is how you check for existence without triggering an AttributeError and crashing your script like a chump.
# The safe way
if hasattr(box, 'public_secret'):
value = getattr(box, 'public_secret')
print(f"The secret is: {value}") # The secret is: I'm here!
# The dangerous, direct way (which you'd normally use)
print(box.public_secret)
# The powerful way: you can even provide a default for missing attributes
missing_value = getattr(box, 'non_existent_thing', 'Nothing to see here')
print(missing_value) # Nothing to see here
Now, vars() is like dir()’s more responsible sibling. It returns the __dict__ attribute of a module, class, instance, or any other object with a __dict__. This is usually the actual namespace of attributes you’ve set, not the kitchen sink of methods.
print(vars(box))
# Output: {'public_secret': "I'm here!", '_private_secret': "I'm *really* here!"}
Much cleaner. It shows us the data, not the machinery. For a class, vars(MyClass) would show the class namespace, including methods.
The inspect Module: Your Surgical Toolkit
The built-ins are fine for poking around, but when you need to do real diagnosis, you bring out the inspect module. This is where Python’s introspection goes from a blunt club to a scalpel. It’s the difference between knowing a function exists and knowing what its arguments are called.
Need to know if something is a function, a method, a module, or a weird old class? inspect.isfunction(), inspect.ismethod(), etc., have you covered.
But the real party trick is getting the signature of a callable. This is absurdly powerful for building APIs, decorators, or debuggers.
import inspect
def confusing_function(a, b=2, *args, c=10, **kwargs):
pass
# Let's make sense of that mess
sig = inspect.signature(confusing_function)
print(sig) # (a, b=2, *args, c=10, **kwargs)
for param_name, param_obj in sig.parameters.items():
print(f"{param_name}: {param_obj.default} (kind: {param_obj.kind})")
# Output:
# a: <class 'inspect._empty'> (kind: POSITIONAL_OR_KEYWORD)
# b: 2 (kind: POSITIONAL_OR_KEYWORD)
# args: <class 'inspect._empty'> (kind: VAR_POSITIONAL)
# c: 10 (kind: KEYWORD_ONLY)
# kwargs: <class 'inspect._empty'> (kind: VAR_KEYWORD)
Suddenly, you can programmatically understand how to call any function. Your code can adapt to the function’s interface instead of the other way around. This is how sophisticated frameworks like FastAPI or pytest work their magic.
You can also get the source code of live objects with inspect.getsource(), find the module an object came from, and walk the stack—which is incredibly useful for advanced logging or debugging.
def who_called_me():
"""A function that tattles on its caller."""
# Get the current call stack frames
stack = inspect.stack()
# The first frame [0] is this function, the second [1] is the caller
caller_frame = stack[1]
print(f"I was called by: {caller_frame.function} in {caller_frame.filename} at line {caller_frame.lineno}")
def some_function():
who_called_me()
some_function()
# Output: I was called by: some_function in your_script.py at line X
Common Pitfalls and The Zen of Introspection
Abstraction Leaks: The most important rule: just because you can see it doesn’t mean you should use it. Anything with a leading underscore
_is considered “non-public.” The maintainer of that code feels free to change, rename, or remove it in any future version. If you use it, you own the broken pieces when it eventually changes. This is the fastest way to write fragile, unmaintainable code.Performance: Introspection is not free. Using
getattr()in a tight loop is slower than direct attribute access. Usinginspect.stack()is very expensive. It’s a powerful tool, not something to sprinkle everywhere.Readability: Code that dynamically figures everything out at runtime can be a nightmare to debug. It’s often clearer to be explicit. Use these tools when the flexibility is the point (e.g., writing a framework), not just to save a few lines of code.
The inspect module is the designer’s well-considered choice. It provides a stable, official API for all this introspection magic. The __dunder__ attributes you might be tempted to access directly (like obj.__class__ or func.__code__) are implementation details. inspect wraps them in a clean, consistent interface that’s far more likely to work across Python versions. Use it. Your future self, who isn’t debugging a breakage in Python 3.12, will thank you.