__init__ is the method Python calls automatically right after it creates a new object, and its job is to set up that object’s initial attributes. When you write User('Sam'), Python builds a blank User and then runs __init__(self, 'Sam'), where self is the new object and the code inside assigns its starting values. People call it the constructor, and functionally it acts like one, but strictly it is an initializer - the object already exists by the time __init__ runs. That distinction explains a lot of the confusion around it.
Table of contents
- What it looks like and when it runs
- self is the object being set up
- It initializes, it does not construct
- Default arguments and optional attributes
- Inheritance and super().init
- How this fits the rest of the stack
- FAQ
What it looks like and when it runs
class User:
def __init__(self, name, email):
self.name = name
self.email = email
u = User("Sam", "[email protected]")
u.name # 'Sam'
__init__ runs the instant you create an object. User("Sam", "[email protected]") creates a new User and immediately calls __init__ with those arguments. Inside, self.name = name and self.email = email store the values on the object.
You never call __init__ directly - Python calls it for you when you use the class name like a function. The double underscores mark it as a special method Python looks for and invokes automatically; these dunder methods hook into language behaviour, and __init__ is the one that hooks into object creation. Every time you see SomeClass(...), __init__ is what runs.
self is the object being set up
self is the first parameter of __init__ (and of every instance method), and it refers to the specific object being created:
a = User("Alice", "[email protected]") # inside __init__, self is a
b = User("Bob", "[email protected]") # inside __init__, self is b
When you create a, self inside __init__ is a; when you create b, self is b. That is how each object gets its own separate data - self.name = name sets the name on this particular object, not shared across all of them.
You do not pass self yourself. User("Alice", "[email protected]") looks like two arguments, but Python supplies self automatically as the first one. This is why __init__ is defined with self but called without it - Python fills that slot with the new object. Forgetting to include self in the definition is the single most common beginner error, and it produces a confusing argument-count error.
It initializes, it does not construct
The pedantic-but-useful distinction: __init__ is not strictly the constructor. The actual object creation happens in __new__, a separate method that builds the empty object. By the time __init__ runs, the object already exists - __init__ just fills it in.
# conceptually, Python does:
obj = User.__new__(User) # __new__ creates the blank object
obj.__init__("Sam", "[email protected]") # __init__ initializes it
For virtually all everyday code, you only ever write __init__ and never touch __new__ - the default __new__ does the right thing. So calling __init__ the constructor is a harmless shorthand, and most people do.
The reason the distinction matters occasionally: __init__ must return None. If you try to return a value from it, Python raises an error, because its job is to set up the already-created object, not to produce one. That constraint only makes sense once you know the object exists before __init__ runs.
Default arguments and optional attributes
__init__ is a normal method, so it takes default arguments - which is how you make some attributes optional at creation:
class User:
def __init__(self, name, email, active=True, roles=None):
self.name = name
self.email = email
self.active = active
self.roles = roles if roles is not None else []
active=True gives a sensible default; callers can override it or not. Note roles=None rather than roles=[] - this is the mutable default argument trap. A default list is created once and shared across every instance, so all your users would end up sharing one roles list. Using None as the sentinel and building a fresh list inside is the correct pattern.
This is worth internalizing because it bites hard in __init__ specifically: an object that mysteriously shares a list or dict with every other object of its class is almost always a mutable default in the initializer. None plus build-it-inside is the fix, and it belongs in every __init__ that has a collection attribute.
Inheritance and super().init
When a class inherits from another, its __init__ usually needs to call the parent’s __init__ so the parent’s setup still happens:
class Admin(User):
def __init__(self, name, email, level):
super().__init__(name, email) # run User's __init__ first
self.level = level # then add Admin's own attribute
super().__init__(name, email) calls the parent class’s initializer, so Admin objects get name and email set up by User before Admin adds level. Skip this call and the parent’s attributes never get initialized - your Admin would have a level but no name, a subtle bug that surfaces later.
The rule for subclass initializers: call super().__init__(...) with the arguments the parent expects, usually first thing, then do your own setup. Forgetting the super() call is a common source of half-initialized objects. When a subclassed object is missing attributes you know the parent sets, a missing super().__init__ is the first thing to check.
How this fits the rest of the stack
Clean object initialization - every instance getting its own state, no shared mutable defaults, parent setup properly chained - is the foundation of code you can extend without surprises. When those objects model the entities behind an API, a half-initialized instance is exactly the kind of bug that hides until a specific request path hits it in production. The RunxBuild hosting calculator lays out the service, database, storage, and bandwidth as separate line items, and the RunxBuild dashboard is where the team watches deploys, logs, and restarts as they happen.
Useful related references:
- Python Request Timeout: The Four Parameters a Working App Actually Sets
- AWS vs GCP: Pricing, Network, and When to Pick Each
- Change Python Version: The Three Tools That Make It Stop Hurting
- Python services on RunxBuild
FAQ
What does init do in Python?
__init__ runs automatically right after an object is created and initializes its attributes. When you write User('Sam'), Python creates the object and calls __init__(self, 'Sam'), where the code assigns the object’s starting values like self.name = 'Sam'. You never call it directly.
What is self in init?
self is the object being created. It is the first parameter of __init__ and every instance method, and Python passes it automatically - so User('Sam') supplies self for you. Assigning self.name = name stores data on that specific object, keeping each instance’s data separate.
Is init a constructor?
Functionally it acts like one, but strictly it is an initializer. The object is actually created by __new__ before __init__ runs, so __init__ fills in an already-existing object rather than building it. This is why __init__ must return None and cannot return a value.
Why does my init share a list between objects?
Because you used a mutable default argument like def __init__(self, roles=[]). That list is created once and shared across all instances. Use roles=None and build a fresh list inside - self.roles = roles if roles is not None else [] - so each object gets its own.
What does super().init do?
It calls the parent class’s __init__ so the parent’s attribute setup still runs in a subclass. Write super().__init__(...) with the arguments the parent expects, usually first, then add the subclass’s own attributes. Skipping it leaves the parent’s attributes uninitialized.