Why is adding attributes to an already instantiated object allowed?

Question:

I am studying python, and although I think I get the whole concept and notion of Python, today I stumbled upon a piece of code that I did not fully understand:

Say I have a class that is supposed to define Circles but lacks a body:

class Circle():
    pass

Since I have not defined any attributes, how can I do this:

my_circle = Circle()
my_circle.radius = 12

The weird part is that Python accepts the above statement. I don’t understand why Python doesn’t raise an undefined name error. I do understand that via dynamic typing I just bind variables to objects whenever I want, but shouldn’t an attribute radius exist in the Circle class to allow me to do this?

EDIT: Lots of wonderful information in your answers! Thank you everyone for all those fantastic answers! It’s a pity I only get to mark one as an answer.

Asked By: NlightNFotis

||

Answers:

No, python is flexible like that, it does not enforce what attributes you can store on user-defined classes.

There is a trick however, using the __slots__ attribute on a class definition will prevent you from creating additional attributes not defined in the __slots__ sequence:

>>> class Foo(object):
...     __slots__ = ()
... 
>>> f = Foo()
>>> f.bar = 'spam'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'Foo' object has no attribute 'bar'
>>> class Foo(object):
...     __slots__ = ('bar',)
... 
>>> f = Foo()
>>> f.bar
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: bar
>>> f.bar = 'spam'
Answered By: Martijn Pieters

It creates a radius data member of my_circle.

If you had asked it for my_circle.radius it would have thrown an exception:

>>> print my_circle.radius # AttributeError

Interestingly, this does not change the class; just that one instance. So:

>>> my_circle = Circle()
>>> my_circle.radius = 5
>>> my_other_circle = Circle()
>>> print my_other_circle.radius # AttributeError
Answered By: inspectorG4dget

Python lets you store attributes of any name on virtually any instance (or class, for that matter). It’s possible to block this either by writing the class in C, like the built-in types, or by using __slots__ which allows only certain names.

The reason it works is that most instances store their attributes in a dictionary. Yes, a regular Python dictionary like you’d define with {}. The dictionary is stored in an instance attribute called __dict__. In fact, some people say “classes are just syntactic sugar for dictionaries.” That is, you can do everything you can do with a class with a dictionary; classes just make it easier.

You’re used to static languages where you must define all attributes at compile time. In Python, class definitions are executed, not compiled; classes are objects just like any other; and adding attributes is as easy as adding an item to a dictionary. This is why Python is considered a dynamic language.

Answered By: kindall

A leading principle is that there is no such thing as a declaration. That is, you never declare “this class has a method foo” or “instances of this class have an attribute bar”, let alone making a statement about the types of objects to be stored there. You simply define a method, attribute, class, etc. and it’s added. As JBernardo points out, any __init__ method does the very same thing. It wouldn’t make a lot of sense to arbitrarily restrict creation of new attributes to methods with the name __init__. And it’s sometimes useful to store a function as __init__ which don’t actually have that name (e.g. decorators), and such a restriction would break that.

Now, this isn’t universally true. Builtin types omit this capability as an optimization. Via __slots__, you can also prevent this on user-defined classes. But this is merely a space optimization (no need for a dictionary for every object), not a correctness thing.

If you want a safety net, well, too bad. Python does not offer one, and you cannot reasonably add one, and most importantly, it would be shunned by Python programmers who embrace the language (read: almost all of those you want to work with). Testing and discipline, still go a long way to ensuring correctness. Don’t use the liberty to make up attributes outside of __init__ if it can be avoided, and do automated testing. I very rarely have an AttributeError or a logical error due to trickery like this, and of those that happen, almost all are caught by tests.

Answered By: user395760

There are two types of attributes in Python – Class Data Attributes and Instance Data Attributes.

Python gives you flexibility of creating Data Attributes on the fly.

Since an instance data attribute is related to an instance, you can also do that in __init__ method or you can do it after you have created your instance..

class Demo(object):
    classAttr = 30
    def __init__(self):
         self.inInit = 10

demo = Demo()
demo.outInit = 20
Demo.new_class_attr = 45; # You can also create class attribute here.

print demo.classAttr  # Can access it 

del demo.classAttr         # Cannot do this.. Should delete only through class

demo.classAttr = 67  # creates an instance attribute for this instance.
del demo.classAttr   # Now OK.
print Demo.classAttr  

So, you see that we have created two instance attributes, one inside __init__ and one outside, after instance is created..

But a difference is that, the instance attribute created inside __init__ will be set for all the instances, while if created outside, you can have different instance attributes for different isntances..

This is unlike Java, where each Instance of a Class have same set of Instance Variables..

  • NOTE: – While you can access a class attribute through an instance, you cannot delete it..
    Also, if you try to modify a class attribute through an instance, you actually create an instance attribute which shadows the class attribute..
Answered By: Rohit Jain

Just to clarify some misunderstandings in the discussions here. This code:

class Foo(object):
    def __init__(self, bar):
        self.bar = bar

foo = Foo(5)

And this code:

class Foo(object):
    pass

foo = Foo()
foo.bar = 5

is exactly equivalent. There really is no difference. It does exactly the same thing. This difference is that in the first case it’s encapsulated and it’s clear that the bar attribute is a normal part of Foo-type objects. In the second case it is not clear that this is so.

In the first case you can not create a Foo object that doesn’t have the bar attribute (well, you probably can, but not easily), in the second case the Foo objects will not have a bar attribute unless you set it.

So although the code is programatically equivalent, it’s used in different cases.

Answered By: Lennart Regebro

As delnan said, you can obtain this behavior with the __slots__ attribute. But the fact that it is a way to save memory space and access type does not discard the fact that it is (also) a/the mean to disable dynamic attributes.

Disabling dynamic attributes is a reasonable thing to do, if only to prevent subtle bugs due to spelling mistakes. “Testing and discipline” is fine but relying on automated validation is certainly not wrong either – and not necessarily unpythonic either.

Also, since the attrs library reached version 16 in 2016 (obviously way after the original question and answers), creating a closed class with slots has never been easier.

>>> import attr
...
... @attr.s(slots=True)
... class Circle:
...   radius = attr.ib()
...
... f = Circle(radius=2)
... f.color = 'red'
AttributeError: 'Circle' object has no attribute 'color'
Answered By: P-Gn

How to prevent new attributes creation ?

Using class

To control the creation of new attributes, you can overwrite the __setattr__ method. It will be called every time my_obj.x = 123 is called.

See the documentation:

class A:
  def __init__(self):
    # Call object.__setattr__ to bypass the attribute checking
    super().__setattr__('x', 123)

  def __setattr__(self, name, value):
    # Cannot create new attributes
    if not hasattr(self, name):
      raise AttributeError('Cannot set new attributes')
    # Can update existing attributes
    super().__setattr__(name, value)

a = A()
a.x = 123  # Allowed
a.y = 456  # raise AttributeError

Note that users can still bypass the checking if they call directly object.__setattr__(a, 'attr_name', attr_value).

Using dataclass

With dataclasses, you can forbid the creation of new attributes with frozen=True. It will also prevent existing attributes to be updated.

@dataclasses.dataclass(frozen=True)
class A:
  x: int


a = A(x=123)
a.y = 123  # Raise FrozenInstanceError
a.x = 123  # Raise FrozenInstanceError

Note: dataclasses.FrozenInstanceError is a subclass of AttributeError

Answered By: Conchylicultor

To add to Conchylicultor’s answer, Python 3.10 added a new parameter to dataclass.

The slots parameter will create the __slots__ attribute in the class, preventing creation of new attributes outside of __init__, but allowing assignments to existing attributes.

If slots=True, assigning to an attribute that was not defined will throw an AttributeError.

Here is an example with slots and with frozen:

from dataclasses import dataclass

@dataclass
class Data:
    x_float=0
    y_float=0

@dataclass(frozen=True)
class DataFrozen:
    x_float=0
    y_float=0

@dataclass(slots=True)
class DataSlots:
    x_float=0
    y_float=0

p = Data(1,2)
p.x = 5 # ok
p.z = 8 # ok

p = DataFrozen(1,2)
p.x = 5 # FrozenInstanceError
p.z = 8 # FrozenInstanceError

p = DataSlots(1,2)
p.x = 5 # ok
p.z = 8 # AttributeError
Answered By: robertspierre