W3docs

Python Packages and the Import System

Learn how Python packages work: create a package with __init__.py, use absolute and relative imports, expose a clean public API, and avoid common pitfalls.

A package is a directory of Python modules that you treat as a single importable unit. Where a module is one .py file, a package is a folder — potentially containing many modules and sub-packages — that Python's import system can navigate like a tree. This chapter explains how to create packages, control what they expose, write absolute and relative imports correctly, and avoid the gotchas that trip up beginners.

Modules vs. Packages — the Key Difference

A module is a single .py file:

greetings.py        ← module

A package is a directory that contains at least one special file called __init__.py:

greetings/          ← package
    __init__.py
    english.py
    spanish.py

Both are imported with the same import keyword, but a package gives you a namespace hierarchy: greetings.english and greetings.spanish are separate modules, yet they share the greetings namespace.

When to use a module vs. a package:

SituationUse
A small, self-contained utilityModule (one .py file)
Multiple related modules you want under one namePackage (a directory)
A library you intend to distribute on PyPIPackage (with src/ layout)

The __init__.py File

__init__.py is what makes a directory a package. Python executes it the first time the package (or any of its modules) is imported. It can be empty, or it can:

  • Import names from sub-modules to make them available at the package level
  • Run package-level initialization (logging setup, version checks, etc.)
  • Define __all__ to control from package import *

Minimal package layout

myapp/
    __init__.py       ← can be empty
    utils.py
    config.py
# myapp/__init__.py  (empty — that is fine)
# myapp/utils.py
def greet(name):
    return f"Hello, {name}!"

Import from outside the package:

from myapp.utils import greet

print(greet("Alice"))   # Hello, Alice!

Surfacing names at the package level

A common pattern is to import the most-used names into __init__.py so callers can write from myapp import greet instead of from myapp.utils import greet.

# myapp/__init__.py
from .utils import greet
from .config import MAX_RETRIES

Now both names are available directly on the package:

import myapp

print(myapp.greet("Bob"))   # Hello, Bob!
print(myapp.MAX_RETRIES)    # whatever config.py defines

Absolute Imports

An absolute import always starts from the top-level package or from a directory on sys.path. It never depends on where the file doing the importing lives.

project/
    myapp/
        __init__.py
        utils.py
        services/
            __init__.py
            email.py

Inside email.py, an absolute import looks like this:

# myapp/services/email.py
from myapp.utils import greet   # absolute — starts from the top-level package

def send_welcome(user):
    message = greet(user)
    print(f"Sending: {message}")

Absolute imports are the default and the recommended style (PEP 8). They are unambiguous regardless of how Python is invoked.

Relative Imports

A relative import uses dots (.) to navigate the package tree relative to the current file's location.

  • . means the current package
  • .. means the parent package
  • ... means the grandparent package, and so on
# myapp/services/email.py

# One dot — import from myapp.services (same directory)
from . import sms

# Two dots — import from myapp (parent directory)
from ..utils import greet

When to use relative imports

Relative imports are useful inside a package when you want to make it clear that greet comes from this package and not from some external library of the same name. They also make refactoring easier because the imports move with the package if you rename the top-level directory.

Gotcha: relative imports only work inside a package. If you run python myapp/utils.py directly, Python treats it as a standalone script, not part of a package, and a relative import raises ImportError: attempted relative import with no known parent package. Run the package with python -m myapp.utils instead.

# Wrong — runs utils.py as a script, breaking relative imports
$ python myapp/utils.py

# Right — runs utils.py as part of the myapp package
$ python -m myapp.utils

Controlling the Public API with __all__

__all__ is a list of names that from package import * exports. It also documents what the package considers public.

# myapp/__init__.py
from .utils import greet, farewell
from .config import MAX_RETRIES

__all__ = ["greet", "MAX_RETRIES"]   # farewell is intentionally not exported

Now from myapp import * brings in only greet and MAX_RETRIES. The farewell function still exists; it is just not part of the advertised public interface. Names prefixed with a single underscore (_private) are also excluded from import * by convention.

Nested Packages (Sub-packages)

Packages can contain other packages. Each sub-directory needs its own __init__.py.

analytics/
    __init__.py
    reports/
        __init__.py
        daily.py
        weekly.py
    charts/
        __init__.py
        bar.py
        pie.py

Import a deeply nested module with the full dotted path:

from analytics.reports.daily import generate_report
from analytics.charts.bar import BarChart

Or, if analytics/__init__.py surfaces them:

# analytics/__init__.py
from .reports.daily import generate_report
# caller
from analytics import generate_report

How deep should you go?

A package three or four levels deep is usually a sign it has grown too large and should be split into separate top-level packages (installable separately). For most projects, two levels (package.module) is enough.

A Worked Example: Building a geometry Package

Let's build a small but realistic package step by step.

Directory layout

geometry/
    __init__.py
    shapes.py
    conversions.py

shapes.py

# geometry/shapes.py
import math

def circle_area(radius):
    """Return the area of a circle with the given radius."""
    if radius < 0:
        raise ValueError("radius must be non-negative")
    return math.pi * radius ** 2

def rectangle_area(width, height):
    """Return the area of a rectangle."""
    return width * height

def triangle_area(base, height):
    """Return the area of a triangle."""
    return 0.5 * base * height

conversions.py

# geometry/conversions.py

def degrees_to_radians(degrees):
    """Convert degrees to radians."""
    import math
    return degrees * math.pi / 180

def radians_to_degrees(radians):
    """Convert radians to degrees."""
    import math
    return radians * 180 / math.pi

__init__.py — expose the key names

# geometry/__init__.py
"""
geometry — simple 2-D geometry utilities.

Public API:
    circle_area(radius) -> float
    rectangle_area(width, height) -> float
    triangle_area(base, height) -> float
    degrees_to_radians(degrees) -> float
    radians_to_degrees(radians) -> float
"""

from .shapes import circle_area, rectangle_area, triangle_area
from .conversions import degrees_to_radians, radians_to_degrees

__all__ = [
    "circle_area",
    "rectangle_area",
    "triangle_area",
    "degrees_to_radians",
    "radians_to_degrees",
]

Using the package

# main.py (sits next to the geometry/ directory)
import geometry

print(geometry.circle_area(5))           # 78.53981633974483
print(geometry.rectangle_area(4, 6))     # 24
print(geometry.degrees_to_radians(90))   # 1.5707963267948966

Or with selective imports:

from geometry import circle_area, degrees_to_radians

print(circle_area(3))              # 28.274333882308138
print(degrees_to_radians(180))     # 3.141592653589793

Namespace Packages (Python 3.3+)

Since Python 3.3, a directory without __init__.py is a namespace package. Python merges all directories of the same name on sys.path into one logical package. This is mainly useful for large organizations that split a single package across multiple repositories or installation directories.

For everyday development, always include __init__.py. It makes your intent unambiguous and works in all Python versions.

How Python Finds Packages

When you write import geometry, Python searches sys.path in order:

  1. The directory of the running script (or the current directory in interactive mode)
  2. Directories in the PYTHONPATH environment variable
  3. The standard library directories
  4. The site-packages directory (where pip-installed packages live)
import sys
print(sys.path)

The package directory must be directly inside one of these locations. If geometry/ is in /home/alice/projects/, Python will not find it unless /home/alice/projects/ is on sys.path.

Tip: use a virtual environment and install your package in development mode (pip install -e .) so Python always finds it without manual sys.path manipulation.

Distributing a Package

To share a package with others (or install it with pip), you need a pyproject.toml at the project root:

my_project/
    pyproject.toml    ← build metadata
    src/
        geometry/
            __init__.py
            shapes.py
            conversions.py

A minimal pyproject.toml:

[build-system]
requires = ["setuptools>=68", "wheel"]
build-backend = "setuptools.backends.legacy:build"

[project]
name = "geometry"
version = "0.1.0"
description = "Simple 2-D geometry utilities"
requires-python = ">=3.9"

Install locally in editable mode during development:

pip install -e .

Now import geometry works anywhere in your virtual environment, regardless of your current directory.

Common Pitfalls

Missing __init__.py

If you forget __init__.py, Python 3 treats the directory as a namespace package (which usually still works), but Python 2 ignores it entirely. Be explicit: always add __init__.py.

Naming a package the same as a stdlib module

Avoid names like math/, json/, os/, email/. Python may import your package instead of the standard library one, breaking unrelated code.

Running a package module as a script

As noted above, running python myapp/services/email.py directly breaks relative imports. Use python -m myapp.services.email instead.

Circular imports between modules in the same package

If shapes.py imports from conversions.py and conversions.py imports from shapes.py, you have a circular import. Symptoms include ImportError or names that unexpectedly appear as None. The fix is usually to move the shared logic into a third module, or to delay the import to inside a function body.

# Delayed import — breaks the cycle at module load time
def some_function():
    from .shapes import circle_area   # imported only when the function is called
    ...

ImportError when using relative imports outside a package

# Will raise: ImportError: attempted relative import with no known parent package
# if run as:  python myapp/utils.py

from . import config   # relative import inside utils.py

Run it as python -m myapp.utils or restructure so the entry point is a separate script that imports the package.

Summary

ConceptOne-liner
PackageA directory with __init__.py containing modules
__init__.pyMakes a directory a package; runs on first import
Absolute importfrom myapp.utils import greet — always from the root
Relative importfrom ..utils import greet — relative to current file
__all__Lists names exported by from package import *
Namespace packageDirectory without __init__.py; Python 3.3+ only
Editable installpip install -e . — package found anywhere in the venv

See Python Modules for single-file code organization, Python pip for installing third-party packages, and Python Virtual Environments for keeping project dependencies isolated.

Practice

Practice
What makes a directory a Python package (in Python versions before 3.3)?
What makes a directory a Python package (in Python versions before 3.3)?
Was this page helpful?