If you’ve ever dug into a popular Python library like requests or pandas, you’ve likely stumbled upon a file named __init__.py. Sometimes it’s empty, sometimes it’s packed with imports, but it’s almost always there.

When I first started with Python, I treated __init__.py as a magical ritual—a file I had to create to make my imports work, without really understanding why.

Turns out, __init__.py isn’t just boilerplate. It’s the gateway to your package. It allows you to transform a messy directory of scripts into a easy-to-use library.

Let’s look at a scenario to see how we can use __init__.py to refactor a messy project into something more professional that you love to show off.

Scenario: Building a Notification System

Imagine you’re building a backend service that needs to send notifications via different channels: Email, SMS, and Push Notifications.

A typical beginner’s project structure might look like this:

1
2
3
4
5
6
notifications/
    __init__.py
    email_service.py
    sms_service.py
    push_service.py
main.py

Inside email_service.py, you might have:

1
2
3
4
# notifications/email_service.py
class EmailSender:
    def send(self, message):
        print(f"Sending Email: {message}")

And similarly for SMS and Push.

The Problem: Clunky, “Deep” Imports

Now, when you want to use these classes in your main.py, you must know exactly which file contains which class. Your imports become long and expose internal file structure:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# main.py (Without proper __init__.py usage)

from notifications.email_service import EmailSender
from notifications.sms_service import SmsSender
from notifications.push_service import PushSender

def notify_user(msg):
    # Usage is fine, but the imports are ugly
    sender = EmailSender()
    sender.send(msg)

I mean it is okay, but it’s annoying.

  1. Verbose: You have to type notifications.filename every time.
  2. Brittle: If you rename email_service.py to email.py later, you break every file that imports it.

The Solution: Expose Classes in __init__.py

We can use __init__.py to create a shortcut.

Let’s modify notifications/__init__.py to import the classes inside the package, so they are available at the top level.

1
2
3
4
5
6
7
8
9
# notifications/__init__.py

# Use relative import (.) to import from the current directory
from .email_service import EmailSender
from .sms_service import SmsSender
from .push_service import PushSender

# Optional: Control what gets imported with 'from notifications import *'
__all__ = ['EmailSender', 'SmsSender', 'PushSender']

The Result: Clean, “Flat” Imports

Now, look at how beautiful and intuitive main.py becomes. The user doesn’t need to know about email_service.py or sms_service.py. They just import from notifications.

1
2
3
4
5
6
7
8
# main.py (With proper __init__.py usage)

# CLEAN! No need to know internal file names
from notifications import EmailSender, SmsSender

def notify_user(msg):
    sender = EmailSender()
    sender.send(msg)

Why is this better?

  • Abstraction: The user of your package doesn’t need to know that EmailSender lives in a file called email_service.py. It just lives in the notifications package.
  • Refactoring Safety: You can move EmailSender to backend/legacy/email.py later, and as long as you update the import in __init__.py, your users won’t notice a thing. Their code doesn’t break.

Advanced Tip: Lazy Loading

One downside of importing everything in __init__.py is that it loads all modules when you import the package. If PushSender requires a heavy library (like a specific SDK) that takes time to load, it might slow down your app even if you only wanted to send an SMS.

You can get fancy with “Lazy Loading” inside __init__.py if performance is critical using __getattr__, but for 99% of projects, the pattern above is the gold standard.

Conclusion

The __init__.py file is a tool for encapsulation. It lets you hide the messy details of your file structure and present a clean, flat interface to the world.

Next time you create a folder in Python, don’t just leave __init__.py empty. Ask yourself: “How do I want other developers to import my code?”

Cheers!