Inspecting Objects at Runtime
Why Introspection Matters
The Problem: Frameworks need to inspect your code: serializers, ORMs, dependency injection, plugin loaders, debuggers all rely on it.
The Solution: Python exposes its object model at runtime through inspect, getattr, type, and __dict__ — letting libraries do magic that would require code generation in other languages.
Real Impact: Use introspection sparingly in app code, generously in library code — it's the foundation of pytest fixtures, Django models, FastAPI dependency injection, and dataclasses.
Real-World Analogy
Think of introspection as the building inspector with x-ray vision:
- type(x) = what kind of building is this?
- dir(x) = the floor plan — what rooms (attributes) exist
- getattr / setattr = knocking down or putting up internal walls dynamically
- inspect.signature = reading the door's handle to know what it takes
- Metaclass = the architect who decides how buildings of this type get constructed
class User:
def __init__(self, name):
self.name = name
def greet(self):
return f"hi {self.name}"
u = User("Alice")
type(u) # <class 'User'>
isinstance(u, User) # True
issubclass(User, object) # True
dir(u) # list of attributes + methods (incl. dunders)
vars(u) # {'name': 'Alice'} — instance __dict__
hasattr(u, "name") # True
getattr(u, "name") # 'Alice'
getattr(u, "email", None) # None — safe access
setattr(u, "email", "[email protected]")
delattr(u, "email")
Dynamic attribute access
getattr/setattr let you treat attribute names as runtime data. Useful for plugin systems, config loaders, ORMs — but harder to follow than explicit attribute access. Use sparingly.
The inspect Module
Standard library's reflection toolkit — read function signatures, source code, class hierarchies.
import inspect
def divide(a: float, b: float = 1.0) -> float:
"""Divide a by b."""
return a / b
# Inspect signature
sig = inspect.signature(divide)
print(sig) # (a: float, b: float = 1.0) -> float
for name, param in sig.parameters.items():
print(name, param.annotation, param.default)
# Get source code and docstring
print(inspect.getsource(divide))
print(inspect.getdoc(divide))
# Class hierarchy
for base in inspect.getmro(User):
print(base)
Inspecting Modules
import inspect
import math
for name, member in inspect.getmembers(math, inspect.isfunction):
print(name)
__dict__, __slots__, and __class__
class Point:
def __init__(self, x, y):
self.x, self.y = x, y
p = Point(1, 2)
p.__dict__ # {'x': 1, 'y': 2}
p.__class__ # <class 'Point'>
p.__class__.__name__ # 'Point'
# With __slots__ — no per-instance __dict__
class Pt:
__slots__ = ("x", "y")
def __init__(self, x, y):
self.x, self.y = x, y
p = Pt(1, 2)
p.z = 3 # AttributeError — no slot for z
__getattr__ and __getattribute__
Customize attribute access. __getattr__ is called only when normal lookup fails — perfect for proxies or lazy attributes.
class LazyConfig:
def __init__(self, source):
self._source = source
self._cache = {}
def __getattr__(self, name):
if name not in self._cache:
self._cache[name] = self._source.load(name)
return self._cache[name]
cfg = LazyConfig(FileSource("settings.yml"))
print(cfg.database_url) # lazily loaded
⚠️ __getattribute__ is invoked for EVERY attribute access
Including self.something inside the method itself — easy to cause infinite recursion. Use __getattr__ instead unless you really need to intercept all accesses.
Metaclasses (Brief)
A metaclass is the class of a class. type is the default metaclass — every class is an instance of type. You almost never need custom metaclasses; class decorators and __init_subclass__ cover 95% of cases.
# type() can create classes dynamically
User = type("User", (), {"name": "Alice", "greet": lambda self: "hi"})
u = User()
print(u.greet()) # 'hi'
__init_subclass__ — the modern alternative
class Plugin:
registry = []
def __init_subclass__(cls, **kwargs):
super().__init_subclass__(**kwargs)
Plugin.registry.append(cls)
class EmailPlugin(Plugin): ...
class SmsPlugin(Plugin): ...
print(Plugin.registry) # [EmailPlugin, SmsPlugin] — auto-registered
Tim Peters' rule
"Metaclasses are deeper magic than 99% of users should ever worry about. If you wonder whether you need them, you don't." Use decorators, mixins, or __init_subclass__ first.
🎯 Practice Exercises
Exercise 1: Function inspector
Write describe(fn) that prints its name, signature, docstring, and source.
Exercise 2: Object diff
Given two instances, return a dict of attributes that differ. Use vars() and set difference.
Exercise 3: Plugin registry
Build a plugin base class using __init_subclass__ that registers concrete plugins keyed by name.
Exercise 4: Lazy proxy
Build a Lazy wrapper using __getattr__ that defers object creation until first attribute access.