Files
2026-05-30 16:02:33 -04:00

4.5 KiB

title, created, tags, category, status, up, related, source, author, published
title created tags category status up related source author published
What is __init__.py? 2026-05-22
python
python_note
packaging
reference
python_note reference project/index
poetry_guide
poetry_project_ideas

What is __init__.py?

__init__.py is the file that tells Python "this folder is a package". When you put it inside a directory, that directory becomes importable like a module.

The Core Purpose

Without __init__.py (in older Python, ≤3.2), a folder was just a folder — Python could not import from it. With it, the folder becomes a package you can do this with:

from recipe_scraper.scraper import fetch_recipe

Here, recipe_scraper/ is a package because it contains __init__.py.

Note: Since Python 3.3, "namespace packages" allow imports without __init__.py, but regular packages still use it because it gives you more control (init code, explicit exports, IDE/tooling support).


What It Does in Practice

1. Marks the folder as a package

Even an empty __init__.py is meaningful. It signals to Python:

"Treat this directory as something you can import from."

2. Runs initialization code

Anything inside __init__.py runs the first time the package is imported. This is useful for:

  • Setting up logging
  • Loading config
  • Registering plugins
# recipe_scraper/__init__.py
import logging
logging.getLogger(__name__).addHandler(logging.NullHandler())

3. Controls the package's public API

You can re-export things so users don't need to know the internal file layout:

# recipe_scraper/__init__.py
from .scraper import fetch_recipe
from .parser import parse_recipe
from .db import save_recipe

__all__ = ["fetch_recipe", "parse_recipe", "save_recipe"]

Now consumers can write the short form:

from recipe_scraper import fetch_recipe       # clean
# instead of:
from recipe_scraper.scraper import fetch_recipe  # verbose

4. Defines package metadata

A common pattern is exposing a version string:

# recipe_scraper/__init__.py
__version__ = "0.1.0"

How It Fits the Recipe Scraper Project

Given the folder structure from Recipe Web Scraper - Getting Started:

recipe-scraper/
├── pyproject.toml
└── recipe_scraper/
    ├── __init__.py          ← makes this a package
    ├── scraper.py
    ├── parser.py
    ├── db.py
    └── cli.py

The __init__.py here lets you:

  1. Run poetry run python -m recipe_scraper.cli — only works because recipe_scraper is a package.
  2. Import cleanly from anywhere in the project:
    from recipe_scraper.db import Recipe
    
  3. Optionally expose a tidy top-level API:
    # recipe_scraper/__init__.py
    from .scraper import scrape_url
    from .db import Recipe, init_db
    
    __version__ = "0.1.0"
    __all__ = ["scrape_url", "Recipe", "init_db"]
    
    So users of your library can just do:
    import recipe_scraper
    recipe_scraper.scrape_url("https://...")
    

When to Leave It Empty vs. Fill It

Situation What to put in __init__.py
Internal-only package, no public API Empty file
You want a clean import surface Re-exports + __all__
Library shipped to PyPI __version__, re-exports, maybe logging setup
One-time setup needed (config, env) Initialization code at the top

Rule of thumb: start with an empty __init__.py. Only add code when you have a concrete reason — re-exports, version, or setup. Don't put heavy logic in __init__.py; it runs on every import.


Common Gotchas

  • Heavy imports slow everything down. If __init__.py imports a big library (e.g., pandas), every import recipe_scraper.anything pays that cost. Keep it light.
  • Circular imports often start in __init__.py. If __init__.py imports from scraper.py, and scraper.py imports from the package root, you get a cycle. Use lazy imports or restructure.
  • Tests need it too. A tests/ folder usually has an empty __init__.py so pytest can discover test modules consistently (though pytest's rootdir config can avoid this).

TL;DR

  • __init__.py = "this folder is a Python package".
  • Can be empty — just its presence matters.
  • Use it to re-export the public API, set __version__, or run small setup.
  • Keep it light: every import of the package runs it.