Python Instance: How Objects Are Created and Managed
python instance: Learn how Python instances are created, how __init__ and __new__ work, and how to manage attributes and memory with __slots__.
python instance requires a clear understanding of the core syntax, runtime behavior, and practical implementation patterns demonstrated in the examples below.
In Python, an instance is the concrete object created from a class definition. When you call a class like MyClass(), Python returns an instance of that class, with its own namespace for attributes. Understanding how instances are created, how they store data, and how they are cleaned up is essential for writing reliable object-oriented code.
How Instances Are Created: new and init
When you instantiate a class, Python calls two special methods in sequence: __new__ and __init__. The __new__ method is responsible for allocating the instance and is called before __init__. In most cases, you only need to define __init__, which initializes the instance's attributes. However, __new__ becomes relevant when you need to control instance creation itself, such as in singleton patterns or when subclassing immutable types.
class Point: def __new__(cls, x, y): print("Creating instance") instance = super().__new__(cls) return instance def __init__(self, x, y): print("Initializing instance") self.x = x self.y = y p = Point(1, 2)
The output shows that __new__ runs first, then __init__. The __new__ method must return an instance, usually by calling super().__new__(cls). If it returns an instance of the same class, Python then calls __init__ on it. If it returns an instance of a different class, __init__ is skipped.
Instance Attributes and the self Parameter
Instance attributes are stored in the instance's __dict__ by default. The self parameter in methods refers to the current instance, allowing you to read and write its attributes. Each instance has its own independent set of attributes, even if they are initialized with the same values.
class Car: def __init__(self, model): self.model = model self.mileage = 0 car1 = Car("Sedan") car2 = Car("SUV") car1.mileage = 100 print(car2.mileage) # 0
The self.model assignment creates an attribute on the instance. Because each instance has its own __dict__, changes to car1 do not affect car2. This is the fundamental behavior that makes instances independent objects.
Instance vs Class Attributes
A common source of confusion is the difference between instance attributes and class attributes. Class attributes are defined directly in the class body and are shared by all instances. Instance attributes are assigned via self and are unique per instance.
class Employee: company = "Acme" # class attribute def __init__(self, name): self.name = name # instance attribute e1 = Employee("Alice") e2 = Employee("Bob") print(e1.company) # Acme e1.company = "Globex" # creates an instance attribute print(e2.company) # Acme
When you assign e1.company, Python creates a new instance attribute that shadows the class attribute for that instance only. The class attribute remains unchanged for other instances. This behavior is useful for default values but can lead to bugs if you expect shared state to be mutable.
Controlling Instance Memory with slots
By default, every instance has a __dict__ and a __weakref__ attribute, which adds memory overhead. For classes with many instances, this overhead can become significant. Defining __slots__ in the class tells Python to allocate a fixed set of attributes, eliminating the per-instance __dict__.
class Point: __slots__ = ("x", "y") def __init__(self, x, y): self.x = x self.y = y
Instances of this class use less memory because they do not have a __dict__. However, you cannot add new attributes beyond those listed in __slots__. Attempting to set an undeclared attribute raises AttributeError. This tradeoff is acceptable when the set of attributes is known in advance and memory usage matters, such as in large data-processing pipelines.
Common Mistakes with Instances
One frequent mistake is mutating a mutable default argument in __init__. Because default arguments are evaluated once at function definition time, all instances share the same list or dictionary.
class ShoppingCart: def __init__(self, items=[]): # problematic self.items = items cart1 = ShoppingCart() cart1.items.append("apple") cart2 = ShoppingCart() print(cart2.items) # ['apple']
The correct approach is to use None as the default and create a new list inside the method.
class ShoppingCart: def __init__(self, items=None): self.items = items if items is not None else []
Another mistake is assuming that is compares instance values. is checks identity, not equality. Use == for value comparison unless you specifically need to check whether two variables reference the same instance.
Instance Lifecycle and Garbage Collection
Instances are reference-counted in CPython. When the last reference to an instance goes out of scope or is reassigned, its memory is reclaimed immediately. You can observe this with the __del__ method, though it is not recommended for resource cleanup because its timing is not guaranteed.
class Resource: def __del__(self): print("Resource released") r = Resource() del r # prints "Resource released"
Circular references can prevent immediate deallocation because reference counting alone cannot detect cycles. The cyclic garbage collector runs periodically to collect such objects. For long-running applications, keeping instances alive unnecessarily can cause memory bloat, so it is wise to release references when they are no longer needed.
When to Use slots and When to Avoid It
__slots__ is a powerful memory optimization, but it comes with constraints. Use it when you have a fixed set of attributes and need to reduce memory footprint, especially in applications that create millions of instances. Avoid it when you need dynamic attributes, when you rely on __dict__ for serialization or debugging, or when you use mixins that expect a __dict__.
class Dynamic: pass d = Dynamic() d.anything = 123 # works class Fixed: __slots__ = ("a",) f = Fixed() f.b = 1 # AttributeError
The decision depends on the tradeoff between flexibility and memory. In most application code, the overhead of __dict__ is acceptable, but in performance-critical or memory-constrained environments, __slots__ can make a measurable difference.