Organize Packages Properly
Problem
Python’s import system is flexible but can lead to confusion with circular imports, relative vs absolute imports, and package structure. Proper package organization makes code maintainable and prevents import issues.
This guide shows how to structure Python packages effectively.
Package Structure Basics
myproject/
├── src/
│ └── mypackage/
│ ├── __init__.py # Package initialization
│ ├── module1.py
│ ├── module2.py
│ └── subpackage/
│ ├── __init__.py
│ └── module3.py
├── tests/
│ ├── __init__.py
│ ├── test_module1.py
│ └── test_module2.py
├── setup.py or pyproject.toml
└── README.mdImport Styles
Absolute Imports (Preferred)
from mypackage import module1
from mypackage.subpackage import module3
from mypackage.module2 import SomeClassRelative Imports
from . import module2 # Same level
from .subpackage import module3 # Subdirectory
from ..otherpackage import module4 # Parent directoryImport Patterns
import mypackage.module1
result = mypackage.module1.function()
from mypackage.module1 import function, SomeClass
result = function()
obj = SomeClass()
import mypackage.very_long_module_name as vlmn
vlmn.function()
from mypackage.module1 import VeryLongClassName as VLCN
obj = VLCN()
from mypackage.module1 import * # Pollutes namespaceinit.py Usage
Empty init.py
Exposing Package API
from .module1 import PublicClass, public_function
from .module2 import AnotherClass
__all__ = ["PublicClass", "public_function", "AnotherClass"]Package Initialization
import logging
logger = logging.getLogger(__name__)
logger.addHandler(logging.NullHandler())
__version__ = "1.0.0"
def _import_heavy_module():
from . import heavy_module
return heavy_moduleAvoiding Circular Imports
The Problem
from module_b import function_b
def function_a():
return function_b()
from module_a import function_a # Circular!
def function_b():
return function_a()Solutions
def shared_function():
return "shared"
from shared import shared_function
def function_a():
return shared_function()
from shared import shared_function
def function_b():
return shared_function()
def function_a():
from module_b import function_b # Delayed import
return function_b()
def function_a(dependency):
return dependency()
from module_b import function_b
result = function_a(function_b)Project Layout Patterns
Flat Layout
myproject/
├── mypackage/
│ ├── __init__.py
│ ├── core.py
│ ├── utils.py
│ └── config.py
├── tests/
├── setup.py
└── README.mdSrc Layout (Recommended)
myproject/
├── src/
│ └── mypackage/
│ ├── __init__.py
│ ├── core.py
│ └── utils.py
├── tests/
├── setup.py or pyproject.toml
└── README.mdApplication Layout
myapp/
├── src/
│ └── myapp/
│ ├── __init__.py
│ ├── __main__.py # Entry point
│ ├── api/
│ │ ├── __init__.py
│ │ ├── routes.py
│ │ └── handlers.py
│ ├── core/
│ │ ├── __init__.py
│ │ ├── models.py
│ │ └── services.py
│ ├── utils/
│ │ ├── __init__.py
│ │ └── helpers.py
│ └── config.py
├── tests/
├── requirements.txt
└── README.mdmain.py for Entry Points
def main():
print("Running mypackage")
# Application logic
if __name__ == "__main__":
main()Managing Dependencies
Import Order (PEP 8)
import os
import sys
from pathlib import Path
import numpy as np
import requests
from flask import Flask
from mypackage import module1
from mypackage.utils import helperConditional Imports
try:
import pandas as pd
HAS_PANDAS = True
except ImportError:
HAS_PANDAS = False
def process_data(data):
if HAS_PANDAS:
return process_with_pandas(data)
else:
return process_without_pandas(data)
from typing import TYPE_CHECKING
if TYPE_CHECKING:
# Only imported for type checkers, not at runtime
from mypackage.heavy_module import HeavyClass
def function(obj: 'HeavyClass'): # String annotation
passNamespace Packages
from mynamespace.package1 import module
from mynamespace.package2 import moduleBest Practices
Keep init.py Minimal
from .core import MainClass, main_function
from .utils import helper_function
__version__ = "1.0.0"
__all__ = ["MainClass", "main_function", "helper_function"]Use Explicit Exports
__all__ = ["public_function", "PublicClass"]
def public_function():
return _private_function()
class PublicClass:
pass
def _private_function(): # Leading underscore = private
pass
class _PrivateClass:
passOrganize by Feature, Not Type
myapp/
├── models/
│ ├── user.py
│ └── order.py
├── views/
│ ├── user.py
│ └── order.py
└── controllers/
├── user.py
└── order.py
myapp/
├── users/
│ ├── __init__.py
│ ├── models.py
│ ├── views.py
│ └── controllers.py
└── orders/
├── __init__.py
├── models.py
├── views.py
└── controllers.pyTesting Package Structure
myproject/
├── src/
│ └── mypackage/
│ └── ...
└── tests/
├── __init__.py
├── conftest.py # pytest fixtures
├── test_module1.py
└── integration/
├── __init__.py
└── test_api.pyPackage Distribution
setup.py (Traditional)
from setuptools import setup, find_packages
setup(
name="mypackage",
version="1.0.0",
packages=find_packages(where="src"),
package_dir={"": "src"},
install_requires=[
"requests>=2.28.0",
"click>=8.0.0",
],
extras_require={
"dev": ["pytest>=7.0.0", "black>=23.0.0"],
},
entry_points={
"console_scripts": [
"myapp=mypackage.cli:main",
],
},
)pyproject.toml (Modern)
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"
[project]
name = "mypackage"
version = "1.0.0"
dependencies = [
"requests>=2.28.0",
"click>=8.0.0",
]
[project.optional-dependencies]
dev = ["pytest>=7.0.0", "black>=23.0.0"]
[project.scripts]
myapp = "mypackage.cli:main"Summary
Package organization in Python centers on clear import paths and preventing circular dependencies. Absolute imports provide explicit, unambiguous paths that work everywhere, while relative imports work only within packages and fail when modules are run directly. Prefer absolute imports for clarity and maintainability.
The src layout separates source code from project metadata and tests, forcing proper installation before use. This prevents accidentally importing from the wrong location and makes testing more reliable. Place your package inside src/ and import it like any third-party package.
The init.py file marks directories as Python packages and controls package initialization. Keep it minimal - expose the public API through targeted imports and avoid placing business logic there. Use all to explicitly define what star imports should include, making the package’s public interface clear.
Circular imports indicate design issues more than import problems. Restructure code to extract shared functionality, use dependency injection, or move imports inside functions when necessary. Circular dependencies usually disappear when responsibilities are properly separated.
Organize modules by feature rather than type. Group related models, views, and controllers together rather than separating all models into one directory and all views into another. Feature-based organization scales better and keeps related code together.
Import order follows PEP 8 conventions: standard library, third-party packages, then local imports, with blank lines separating groups. This consistency makes imports scannable and prevents ambiguity about dependencies.
Package distribution through setup.py or pyproject.toml defines dependencies, entry points, and metadata. Modern Python uses pyproject.toml for configuration, consolidating multiple config files into one standard format. Entry points enable installing command-line tools that users can run directly.
Proper package organization makes code maintainable as projects grow. Clear import paths, minimal circular dependencies, and feature-based organization prevent common Python project problems.