Where do Python objects store their attributes?
We have a class here, called Product:
class Product:
def __init__(self, name, price):
self.name = name
self.price = price
def display_price(self):
return f"${self.price:,.2f}"
And we have two instances of this class:
>>> duck = Product("rubber duck", price=1)
>>> mug = Product("mug", price=5)
Each of these class instances has their own separate data (a name attribute, and a price attribute):
>>> duck.name
'rubber duck'
>>> mug.price
5
Where are these attributes actually stored? Where does their data live?
__dict__ attributeEach of these class instances have a __dict__ attribute, which is a dictionary that actually stores the data that we'd normally look up on these objects:
>>> duck.__dict__
{'name': 'rubber duck', 'price': 1}
We can actually modify this dictionary in order to change the attributes on one of these objects:
>>> duck.__dict__["price"] = 99
>>> duck.__dict__
{'name': 'rubber duck', 'price': 99}
Note that you shouldn't do this. This is not something you're supposed to do in Python, but you can do it.
And in fact, this is what Python does under the hood whenever you assign to an attribute on a class instance.
So, if we assign to an attribute on our duck object:
>>> duck.price = 7
The __dict__ dictionary for that object will change:
>>> duck.__dict__
{'name': 'rubber duck', 'price': 7}
__dict__ attributePython's modules also use a __dict__ attribute to store their data.
If we look at the __dict__ attribute of the math module, we'll see a whole bunch of stuff in this dictionary:
>>> import math
>>> math.__dict__
{'__name__': 'math', '__package__': '',
'__doc__': 'This module provides access to the mathematical functions\ndefined by the C standard.',
'__loader__': <_frozen_importlib_external.ExtensionFileLoader object at 0x7f47ad279f20>,
'__spec__': ModuleSpec(name='math', loader=<_frozen_importlib_external.ExtensionFileLoader object at 0x7f47ad279f20>, origin='/home/trey/.pyenv/versions/3.14.0rc1/lib/python3.14/lib-dynload/math.cpython-314-x86_64-linux-gnu.so'),
'__file__': '/usr/lib/python3.14/lib-dynload/math.cpython-314-x86_64-linux-gnu.so',
'acos': <built-in function acos>, 'acosh': <built-in function acosh>,
'asin': <built-in function asin>, 'asinh': <built-in function asinh>,
'atan': <built-in function atan>, 'atan2': <built-in function atan2>,
'atanh': <built-in function atanh>, 'cbrt': <built-in function cbrt>,
'ceil': <built-in function ceil>, 'copysign': <built-in function copysign>,
'cos': <built-in function cos>, 'cosh': <built-in function cosh>,
'degrees': <built-in function degrees>, 'dist': <built-in function dist>,
'erf': <built-in function erf>, 'erfc': <built-in function erfc>,
'exp': <built-in function exp>, 'exp2': <built-in function exp2>,
'expm1': <built-in function expm1>, 'fabs': <built-in function fabs>,
'factorial': <built-in function factorial>, 'floor': <built-in function floor>,
'fma': <built-in function fma>, 'fmod': <built-in function fmod>,
'frexp': <built-in function frexp>, 'fsum': <built-in function fsum>,
'gamma': <built-in function gamma>, 'gcd': <built-in function gcd>,
'hypot': <built-in function hypot>, 'isclose': <built-in function isclose>,
'isfinite': <built-in function isfinite>, 'isinf': <built-in function isinf>,
'isnan': <built-in function isnan>, 'isqrt': <built-in function isqrt>,
'lcm': <built-in function lcm>, 'ldexp': <built-in function ldexp>,
'lgamma': <built-in function lgamma>, 'log': <built-in function log>,
'log1p': <built-in function log1p>, 'log10': <built-in function log10>,
'log2': <built-in function log2>, 'modf': <built-in function modf>,
'pow': <built-in function pow>, 'radians': <built-in function radians>,
'remainder': <built-in function remainder>, 'sin': <built-in function sin>,
'sinh': <built-in function sinh>, 'sqrt': <built-in function sqrt>,
'tan': <built-in function tan>, 'tanh': <built-in function tanh>,
'sumprod': <built-in function sumprod>, 'trunc': <built-in function trunc>,
'prod': <built-in function prod>, 'perm': <built-in function perm>,
'comb': <built-in function comb>, 'nextafter': <built-in function nextafter>,
'ulp': <built-in function ulp>, 'pi': 3.141592653589793,
'e': 2.718281828459045, 'tau': 6.283185307179586, 'inf': inf, 'nan': nan}
Most of these things are functions that exist in the math module, but some of them are constants (like pi, e, and tau).
__dict__ attributeJust like modules and class instances, classes also use a __dict__ attribute to store class attributes:
>>> Product.__dict__
mappingproxy({'__module__': '__main__', '__firstlineno__': 1,
'__init__': <function Product.__init__ at 0x76c124f2b480>,
'display_price': <function Product.display_price at 0x76c124d32b90>,
'__static_attributes__': ('name', 'price'),
'__dict__': <attribute '__dict__' of 'Product' objects>,
'__weakref__': <attribute '__weakref__' of 'Product' objects>,
'__doc__': None})
Note that we're not looking at __dict__ on an instance of the Product class.
We're looking at __dict__ on the class itself.
You can see that our __init__ method and the display_price method are in this __dict__ dictionary.
We often think of attributes and methods as completely distinct groups. But methods are also attributes.
A methods is a function that lives on a class, which means it's also a class-level attribute.
__dict__ attributeIf you pass an object to the built-in vars function, it will return the __dict__ dictionary for that object:
>>> duck = Product("rubber duck", price=1)
>>> vars(duck)
{'name': 'rubber duck', 'price': 1}
You might wonder, what's the point of that?
Why not just access the __dict__ attribute directly:
>>> duck.__dict__
{'name': 'rubber duck', 'price': 1}
I usually try to avoid accessing dunder attributes if there's a higher-level way to get the same information.
For example, I prefer to use the built-in type function to get the type of an object, instead of accessing that object's __class__ attribute:
>>> type(duck)
<class '__main__.Product'>
>>> duck.__class__
<class '__main__.Product'>
They do the same thing, but the type function is higher-level.
Similarly, I often consider using the built-in vars function instead of directly accessing the __dict__ attribute of an object.
But, they do the same thing.
Meaning, the vars function only works on objects that have a __dict__ attribute.
And not every object does!
__dict__ attributeUp to this point, you might have assumed that every object in Python is powered by a dictionary that stores its attributes. But that's not quite true.
Not every object in Python uses a dictionary to store its attributes.
For example, all integers have a real attribute:
>>> x = 4
>>> x.real
4
But they don't store their data in a dictionary.
They don't have a __dict__ attribute:
>>> x.__dict__
Traceback (most recent call last):
File "<python-input-33>", line 1, in <module>
x.__dict__
AttributeError: 'int' object has no attribute '__dict__'. Did you mean: '__dir__'?
Most of Python's built-in classes do not use a __dict__ dictionary to store the data for their class instances.
They store that data somewhere else that we can't easily access.
This is a memory optimization.
It's actually possible to optimize the memory of our own classes using a __slots__ attribute.
But that's a discussion for another time.
If we want to see all the attributes that live on an arbitrary Python object, we can use the built-in dir function, which I often pronounce dur.
Every object in Python can be be passed to the built-in dir function to see the attributes that object has:
>>> dir(x)
['__abs__', '__add__', '__and__', '__bool__', '__ceil__', '__class__',
'__delattr__', '__dir__', '__divmod__', '__doc__', '__eq__', '__float__',
'__floor__', '__floordiv__', '__format__', '__ge__', '__getattribute__',
'__getnewargs__', '__gt__', '__hash__', '__index__', '__init__',
'__init_subclass__', '__int__', '__invert__', '__le__', '__lshift__', '__lt__',
'__mod__', '__mul__', '__ne__', '__neg__', '__new__', '__or__', '__pos__',
'__pow__', '__radd__', '__rand__', '__rdivmod__', '__reduce__',
'__reduce_ex__', '__repr__', '__rfloordiv__', '__rlshift__', '__rmod__',
'__rmul__', '__ror__', '__round__', '__rpow__', '__rrshift__', '__rshift__',
'__rsub__', '__rtruediv__', '__rxor__', '__setattr__', '__sizeof__', '__str__',
'__sub__', '__subclasshook__', '__truediv__', '__trunc__', '__xor__',
'as_integer_ratio', 'bit_length', 'conjugate', 'denominator', 'from_bytes',
'imag', 'numerator', 'real', 'to_bytes']
But note that this is a little bit different from accessing __dict__ attribute on a class instance.
The dir function shows us both attributes that live on the object, and attributes that live on that object's class (like the dunder methods).
So when you pass an object to dir, you'll see instance attributes and class attributes, including methods.
The dir function works on any object.
So for example, if we pass a module to the dir function, we'll see all the attributes that live in that module:
>>> import math
>>> dir(math)
['__doc__', '__file__', '__loader__', '__name__', '__package__', '__spec__',
'acos', 'acosh', 'asin', 'asinh', 'atan', 'atan2', 'atanh', 'ceil', 'comb',
'copysign', 'cos', 'cosh', 'degrees', 'dist', 'e', 'erf', 'erfc', 'exp',
'expm1', 'fabs', 'factorial', 'floor', 'fmod', 'frexp', 'fsum', 'gamma', 'gcd',
'hypot', 'inf', 'isclose', 'isfinite', 'isinf', 'isnan', 'isqrt', 'lcm',
'ldexp', 'lgamma', 'log', 'log10', 'log1p', 'log2', 'modf', 'nan', 'nextafter',
'perm', 'pi', 'pow', 'prod', 'radians', 'remainder', 'sin', 'sinh', 'sqrt',
'tan', 'tanh', 'tau', 'trunc', 'ulp']
It's actually a pretty handy way to see what a module contains.
__dict__ dictionaryMost objects in Python store their attributes in a __dict__ dictionary.
The built-in vars function can be used to access this dictionary, returning all the instance attributes of an object.
But not all objects have a __dict__ dictionary, so not all objects work with the built-in vars function.
All objects do work with the built-in dir function, which will give you not just the attributes, but also the methods and class attributes of an object.
And if you're curious where your object's attributes are stored, it's probably in a __dict__ dictionary.
Need to fill-in gaps in your Python skills?
Sign up for my Python newsletter where I share one of my favorite Python tips every week.
Need to fill-in gaps in your Python skills? I send weekly emails designed to do just that.
Sign in to your Python Morsels account to track your progress.
Don't have an account yet? Sign up here.