Python - vars() and dir() Functions
Python's introspection capabilities are among its most powerful features for debugging, metaprogramming, and building dynamic systems. Two functions sit at the heart of object inspection: `vars()`...
Key Insights
vars()returns an object’s__dict__attribute (instance variables only), whiledir()returns a comprehensive list of all accessible names including inherited methods and attributes- Use
vars()when you need to read or modify instance attributes dynamically; usedir()when exploring an unfamiliar object’s capabilities - Both functions fail gracefully in different ways:
vars()raisesTypeErroron objects without__dict__, whiledir()always returns something useful
Introduction
Python’s introspection capabilities are among its most powerful features for debugging, metaprogramming, and building dynamic systems. Two functions sit at the heart of object inspection: vars() and dir(). Despite their similar-sounding purposes, they serve fundamentally different roles.
Understanding when to reach for each function separates developers who fumble through debugging sessions from those who surgically identify issues. These functions aren’t just debugging tools—they’re essential for building serializers, ORMs, testing frameworks, and any code that needs to work with objects generically.
Let’s cut through the confusion and establish exactly what each function does, when to use it, and the gotchas that trip up even experienced Python developers.
Understanding vars()
The vars() function returns the __dict__ attribute of an object. This dictionary contains the object’s writable instance attributes—the data that makes each instance unique.
When called without arguments, vars() returns the local symbol table, equivalent to locals().
class User:
def __init__(self, name, email, age):
self.name = name
self.email = email
self.age = age
def greet(self):
return f"Hello, {self.name}"
user = User("Alice", "alice@example.com", 30)
# Using vars() on an instance
print(vars(user))
# Output: {'name': 'Alice', 'email': 'alice@example.com', 'age': 30}
# This is identical to accessing __dict__ directly
print(user.__dict__)
# Output: {'name': 'Alice', 'email': 'alice@example.com', 'age': 30}
# Verify they're the same object
print(vars(user) is user.__dict__)
# Output: True
The critical insight here: vars() returns the actual dictionary, not a copy. Modifications to this dictionary directly affect the object:
# Dynamically add an attribute
vars(user)['role'] = 'admin'
print(user.role)
# Output: admin
# Modify existing attribute
vars(user)['age'] = 31
print(user.age)
# Output: 31
When called without arguments inside a function, vars() returns the local namespace:
def calculate_total(price, quantity, tax_rate=0.08):
subtotal = price * quantity
tax = subtotal * tax_rate
total = subtotal + tax
print(vars())
return total
calculate_total(29.99, 3)
# Output: {'price': 29.99, 'quantity': 3, 'tax_rate': 0.08,
# 'subtotal': 89.97, 'tax': 7.1976, 'total': 97.1676}
Understanding dir()
The dir() function takes a broader approach. It returns a sorted list of names—strings representing attributes, methods, and other accessible names. Unlike vars(), it includes inherited attributes and special methods.
class Animal:
species = "Unknown"
def breathe(self):
return "Breathing..."
class Dog(Animal):
def __init__(self, name):
self.name = name
def bark(self):
return "Woof!"
dog = Dog("Rex")
print(dir(dog))
# Output includes: [..., 'bark', 'breathe', 'name', 'species', ...]
# Plus all inherited object methods like __class__, __init__, etc.
When called without arguments, dir() returns names in the current local scope:
import json
def example_function():
x = 10
y = "hello"
print(dir())
example_function()
# Output: ['x', 'y']
dir() is particularly useful for exploring modules and built-in types:
# Exploring a module
import datetime
print([name for name in dir(datetime) if not name.startswith('_')])
# Output: ['MAXYEAR', 'MINYEAR', 'date', 'datetime', 'time', 'timedelta', 'timezone', 'tzinfo']
# Exploring string methods
print([m for m in dir(str) if not m.startswith('_')])
# Output: ['capitalize', 'casefold', 'center', 'count', 'encode', ...]
Key Differences Between vars() and dir()
The differences become stark when you examine the same object with both functions:
class Vehicle:
wheels = 4 # Class attribute
def __init__(self, brand, model):
self.brand = brand # Instance attribute
self.model = model # Instance attribute
def start(self):
return f"{self.brand} {self.model} starting..."
car = Vehicle("Toyota", "Camry")
print("vars() output:")
print(vars(car))
# Output: {'brand': 'Toyota', 'model': 'Camry'}
print("\ndir() output (filtered):")
print([attr for attr in dir(car) if not attr.startswith('_')])
# Output: ['brand', 'model', 'start', 'wheels']
Notice the key differences:
| Aspect | vars() |
dir() |
|---|---|---|
| Return type | dict |
list of strings |
| Class attributes | Excluded | Included |
| Methods | Excluded | Included |
| Inherited members | Excluded | Included |
| Modifiable | Yes (returns actual __dict__) |
No (returns new list) |
The TypeError behavior also differs significantly:
# vars() fails on objects without __dict__
try:
print(vars(42))
except TypeError as e:
print(f"vars() error: {e}")
# Output: vars() error: vars() argument must have __dict__ attribute
# dir() always works
print(dir(42)[:5])
# Output: ['__abs__', '__add__', '__and__', '__bool__', '__ceil__']
Practical Use Cases
Building a Simple Object Serializer
vars() shines when converting objects to dictionaries for JSON serialization:
import json
from datetime import datetime
class Order:
def __init__(self, order_id, items, total, created_at=None):
self.order_id = order_id
self.items = items
self.total = total
self.created_at = created_at or datetime.now()
def serialize_object(obj):
"""Convert an object to a JSON-serializable dictionary."""
data = {}
for key, value in vars(obj).items():
if isinstance(value, datetime):
data[key] = value.isoformat()
elif hasattr(value, '__dict__'):
data[key] = serialize_object(value)
else:
data[key] = value
return data
order = Order("ORD-001", ["Widget", "Gadget"], 59.99)
print(json.dumps(serialize_object(order), indent=2))
Attribute Discovery and Documentation
dir() excels at exploring unfamiliar objects:
def discover_object(obj, show_private=False):
"""Discover and categorize an object's attributes."""
attributes = []
methods = []
for name in dir(obj):
if not show_private and name.startswith('_'):
continue
attr = getattr(obj, name)
if callable(attr):
methods.append(name)
else:
attributes.append(name)
return {
'type': type(obj).__name__,
'attributes': attributes,
'methods': methods
}
import collections
result = discover_object(collections.Counter([1, 2, 2, 3, 3, 3]))
print(f"Type: {result['type']}")
print(f"Attributes: {result['attributes']}")
print(f"Methods: {result['methods'][:10]}...") # First 10 methods
Dynamic Configuration Loading
class Config:
def __init__(self, **kwargs):
for key, value in kwargs.items():
setattr(self, key, value)
def to_dict(self):
return vars(self).copy()
def update_from_dict(self, data):
vars(self).update(data)
config = Config(debug=True, database_url="postgres://localhost/app")
config.update_from_dict({'cache_ttl': 3600, 'debug': False})
print(config.to_dict())
# Output: {'debug': False, 'database_url': 'postgres://localhost/app', 'cache_ttl': 3600}
Common Pitfalls and Edge Cases
Objects Using slots
Classes that define __slots__ don’t have a __dict__ attribute, causing vars() to fail:
class Point:
__slots__ = ['x', 'y']
def __init__(self, x, y):
self.x = x
self.y = y
point = Point(3, 4)
# dir() works fine
print([attr for attr in dir(point) if not attr.startswith('_')])
# Output: ['x', 'y']
# vars() raises TypeError
try:
print(vars(point))
except TypeError as e:
print(f"Error: {e}")
# Output: Error: vars() argument must have __dict__ attribute
# Workaround: manually build dict from slots
def slots_to_dict(obj):
slots = getattr(type(obj), '__slots__', [])
return {slot: getattr(obj, slot) for slot in slots}
print(slots_to_dict(point))
# Output: {'x': 3, 'y': 4}
Read-Only Mappings
Some objects return read-only mappings from vars():
import types
# Module __dict__ is a read-only mappingproxy
module_vars = vars(types)
print(type(module_vars))
# Output: <class 'mappingproxy'>
try:
module_vars['custom'] = 'value'
except TypeError as e:
print(f"Cannot modify: {e}")
# Output: Cannot modify: 'mappingproxy' object does not support item assignment
Conclusion
| Use Case | Function | Reason |
|---|---|---|
| Serialize object to dict | vars() |
Direct access to instance data |
| Explore unknown object | dir() |
Shows all accessible names |
| Modify attributes dynamically | vars() |
Returns mutable __dict__ |
| Check if method exists | dir() |
Includes methods and inherited members |
| Debug local variables | vars() |
Returns local namespace |
| Build documentation tools | dir() |
Comprehensive attribute listing |
Use vars() when you need the raw instance data for serialization, cloning, or dynamic modification. Use dir() when you’re exploring, debugging, or need to know everything an object can do. Both functions are indispensable—knowing which to reach for makes you a more effective Python developer.