Q1If a class C has __init__(self, name), what happens when you call C() with no arguments?
The Constructor __init__ and the Destructor __del__
A practical guide to Python's __init__ and __del__. Covers required attributes, default values, and the cleanup method called on del and program exit — all with diagrams.
Last time you got the basics of classes and instances, self, and the simplest form of __init__. This time we go deeper, using __init__ to make sure every instance is born in a valid state. We'll cover required arguments that fail loudly when you forget them, default arguments for optional fields, and the matching destructor __del__.
What Happens Without __init__
If your class doesn't initialize attributes properly, you can end up with instances missing those attributes, and the moment you call a method that touches them, you get hit with AttributeError.
In the code below, User only has a set_name method — no initializer. Calling display() before set_name blows up because self.name doesn't exist yet.
class User:
def set_name(self, name):
self.name = name
def display(self):
print(self.name)
user = User()
user.display() # AttributeError: no name attribute
# self.name doesn't exist until you call user.set_name("Alice") first
Use __init__ to Enforce Required Attributes
__init__ is the special method Python automatically calls when an instance is created. It's also called the constructor. Names wrapped in double underscores are dunder methods — Python's convention for methods it calls at specific moments.
By declaring __init__(self, name, email) with required parameters, forgetting to pass either one halts the program with TypeError. You never end up with an incomplete User(), so all the code after that can rely on "name and email are definitely set."
User("Alice", "alice@x.com") makes Python build an empty instance, run __init__, fill the attributes, and return the finished instance. Forget an argument and __init__ itself raises TypeError.class User:
def __init__(self, name, email): # required parameters
print("__init__ called")
self.name = name
self.email = email
def display(self):
print(f"{self.name} <{self.email}>")
user = User("Alice", "alice@example.com") # sets user.name to "Alice" and user.email to "alice@example.com"
# __init__ called
user.display()
# Alice <alice@example.com>
# Forgetting an argument raises TypeError
# User() # → TypeError: missing 2 required positional arguments
Default Arguments for Optional Fields
__init__ is a regular function, so you can give it default arguments. Write something like age=0, and the caller can either skip it (and get the default) or pass a value. This lets you split your design into "required attributes" and "optional attributes."
class User:
def __init__(self, name, email, age=0): # age is optional
self.name = name
self.email = email
self.age = age
alice = User("Alice", "alice@example.com") # age omitted → 0
bob = User("Bob", "bob@example.com", 30) # age provided
print(alice.age) # 0
print(bob.age) # 30
Don't Put list / dict Directly in a Default Argument
Writing a mutable object as a default — like def __init__(self, tags=[]) — runs into a notorious trap: all instances end up sharing the same list. We cover this in detail in Function arguments and mutability. It's the same trap inside __init__, so the standard fix is tags=None plus if tags is None: tags = [] inside the function.
__del__ — Cleanup When an Instance Goes Away
The dunder method paired with __init__ is __del__ (the destructor). While __init__ runs when an instance is created, __del__ runs the moment an instance is destroyed, called automatically by Python.
There are two main ways destruction happens:
- You delete it explicitly with del variable_name
- Memory is reclaimed when the program ends (any leftover instances are destroyed in turn)
Because of the second case, __del__ runs on program exit even if you don't trigger it yourself.
del or program exit.__del__ Doesn't Run in Browser Execution
This site's browser-side execution is built on MicroPython, which by design doesn't call __del__. The code samples in this section are for reading and understanding the behavior. Run them in CPython on your local terminal if you want to actually see __del__ fire.
class User:
def __init__(self, name):
print(f"created {name}")
self.name = name
def __del__(self):
print(f"destroyed {self.name}")
alice = User("Alice") # created Alice
bob = User("Bob") # created Bob
del alice # destroyed Alice ← fires explicitly here
print("about to exit")
# about to exit
# destroyed Bob ← fires automatically on program exit
A Common Cleanup Pattern — Removing Yourself from a List
A typical use of __del__ is cleanup that pairs with creation — like "remove myself from a list the class keeps." In the example below, the User class adds the name to the class variable users on creation and removes it on destruction.
class User:
users = [] # class variable: users currently alive
def __init__(self, name):
self.name = name
User.users.append(name) # register on creation
def __del__(self):
User.users.remove(self.name) # remove on destruction
alice = User("Alice")
bob = User("Bob")
print(User.users) # ['Alice', 'Bob']
del alice
print(User.users) # ['Bob']
Direct __del__ Use Is Rare in Practice
When __del__ runs is up to Python's garbage collector, so you can't reliably predict the timing. For deterministic release of files or network connections, use with statements (context managers) instead of __del__.
This time we covered using __init__ practically — enforcing required attributes and default arguments — and met the matching __del__ with its typical pattern. You can now control "the lifespan of an instance from creation to destruction" from Python's side.
Next up, we'll dig into the truth behind self.x = ... that you've been writing without much thought. We'll sort out the two kinds of variables in a class — class variables and instance variables — and walk through what's actually being created when you write self.x = ..., watching the references with id().
Knowledge Check
Answer each question one by one.
Q2Which is the correct timing for __del__ to run?
Q3Why is writing def __init__(self, tags=[]) considered problematic?