If you use class attributes (self.key1 ...) a tool that checks your code statically (like pylint) will show you unused and unsefined variables and therefore mistypes.
class toy(object):
pass
a = toy()
a.key1 = "hello world"
print a.key10
Pylint run:
> pylint toto.py
************* Module toto
C: 1,0: Black listed name "toto"
C: 1,0: Missing docstring
C: 1,0:toy: Invalid name "toy" (should match [A-Z_][a-zA-Z0-9]+$)
C: 1,0:toy: Missing docstring
W: 5,0: Attribute 'key1' defined outside __init__
R: 1,0:toy: Too few public methods (0/2)
C: 4,0: Invalid name "a" (should match (([A-Z_][A-Z0-9_]*)|(__.*__))$)
E: 6,6: Instance of 'toy' has no 'key10' member
That won't be the case with keys in a dictionary. A typing mistake will go silent, which is why I would prefer class attributes. However if you have a dictionary you can easily iterate through the set of keys. While you can also get the list of attributes of a class instance, you will get some noise in it. (see key1 lost among the other attributes defined by default)
>>> class toy(object):
... pass
...
>>> a = toy()
>>> a.key1 = "hello world"
>>> dir(a)
['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'key1']
So, if you don't need to iterate in the list of "keys" you have created, I'd use the class attribute way.