Back to Blog
Python

Python Package Structure: Layout, Imports, and Packaging

A practical guide to Python package structure: directory layout, __init__.py files, import resolution, and preparing packages for distribution.

pythonmodule organizationrelative importspyproject.tomlnamespace packages
A visual diagram of a Python package directory tree showing modules, __init__.py, and import connections.

Python package structure determines how imports resolve, how the code can be tested, and how easily it can be published to PyPI. Getting the layout right from the start saves time when the project grows.

What a Python Package Actually Is

A conventional package is a directory that contains an __init__.py file. That file marks the directory as a Python package and lets you use dotted module names such as import mypackage.module_a. Without such a file, a directory can still be imported as a namespace package in Python 3 if its parent directory is on sys.path, but most projects use __init__.py to make a directory an importable unit.

The __init__.py file can be empty, but it often contains package-level imports or defines __all__. When a package is first imported into a Python process, Python executes __init__.py once (unless the package is explicitly reloaded), so avoid putting expensive or side-effect-heavy work there.

The Minimal Package Layout

A typical package structure looks like this:

mypackage/ ├── __init__.py ├── module_a.py └── subpackage/ ├── __init__.py └── module_b.py

In a conventional package, every directory that should be importable must contain its own __init__.py. The top-level __init__.py makes mypackage importable; the one inside subpackage makes mypackage.subpackage importable. Modules are just .py files that live inside these directories.

Here is a minimal module_a.py:

def useful_function(): return "result"

And module_b.py in the subpackage:

def helper(): return "helper"

With this layout, you can import either module from anywhere in your codebase, as long as the parent directory of mypackage is on sys.path.

How Imports Resolve Inside a Package

When you import a module from a package, Python uses the package's __path__ to find the module. Two styles of import statements work inside a package:

Absolute imports use the full package path from the top-level package name:

from mypackage.module_a import useful_function from mypackage.subpackage.module_b import helper

Relative imports use dots to refer to the current or parent package:

from .module_a import useful_function from .subpackage.module_b import helper

A single dot refers to the current package, two dots refer to the parent package, and so on. Relative imports are useful because they let you rename the top-level package without rewriting every import statement inside it.

Choosing Between Absolute and Relative Imports

The choice depends on how the package will be used and how much flexibility you need.

Use absolute imports when the package is installed as a dependency and you want the imports to be explicit and easy to grep. Absolute imports also work when the package is run as a script, as long as the top-level package is on sys.path.

Use relative imports when you want the package to remain self-contained and movable. If you later change the top-level package name, relative imports continue to work without modification. However, relative imports only work when the module is imported as part of a package, not when it is executed directly as a script.

A common rule is to use relative imports for internal references between modules within the same package, and absolute imports for references to external packages or to the top-level package from outside. This keeps the internal structure decoupled from the package name.

What init.py Should and Should Not Do

The __init__.py file is the entry point for the package. It can re-export names to provide a clean public API. For example:

# mypackage/__init__.py from .module_a import useful_function __all__ = ["useful_function"]

Now from mypackage import useful_function works, and from mypackage import * imports only the names in __all__. This is a common pattern for hiding internal implementation details.

Avoid placing heavy logic in __init__.py. It runs when the package is imported, so expensive imports or side effects there can slow down any code that imports the package. Keep it to re-exports, version constants, or simple package-level configuration.

Avoiding Circular Imports and Import Errors

Circular imports happen when two modules import each other at the top level. For example:

# module_a.py from .module_b import helper def useful_function(): return helper()
# module_b.py from .module_a import useful_function def helper(): return useful_function()

Importing mypackage.module_a triggers an import of module_b, which in turn tries to import module_a again. Since module_a is still being initialized, Python raises an ImportError.

Fix this by moving one of the imports inside a function or method, so it runs only when the function is called, not at module load time. Alternatively, restructure the code to avoid the mutual dependency. Circular imports are usually a sign that the modules are too tightly coupled.

Another common error is ImportError: attempted relative import with no known parent package. This occurs when you run a module inside a package directly as a script, like python mypackage/module_a.py. Relative imports require the module to be loaded as part of a package. To run it correctly, use python -m mypackage.module_a from the directory that contains the mypackage folder.

Preparing the Package for Distribution

To distribute your package, you need a build configuration. A minimal pyproject.toml using setuptools looks like this:

[build-system] requires = ["setuptools>=61.0"] build-backend = "setuptools.build_meta" [project] name = "mypackage" version = "0.1.0" [tool.setuptools.packages.find] include = ["mypackage*"]

The packages.find directive tells setuptools to discover packages whose names match the mypackage* pattern, so subpackages are included in the build. Without it, the build might miss subpackages or include unrelated directories. With the build package installed, this configuration is enough to produce a wheel with python -m build.

Namespace Packages and When They Are Useful

A namespace package is a package built from one or more directories that do not contain an __init__.py file. Since Python 3.3, a directory without __init__.py can be imported as a namespace package when its parent directory is on sys.path. Multiple directories that share the same top-level name can contribute to the same namespace package, allowing you to split a package across different locations on sys.path. This is useful for plugin systems or when different teams maintain separate parts of a package under the same top-level name.

For example, if sys.path contains /path/a and /path/b, and both directories contain a subdirectory named acme_plugins, then import acme_plugins merges the contents of both acme_plugins directories into one namespace package.

Namespace packages are more advanced and usually not needed for typical applications. They add complexity to packaging and can make debugging harder. Use them only when you have a genuine need to split a package across multiple distributions.

Maintainability: Organizing Code Within a Package

As a package grows, its structure directly affects how easy it is to maintain. Group related modules into subpackages to keep the top level clean. For example, a web framework might have core, http, and utils subpackages. Keep modules small and focused on a single responsibility. Large modules that do many things are harder to test and more likely to create import cycles.

Use clear, descriptive names for modules and subpackages. Avoid a single utils.py that accumulates unrelated functions; split it into string_utils.py, file_utils.py, and so on. This makes imports more readable and helps new developers find code faster.

Finally, keep the public API surface explicit. Use __all__ in __init__.py to define what is exported. This prevents accidental exposure of internal functions and makes the package's interface predictable.

Python Package Structure: Layout, Imports, and Packaging | RYUSLOG DEV