Of course! "Introspection" in Python refers to the ability of a program to examine the type or properties of an object at runtime. This is a powerful feature that allows you to write more dynamic, flexible, and "Pythonic" code.

Let's break down how Python introspection works, from the built-in functions to more advanced techniques.
The Built-in Functions: The Core of Introspection
These are the most common and direct tools you'll use. They are built into the language and work on virtually any object.
type() and isinstance(): Checking an Object's Type
These are your primary tools for figuring out what kind of object you're dealing with.
type(obj): Returns the exact type of the object.isinstance(obj, classinfo): ReturnsTrueif the object is an instance of the class or a subclass thereof. This is generally preferred overtype()because it respects inheritance.
class Animal:
pass
class Dog(Animal):
pass
my_dog = Dog()
# Using type()
print(f"type(my_dog): {type(my_dog)}")
# Output: type(my_dog): <class '__main__.Dog'>
# Using isinstance()
print(f"isinstance(my_dog, Dog): {isinstance(my_dog, Dog)}")
# Output: isinstance(my_dog, Dog): True
print(f"isinstance(my_dog, Animal): {isinstance(my_dog, Animal)}")
# Output: isinstance(my_dog, Animal): True # This is why isinstance is better!
print(f"isinstance(my_dog, str): {isinstance(my_dog, str)}")
# Output: isinstance(my_dog, str): False
dir(): Listing Attributes and Methods
dir() is your "directory" function. It returns a list of strings containing the names of an object's attributes and methods.

my_list = [1, 2, 3] # Get all attributes and methods of the list object print(dir(my_list)) # Output (partial list): # ['__add__', '__class__', '__contains__', ..., 'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert', 'pop', 'remove', 'reverse', 'sort']
You can use this to programmatically check if an object has a certain capability.
if 'append' in dir(my_list):
print("This object can append items.")
my_list.append(4)
print(my_list)
# Output:
# This object can append items.
# [1, 2, 3, 4]
hasattr(), getattr(), setattr(), delattr(): Interacting with Attributes
These four functions work together to get, set, and check for attributes dynamically.
hasattr(obj, 'name'): ReturnsTrueif the object has an attribute with the given name.getattr(obj, 'name', default): Returns the value of the attribute. If the attribute doesn't exist, it returns thedefaultvalue (or raises anAttributeErrorif no default is provided).setattr(obj, 'name', value): Sets the attribute to the given value. If the attribute doesn't exist, it will be created.delattr(obj, 'name'): Deletes the attribute.
class MyClass:
def __init__(self):
self.existing_attr = "I exist!"
obj = MyClass()
# Check for an attribute
print(f"Has 'existing_attr': {hasattr(obj, 'existing_attr')}")
# Output: Has 'existing_attr': True
print(f"Has 'new_attr': {hasattr(obj, 'new_attr')}")
# Output: Has 'new_attr': False
# Get an attribute's value
print(f"getattr(obj, 'existing_attr'): {getattr(obj, 'existing_attr')}")
# Output: getattr(obj, 'existing_attr'): I exist!
# Safely get an attribute that might not exist
print(f"getattr(obj, 'new_attr', 'default_value'): {getattr(obj, 'new_attr', 'default_value')}")
# Output: getattr(obj, 'new_attr', 'default_value'): default_value
# Set a new attribute
setattr(obj, 'new_attr', 'I was set dynamically!')
print(f"obj.new_attr: {obj.new_attr}")
# Output: obj.new_attr: I was set dynamically!
# Delete an attribute
delattr(obj, 'existing_attr')
print(f"After deletion, has 'existing_attr': {hasattr(obj, 'existing_attr')}")
# Output: After deletion, has 'existing_attr': False
callable(): Checking if an Object is Callable
This function tells you if you can "call" an object like a function.
def my_function():
return "Hello"
class MyCallableClass:
def __call__(self):
return "Hello from a callable instance!"
my_obj = MyCallableClass()
print(f"callable(my_function): {callable(my_function)}")
# Output: callable(my_function): True
print(f"callable(my_obj): {callable(my_obj)}")
# Output: callable(my_obj): True (because it has a __call__ method)
print(f"callable(42): {callable(42)}")
# Output: callable(42): False
The inspect Module: Advanced Introspection
For more detailed analysis, Python's standard library provides the inspect module. It's like a powerful microscope for your code.

import inspect import my_module # Assume this module has functions and classes
Inspecting Source Code
inspect.getsource(object): Get the source code of an object (function, class, method).inspect.getsourcelines(object): Get the source code as a list of lines and the starting line number.
def example_function(x, y):
"""This is a docstring."""
return x + y
print(inspect.getsource(example_function))
# Output:
# def example_function(x, y):
# """This is a docstring."""
# return x + y
Inspecting Signatures
inspect.signature(callable): Get aSignatureobject representing the call signature of a function. This is incredibly useful for building libraries and decorators.
def another_func(a, b=10, *args, c, **kwargs):
pass
sig = inspect.signature(another_func)
print(f"Signature: {sig}")
# Output: Signature: (a, b=10, *args, c, **kwargs)
# You can inspect the parameters
for name, param in sig.parameters.items():
print(f"Parameter: {name}, Default: {param.default}, Kind: {param.kind}")
# Output:
# Parameter: a, Default: <class inspect._empty>, Kind: POSITIONAL_OR_KEYWORD
# Parameter: b, Default: 10, Kind: POSITIONAL_OR_KEYWORD
# Parameter: args, Default: <class inspect._empty>, Kind: VAR_POSITIONAL
# Parameter: c, Default: <class inspect._empty>, Kind: KEYWORD_ONLY
# Parameter: kwargs, Default: <class inspect._empty>, Kind: VAR_KEYWORD
Inspecting Call Stacks
inspect.stack(): Get a list of frame records from the current call stack. This is used for advanced debugging and logging.
def function_b():
# Get the current call stack
stack = inspect.stack()
print("Call stack from function_b:")
for frame_info in stack:
print(f" - File: {frame_info.filename}, Function: {frame_info.function}, Line: {frame_info.lineno}")
def function_a():
function_b()
function_a()
# Output:
# Call stack from function_b:
# - File: <stdin>, Function: function_b, Line: 18
# - File: <stdin>, Function: function_a, Line: 22
# - File: <stdin>, Function: <module>, Line: 25
"Magic" (Dunder) Methods: The Engine of Introspection
Many of the introspection tools work by calling special "dunder" (double underscore) methods on your objects. When you call len(my_list), Python is actually calling my_list.__len__(). When you call print(my_list), it calls my_list.__str__().
You can implement these in your own classes to make them work seamlessly with Python's built-in functions.
class Book:
def __init__(self, title, author, pages):
self.title = title
self.author = author
self.pages = pages
# Called by len()
def __len__(self):
return self.pages
# Called by str()
def __str__(self):
return f"Book: {self.title} by {self.author}"
# Called by repr()
def __repr__(self):
return f"Book('{self.title}', '{self.author}', {self.pages})"
my_book = Book("The Hobbit", "J.R.R. Tolkien", 310)
# These calls trigger the dunder methods
print(my_book) # Calls __str__
# Output: Book: The Hobbit by J.R.R. Tolkien
print(repr(my_book)) # Calls __repr__
# Output: Book('The Hobbit', 'J.R.R. Tolkien', 310)
print(f"Length: {len(my_book)} pages") # Calls __len__
# Output: Length: 310 pages
Real-World Use Cases
- Data Validation Libraries (e.g., Pydantic): These libraries inspect the type hints of your data classes to automatically validate input data.
- Dependency Injection Frameworks: They inspect function signatures (
inspect.signature) to know which dependencies (arguments) a function needs and provide them automatically. - Debuggers and Profilers: Tools like
pdbuse introspection to inspect variables, call stacks, and object states at runtime. - Serialization Libraries (e.g., for JSON): They use
dir()andgetattr()to iterate over an object's attributes and convert them into a dictionary for serialization. - Dynamic API Wrappers: You can create a class that, when asked for an attribute (
getattr), makes an API call to fetch data and returns it as if it were a local attribute. This is how ORMs (Object-Relational Mappers) often work.
