Python Packages

A Python package is simply a directory of modules that lets you organize related code into a namespace you can import with dotted names, like store.models.product. Once a project grows past a handful of functions, splitting code into a single flat module becomes unwieldy; packages let you group related modules together, control what’s public, and reuse the same names (like utils) in different parts of a large codebase without collisions. Every major library you’ve used — requests, numpy, django — is distributed as a package. This lesson covers how packages are structured, how imports actually resolve under the hood, and the mistakes that trip up almost everyone the first time they build one.

Overview: What Is a Package?

A single .py file is a module. A package is a directory containing modules (and possibly further subdirectories, called subpackages) that Python’s import system recognizes as a single importable unit. Historically, a directory became a "regular package" by containing a special file named __init__.py. That file runs exactly once, the first time the package (or anything inside it) is imported, and whatever names it defines or imports become attributes of the package itself.

Since Python 3.3, a directory without an __init__.py can still be imported as a so-called namespace package (defined in PEP 420). This exists mainly so that a single logical package can be split across multiple installed distributions on disk. For ordinary application and library code, you should almost always create an explicit __init__.py — even an empty one — because it makes the package boundary obvious, lets you control the public API, and avoids subtle bugs where two unrelated directories on sys.path accidentally merge into one namespace package.

Internally, when Python imports a package, it creates a real module object for it (yes, a package is a module object) with an extra attribute called __path__, which is a list of directories the import system searches when you ask for a submodule. Submodules and subpackages are attributes of their parent once imported — that’s why import store alone does not automatically give you store.models; a submodule only becomes an attribute of its parent after it (or something that imports it) has actually been imported.

Syntax

A typical package layout looks like this: a top-level directory (say store) containing an __init__.py, one or more sibling modules such as utils.py, and a subdirectory (say models) that is itself a package because it has its own __init__.py, containing product.py. The table below shows the import forms you’ll use against a layout like that.

Statement What it binds
import store Binds the name store; access members as store.thing
import store.models.product Binds store; access the deep module via store.models.product
from store import utils Binds the submodule name utils directly
from store.models.product import Product Binds just the name Product
from . import utils Relative import: utils from the current package
from .. import utils Relative import: utils from the parent package
from .product import Product Relative import from a sibling module

Relative imports (the ones starting with a dot) only work inside a package’s own modules, never in a top-level script you run directly — more on that in Common Mistakes.

Examples

Consider a small package named mypkg with two submodules and an __init__.py that re-exports selected names.

mypkg/greetings.py

def hello(name: str) -> str:
    return f"Hello, {name}!"

mypkg/farewells.py

def goodbye(name: str) -> str:
    return f"Goodbye, {name}!"

mypkg/__init__.py

from .greetings import hello
from .farewells import goodbye

__all__ = ["hello"]

main.py

from mypkg import *

print(hello("Sam"))
try:
    print(goodbye("Sam"))
except NameError as exc:
    print(f"Error: {exc}")

Output:

Hello, Sam!
Error: name 'goodbye' is not defined

Both hello and goodbye are imported into __init__.py and both are real attributes of mypkgmypkg.goodbye("Sam") would work fine. But __all__ controls what a wildcard import (from mypkg import *) pulls into the caller’s namespace. Since __all__ only lists "hello", the wildcard import skips goodbye, and using it raises a NameError. This is exactly why relying on __all__ to define a package’s public surface is a common and useful pattern.

Now a more realistic example with a subpackage and a relative import that reaches into a sibling module two levels up.

store/utils.py

def format_price(cents: int) -> str:
    return f"${cents / 100:.2f}"

store/models/product.py

from dataclasses import dataclass

from ..utils import format_price


@dataclass
class Product:
    name: str
    price_cents: int

    def __repr__(self) -> str:
        return f"{self.name} - {format_price(self.price_cents)}"

store/models/__init__.py

from .product import Product

__all__ = ["Product"]

store/__init__.py

from .models.product import Product
from .utils import format_price

__all__ = ["Product", "format_price"]

main2.py

import store

widget = store.Product("Widget", 1999)
gadget = store.Product("Gadget", 4999)

for item in (widget, gadget):
    print(item)

Output:

Widget - $19.99
Gadget - $49.99

Here product.py, which lives two directories below the top-level package, reaches back up to its grandparent’s utils module using from ..utils import format_price. Each __init__.py re-exports the useful names from deeper in the tree, so the caller only ever needs import store and never has to know about the internal models subpackage — a very common way to design a clean public API for a package.

Under the Hood: How Imports Actually Happen

When you write import store.models.product, Python does the following, roughly in order:

  • It checks sys.modules (a cache dictionary of already-imported modules) for the key "store". If present, it reuses that module object instead of re-running any code.
  • If not cached, it searches sys.path — a list of directories that includes the script’s own directory, PYTHONPATH entries, and the standard library and site-packages locations — for a directory named store.
  • Finding store/__init__.py (or, for a namespace package, just the bare directory), it creates a module object, sets its __path__ to point at that directory, executes __init__.py in that module’s namespace, and stores the result in sys.modules["store"].
  • It repeats the process for store.models, searching within store‘s __path__, and again for store.models.product, searching within models‘s __path__.
  • Each fully imported submodule is also set as an attribute on its parent package object, which is why, after importing store.models.product, you can reach it as store.models.product.

Because everything is cached in sys.modules, a package’s __init__.py runs only once per process no matter how many places import it — every subsequent import store anywhere in your program is effectively a dictionary lookup, not a re-execution.

Common Mistakes

1. Circular imports

If package_a/__init__.py does from package_b import thing at the top level, and package_b/__init__.py does from package_a import other_thing at the top level, importing either package first can fail with an error like ImportError: cannot import name 'other_thing' from partially initialized module 'package_a'. This happens because Python starts executing package_a, reaches the import of package_b, starts executing that, and hits its import of package_a back — but package_a is still mid-execution and doesn’t yet have other_thing defined. Fix it by moving the import inside the function that needs it (a "local import"), or by restructuring so shared code lives in a third module both packages depend on instead of depending on each other.

2. Running a package submodule as a script

If store/models/product.py uses a relative import like from ..utils import format_price, running it directly with python store/models/product.py fails with ImportError: attempted relative import with no known parent package. Run as a script, the file is loaded as the top-level module __main__ with no parent package, so Python has nothing to resolve .. against. The fix is to run it as part of the package using the -m flag from the project root, e.g. python -m store.models.product, or to only invoke that logic through a normal top-level entry point that imports the package properly.

3. Assuming a missing __init__.py is always harmless

Because namespace packages let a directory work without __init__.py, it’s tempting to just skip it. But if you have two different directories named, say, utils on sys.path (perhaps one from your project and one from an installed dependency) and neither has an __init__.py, Python may silently merge them into a single namespace package, mixing modules from both in ways that are very hard to debug. Adding an explicit, even empty, __init__.py to your own packages removes this ambiguity entirely.

Best Practices

  • Give every package you author an explicit __init__.py, even if it’s empty, so its boundary is unambiguous.
  • Use __init__.py to define a clean public API with __all__ and re-exports, so callers can write import store instead of reaching into store.models.product directly.
  • Keep __init__.py itself light — imports and constants, not heavy logic — since it runs on every import of the package.
  • Prefer absolute imports (from store.utils import format_price) for anything outside the current package, and relative imports only for close siblings within the same package.
  • Avoid wildcard imports (from module import *) in application code; they hide where names come from and can silently shadow existing names.
  • Keep the dependency graph between subpackages a directed acyclic graph — if two subpackages need each other’s types, that’s a sign shared code should move to a common lower-level module.
  • Name packages and modules in short, lowercase snake_case with no spaces or hyphens, since hyphens aren’t valid in Python identifiers.

Practice Exercises

  • Create a package named shapes with two submodules, circle.py and square.py, each exposing an area function. Write an __init__.py that re-exports both so a script can do from shapes import area_circle, area_square (hint: import and alias them inside __init__.py).
  • Add a subpackage shapes/three_d containing sphere.py, and have sphere.py use a relative import to reach a helper function defined in shapes/utils.py. Confirm the dotted path shapes.three_d.sphere resolves correctly.
  • Deliberately create a circular import between two small packages (each importing a name from the other at the top level of __init__.py), observe the ImportError, then fix it by moving one of the imports inside a function.

Summary

  • A package is a directory of modules, made importable by an __init__.py (regular package) or, since Python 3.3, without one (implicit namespace package).
  • __init__.py runs once per process and its names become attributes of the package; use it to build a clean public API.
  • Dotted imports like store.models.product are resolved step by step through sys.path, cached in sys.modules, and each submodule becomes an attribute of its parent.
  • Relative imports (from . import x) only work inside a package’s own modules, never when a file is run directly as a script.
  • __all__ controls what a wildcard import exposes, not what’s actually accessible on the module.
  • Circular imports between packages are avoided by keeping dependencies one-directional or moving shared imports inside functions.