Project Layout
Standard Project Structure
my_project/
├── src/
│ └── my_project/ # Main package
│ ├── __init__.py
│ ├── core.py # Core logic
│ ├── models.py # Data models
│ ├── services.py # Business logic
│ └── utils.py # Utilities
├── tests/ # Test directory
│ ├── __init__.py
│ ├── conftest.py # Pytest fixtures
│ ├── test_core.py
│ └── test_services.py
├── config/ # Configuration files
│ ├── settings.py
│ └── logging.py
├── scripts/ # Utility scripts
│ └── seed_database.py
├── docs/ # Documentation
│ └── README.md
├── .env # Environment variables (not in git)
├── .gitignore
├── pyproject.toml # Project metadata
├── requirements.txt # Dependencies
├── requirements-dev.txt # Development dependencies
└── README.md
src Layout vs Flat Layout
# src layout (recommended)
src/
└── my_package/
└── __init__.py
# Flat layout (simpler)
my_package/
└── __init__.py
# src layout benefits:
# - Prevents accidental imports from project root
# - Clearer package boundaries
# - Better for testing
init.py Best Practices
# my_package/__init__.py
# Option 1: Empty (just marks as package)
# Option 2: Export public API
from .core import main_function
from .models import User, Product
from .utils import helper_function
__all__ = ['main_function', 'User', 'Product', 'helper_function']
# Option 3: Lazy imports (for large packages)
import importlib
def __getattr__(name):
if name == 'heavy_module':
return importlib.import_module('.heavy_module', __name__)
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
Configuration Management
Environment Variables
# config/settings.py
import os
from pathlib import Path
# Base directory
BASE_DIR = Path(__file__).resolve().parent.parent
# Database
DATABASE_URL = os.getenv('DATABASE_URL', 'sqlite:///db.sqlite3')
# API Keys
SECRET_KEY = os.getenv('SECRET_KEY', 'dev-secret-key')
API_KEY = os.getenv('API_KEY')
# Debug mode
DEBUG = os.getenv('DEBUG', 'False').lower() == 'true'
# Logging
LOG_LEVEL = os.getenv('LOG_LEVEL', 'INFO')
.env File
# .env (not committed to git)
DATABASE_URL=postgresql://user:pass@localhost/mydb
SECRET_KEY=my-secret-key
DEBUG=true
LOG_LEVEL=DEBUG
python-dotenv
# Load .env file
from dotenv import load_dotenv
load_dotenv() # Loads from .env in current directory
# Or specify path
from pathlib import Path
load_dotenv(Path(__file__).parent / '.env')
# Now access with os.getenv
import os
db_url = os.getenv('DATABASE_URL')
Configuration Classes
from dataclasses import dataclass
import os
@dataclass
class DatabaseConfig:
url: str = 'sqlite:///db.sqlite3'
pool_size: int = 5
echo: bool = False
@dataclass
class AppConfig:
debug: bool = False
secret_key: str = ''
database: DatabaseConfig = None
def __post_init__(self):
if self.database is None:
self.database = DatabaseConfig()
# Load from environment
def load_config() -> AppConfig:
return AppConfig(
debug=os.getenv('DEBUG', 'False').lower() == 'true',
secret_key=os.getenv('SECRET_KEY', ''),
database=DatabaseConfig(
url=os.getenv('DATABASE_URL', 'sqlite:///db.sqlite3')
)
)
config = load_config()
Logging Setup
import logging
from config.settings import LOG_LEVEL
def setup_logging():
logging.basicConfig(
level=getattr(logging, LOG_LEVEL),
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
handlers=[
logging.StreamHandler(),
logging.FileHandler('app.log')
]
)
# Usage
logger = logging.getLogger(__name__)
def my_function():
logger.info('Starting function')
logger.debug('Debug info')
logger.warning('Warning!')
logger.error('Error occurred')
Testing Structure
Test Directory Structure
tests/
├── __init__.py
├── conftest.py # Shared fixtures
├── unit/ # Unit tests
│ ├── __init__.py
│ ├── test_models.py
│ └── test_utils.py
├── integration/ # Integration tests
│ ├── __init__.py
│ └── test_database.py
└── fixtures/ # Test data
├── users.json
└── products.json
conftest.py
# tests/conftest.py
import pytest
from my_package.models import User
from my_package.database import create_test_db
@pytest.fixture
def sample_user():
return User(name='Alice', email='alice@example.com')
@pytest.fixture
def test_db():
db = create_test_db()
yield db
db.cleanup()
@pytest.fixture
def client(app):
return app.test_client()
Test Naming Conventions
# tests/unit/test_models.py
import pytest
from my_package.models import User
class TestUser:
def test_creation(self, sample_user):
assert sample_user.name == 'Alice'
def test_email_validation(self):
with pytest.raises(ValueError):
User(name='Alice', email='invalid')
def test_repr(self, sample_user):
assert 'Alice' in repr(sample_user)
# Or function-based
def test_user_creation():
user = User(name='Alice', email='alice@example.com')
assert user.name == 'Alice'
# Pattern: test_<what>_<condition>_<expected>
def test_user_creation_with_valid_email_succeeds():
pass
def test_user_creation_with_invalid_email_raises_error():
pass
Running Tests
# Run all tests
pytest
# Run specific file
pytest tests/unit/test_models.py
# Run with coverage
pytest --cov=my_package
# Run only failed tests
pytest --lf
# Verbose output
pytest -v
# Stop on first failure
pytest -x