Learn by reading through in order

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

Confirm that calling a method before initializing its attributes triggers an AttributeError. Run it and watch the error happen.

Python Editor

Run code to see output

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."

How __init__ Hooks into Instance Creation
User('Alice', 'alice@x.com')build emptyinstancecall__init__userattributes set
Calling 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

Write __init__ with two required parameters and try both creation, printing, and what happens when you forget one.

① Define class User:. Inside __init__(self, name, email), write self.name = name and self.email = email.

② Define def display(self): that prints f"{self.name} <{self.email}>".

③ Build alice = User("Alice", "alice@example.com") and call alice.display(). Confirm it prints Alice <alice@example.com>.

④ Then try bob = User("Bob")forgetting the email — and confirm it stops with TypeError (wrap it in try / except to inspect the message).

(If you run it correctly, an explanation will appear.)

Python Editor

Run code to see output

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.

Write a default argument as an empty list directly and watch what goes wrong.

① Define class User:. Inside __init__(self, name, tags=[]), write self.name = name and self.tags = tags (intentionally putting tags=[] directly).

② Build alice = User("Alice") and bob = User("Bob").

③ Run alice.tags.append("vip"), then print(alice.tags) and print(bob.tags) — confirm that bob ends up with "vip" too.

④ Finally, print(alice.tags is bob.tags) to confirm they share the same list.

Python Editor

Run code to see output

__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.

When __init__ and __del__ Run
call User('Alice')__init__firesuseralivedel user/ program exits__del__firesmemoryreleasedcreatereadydestroycleanup
Top row is the __init__ flow — it runs when the instance is born, called automatically by Python. Bottom row is the __del__ flow — it runs when the instance dies, whether by 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().

QUIZ

Knowledge Check

Answer each question one by one.

Q1If a class C has __init__(self, name), what happens when you call C() with no arguments?

Q2Which is the correct timing for __del__ to run?

Q3Why is writing def __init__(self, tags=[]) considered problematic?