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 mypkg — mypkg.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,PYTHONPATHentries, and the standard library and site-packages locations — for a directory namedstore. - 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__.pyin that module’s namespace, and stores the result insys.modules["store"]. - It repeats the process for
store.models, searching withinstore‘s__path__, and again forstore.models.product, searching withinmodels‘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 asstore.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__.pyto define a clean public API with__all__and re-exports, so callers can writeimport storeinstead of reaching intostore.models.productdirectly. - Keep
__init__.pyitself 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_casewith no spaces or hyphens, since hyphens aren’t valid in Python identifiers.
Practice Exercises
- Create a package named
shapeswith two submodules,circle.pyandsquare.py, each exposing anareafunction. Write an__init__.pythat re-exports both so a script can dofrom shapes import area_circle, area_square(hint: import and alias them inside__init__.py). - Add a subpackage
shapes/three_dcontainingsphere.py, and havesphere.pyuse a relative import to reach a helper function defined inshapes/utils.py. Confirm the dotted pathshapes.three_d.sphereresolves 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 theImportError, 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__.pyruns once per process and its names become attributes of the package; use it to build a clean public API.- Dotted imports like
store.models.productare resolved step by step throughsys.path, cached insys.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.
