Python Object Creation: __new__, __init__, and Beyond
python object creation: Understand how Python creates objects: the __new__ and __init__ sequence, dataclasses, factory methods, and memory tradeoffs.
Python object creation follows a two-step sequence that most developers never need to touch. When you write MyClass(), Python first calls __new__ to allocate the instance, then calls __init__ to initialize it. Understanding this sequence matters when you need to control how objects come into existence, whether that means caching instances, building immutable types, or reducing memory overhead.
What Happens When You Call a Class
When you write MyClass(), Python performs a two-step process. First it calls __new__ to allocate the object, then it calls __init__ to initialize it. Most developers only ever override __init__, but understanding the full sequence matters when you need to control allocation itself.
class Point: def __new__(cls, x, y): instance = super().__new__(cls) print("allocating") return instance def __init__(self, x, y): self.x = x self.y = y print("initializing")
When you call Point(3, 4), Python invokes __new__ first, which receives the class as its first argument. The __new__ method must return an instance, and then __init__ is called on that returned instance with the same arguments. If __new__ returns an instance of a different class, __init__ is skipped entirely.
This separation matters in a few practical situations. Immutable types like tuples and frozensets need __new__ because their values are set at allocation time and cannot be changed afterward. Singleton patterns also rely on __new__ to return a cached instance instead of allocating a fresh one.
Why __init__ Is Usually Enough
For the vast majority of classes, __init__ is the only hook you need. It receives the freshly allocated instance and sets up its attributes. Python's default __new__ already handles allocation correctly for regular classes, so overriding it adds complexity without changing behavior.
class Account: def __init__(self, owner: str, balance: float = 0.0): self.owner = owner self.balance = balance
The instance already exists by the time __init__ runs. That is why you can assign attributes freely inside it. The object was allocated by object.__new__, which every class inherits unless it explicitly overrides __new__.
One common mistake is returning a value from __init__. That raises TypeError because __init__ must return None. The allocation step is the one responsible for producing the instance, and __init__ only mutates it.
Dataclasses for Structured Object Creation
The dataclasses module in the standard library removes a large amount of boilerplate from object creation. A dataclass generates __init__, __repr__, __eq__, and other methods based on annotated class attributes.
from dataclasses import dataclass @dataclass class Order: order_id: str items: list[str] total: float status: str = "pending"
The generated __init__ accepts order_id, items, and total as required parameters, while status defaults to "pending". Field order follows declaration order, and default values must come after non-default fields, just as in a plain function signature.
Dataclasses support frozen=True to make instances immutable, which produces a __setattr__ that raises FrozenInstanceError on assignment. They also support slots=True (Python 3.10+) to reduce memory usage by generating a class with __slots__ instead of a per-instance __dict__.
Factory Functions and Class Methods
Sometimes object creation needs more logic than a constructor can express. A factory function or a class method can encapsulate that logic while keeping the public API clean.
import json class Config: def __init__(self, path: str, values: dict): self.path = path self.values = values @classmethod def from_file(cls, path: str) -> "Config": with open(path) as f: data = json.load(f) return cls(path, data)
The class method from_file reads a JSON file and passes the parsed dictionary to the constructor. Callers use Config.from_file("app.json") instead of reading the file themselves. This keeps the file-reading logic in one place and makes the construction path explicit.
A factory function can serve a similar purpose when the return type is not always the same class. For example, a function that returns different subclasses based on a configuration value gives callers a uniform interface while hiding the branching logic.
Customizing Creation with Metaclasses
Metaclasses intercept class creation itself, which is a different layer from instance creation. A metaclass's __call__ controls what happens when you invoke the class, including whether __new__ and __init__ run at all.
class SingletonMeta(type): _instances = {} def __call__(cls, *args, **kwargs): if cls not in cls._instances: cls._instances[cls] = super().__call__(*args, **kwargs) return cls._instances[cls]
With this metaclass, every call to MySingleton() returns the same instance. The first call proceeds through normal allocation and initialization; subsequent calls return the cached object. This is a classic pattern, but it comes with tradeoffs. The constructor arguments are ignored on subsequent calls, which can surprise callers who expect a fresh object.
Metaclasses are rarely necessary in application code. They are more common in frameworks and libraries where the framework needs to inspect or modify classes at definition time. If you are choosing between a metaclass and a class method, prefer the class method unless you need to alter behavior for every subclass automatically.
Runtime Cost of Object Creation
Object creation in Python is not free. Each call to a class allocates a new instance, and if the class has a __dict__, that dictionary is allocated as well. For code that creates millions of short-lived objects, this allocation pressure can become measurable.
Using __slots__ is one way to reduce per-instance memory. A class with __slots__ declares a fixed set of attribute names, and Python stores those attributes in a compact internal structure instead of a dictionary.
class Vector: __slots__ = ("x", "y") def __init__(self, x: float, y: float): self.x = x self.y = y
The tradeoff is flexibility: instances of a slotted class cannot have attributes that are not declared in __slots__. That is usually acceptable for a well-defined data type, but it breaks code that relies on dynamically attaching attributes.
Dataclasses with slots=True give you the same memory benefit without writing __slots__ manually. The generated class includes the slot declaration automatically.
For high-throughput code, reusing objects instead of creating new ones can help, but it introduces statefulness and makes concurrency harder. Object pools are worth considering only when profiling shows that allocation is the actual bottleneck.
Common Failure Modes
Several mistakes show up repeatedly when developers work with object creation in Python.
Returning a value from __init__ raises TypeError. The method must return None.
Forgetting to call super().__init__() in a subclass is a different problem. The parent's initialization never runs, so attributes set there are missing. This is not an error at creation time; it surfaces later when code tries to access an attribute that was never set.
class Base: def __init__(self): self.ready = True class Child(Base): def __init__(self): # missing super().__init__() pass child = Child() print(child.ready) # AttributeError
Mutable default arguments are another classic issue. If a constructor uses a mutable default like def __init__(self, items=[]), every instance shares the same list. Dataclasses handle this correctly by generating field(default_factory=list), but a plain class requires you to write that logic yourself.
Choosing the Right Creation Strategy
The choice of creation strategy depends on what the object represents and how it is used.
| Approach | Best fit | Tradeoff |
|---|---|---|
Plain __init__ | Simple classes with straightforward setup | Manual boilerplate for repr and equality |
| Dataclass | Data containers with known fields | Generated methods may not match custom needs |
| Class method factory | Construction from external input | Extra indirection in the API |
__new__ override | Immutable types or cached instances | Easy to misuse; skips __init__ in some cases |
| Metaclass | Framework-level control over class creation | High complexity; rarely needed in apps |
A plain class with __init__ remains the right choice when the class has real behavior and only a few attributes. A dataclass is better when the class is mostly a structured container. A class method factory is appropriate when the source of the data varies, such as loading from a file, a database row, or an API response.
__new__ should be reserved for cases where allocation itself must change, such as returning a cached instance or implementing an immutable type. Metaclasses should be the last resort, used only when you need to control behavior at the class level rather than the instance level.