This document describes a modern Python project structure with best practices for tooling, testing, and code quality. Use this as a template for generating any Python project.
Package Management: Poetry Python Version: 3.10+ Key Tools: pytest, ruff, mypy, poethepoet
project-root/
├── .git/ # Git repository
├── .mypy_cache/ # MyPy type checker cache (gitignored)
├── .pytest_cache/ # Pytest cache (gitignored)
├── .ruff_cache/ # Ruff linter/formatter cache (gitignored)
├── data/ # Data files (gitignored if contains secrets/large files)
├── examples/ # Example usage scripts
│ └── sample_usage.py # Example of how to use the package
├── scripts/ # Utility scripts (e.g., data processing, setup)
│ └── helper_script.py # Helper scripts for development
├── sessions/ # Session notes/documentation (gitignored)
│ └── 2025-11-01-notes.md # Development session logs
├── src/ # Source code (src layout for packages)
│ └── package_name/ # Main package
│ ├── __init__.py # Package initialization, version
│ ├── config.py # Configuration management
│ ├── core.py # Core business logic
│ └── utils.py # Utility functions
├── tests/ # Test suite (mirrors src structure)
│ ├── __init__.py # Makes tests a package
│ ├── conftest.py # Pytest fixtures and configuration
│ ├── test_config.py # Configuration tests
│ ├── test_core.py # Core logic tests
│ └── test_utils.py # Utility function tests
├── .coverage # Coverage report (gitignored)
├── .env # Environment variables (gitignored, required)
├── .env.example # Environment variable template (committed)
├── .gitignore # Git ignore rules
├── LICENSE # License file
├── poetry.lock # Poetry dependency lock file (committed)
├── pyproject.toml # Poetry config, dependencies, tool configs
├── README.md # Main documentation
└── ruff.toml # Ruff linter/formatter configuration
The central configuration file for the project. Contains:
Project Metadata:
[tool.poetry]
name = "project-name"
version = "0.1.0"
description = "Project description"
authors = ["Author Name <[email protected]>"]
readme = "README.md"
packages = [{include = "package_name", from = "src"}]
[tool.poetry.dependencies]
python = "^3.10"
# Add your production dependencies here
# requests = "^2.31.0"
# fastapi = "^0.104.0"
# sqlalchemy = "^2.0.0"
[tool.poetry.group.dev.dependencies]
pytest = "^8.3.0"
pytest-cov = "^6.0.0"
pytest-mock = "^3.14.0"
ruff = "^0.8.0"
mypy = "^1.13.0"
poethepoet = "^0.35.0"
[build-system]
requires = ["poetry-core"]
build-backend = "poetry.core.masonry.api"Task Runner (Poe the Poet):
[tool.poe.tasks]
# Application tasks
start = {cmd = "python -m package_name", help = "Start the application"}
# Testing tasks
test = {cmd = "pytest tests/ -v --cov=src/package_name --cov-report=term-missing", help = "Run tests with coverage"}
test-fast = {cmd = "pytest tests/ -v", help = "Run tests without coverage"}
test-watch = {cmd = "pytest tests/ -v --lf", help = "Run only failed tests"}
# Code quality tasks
lint = {cmd = "ruff check src tests", help = "Run linter on source and tests"}
format = {cmd = "ruff format src tests", help = "Format code with Ruff"}
format-check = {cmd = "ruff format --check src tests", help = "Check code formatting"}
typecheck = {cmd = "mypy src", help = "Run static type checking"}
# Combined quality check
check-all = ["format-check", "lint", "typecheck", "test"]Pytest Configuration:
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"-v",
"--strict-markers",
"--strict-config",
]
markers = [
"unit: Unit tests",
"integration: Integration tests",
"slow: Slow-running tests",
]Ruff Configuration:
[tool.ruff]
line-length = 100
target-version = "py310"
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"PTH", # flake8-use-pathlib
]
ignore = []
[tool.ruff.lint.per-file-ignores]
"tests/*" = ["S101"] # Allow assert in testsMyPy Configuration:
[tool.mypy]
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
strict_equality = true
# Add overrides for third-party packages without type hints
[[tool.mypy.overrides]]
module = ["some_package.*"]
ignore_missing_imports = trueCoverage Configuration:
[tool.coverage.run]
source = ["src"]
omit = ["*/tests/*", "*/__init__.py"]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"raise AssertionError",
"raise NotImplementedError",
"if __name__ == .__main__.:",
"if TYPE_CHECKING:",
]Ruff linter and formatter configuration (can also be in pyproject.toml):
line-length = 100
target-version = "py310"
[lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort (import sorting)
"N", # pep8-naming
"UP", # pyupgrade (Python version upgrades)
"B", # flake8-bugbear (common bugs)
"C4", # flake8-comprehensions
"PTH", # flake8-use-pathlib (prefer pathlib)
]
ignore = []
[lint.per-file-ignores]
"tests/*" = ["S101"] # Allow assert statements in testsTemplate for environment variables (committed to version control):
# Application Configuration
APP_ENV=development
APP_DEBUG=true
LOG_LEVEL=INFO
# Database (example)
# DATABASE_URL=postgresql://user:password@localhost:5432/dbname
# API Keys (example)
# API_KEY=your-api-key-here
# SECRET_KEY=your-secret-key-here
# Paths
DATA_DIR=./dataStandard Python gitignore:
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# Virtual environments
.venv/
venv/
ENV/
env/
# IDEs
.vscode/
.idea/
*.swp
*.swo
*~
# Testing
.pytest_cache/
.coverage
htmlcov/
.tox/
# Type checking
.mypy_cache/
.pytype/
.pyre/
# Linting
.ruff_cache/
# Environment
.env
# Project-specific
data/
sessions/
*.log
# OS
.DS_Store
Thumbs.dbUse the src/ layout (not flat layout) for better separation and import testing:
# src/package_name/__init__.py
"""Package description."""
__version__ = "0.1.0"
# Optionally expose main functionality
from package_name.core import main_function
__all__ = ["main_function"]# src/package_name/config.py
"""Configuration management."""
from pathlib import Path
from dotenv import load_dotenv
import os
load_dotenv()
class Config:
"""Application configuration from environment variables."""
# Application settings
APP_ENV: str = os.getenv("APP_ENV", "development")
DEBUG: bool = os.getenv("APP_DEBUG", "false").lower() == "true"
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
# Paths
DATA_DIR: Path = Path(os.getenv("DATA_DIR", "./data"))
@classmethod
def validate(cls) -> None:
"""Validate required configuration."""
# Add validation logic
if not cls.DATA_DIR.exists():
cls.DATA_DIR.mkdir(parents=True, exist_ok=True)Main modules should be runnable:
# src/package_name/module.py
"""Module description."""
from typing import Any
def main() -> None:
"""Main entry point."""
# Implementation
pass
if __name__ == "__main__":
main()"""Pytest configuration and shared fixtures."""
import tempfile
from collections.abc import Generator
from pathlib import Path
import pytest
@pytest.fixture
def temp_dir() -> Generator[Path, None, None]:
"""Create a temporary directory for tests."""
with tempfile.TemporaryDirectory() as tmp_dir:
yield Path(tmp_dir)
@pytest.fixture
def sample_data() -> list[dict[str, Any]]:
"""Return sample test data."""
return [
{"id": 1, "name": "test1", "value": 100},
{"id": 2, "name": "test2", "value": 200},
]
@pytest.fixture
def mock_config(monkeypatch: pytest.MonkeyPatch) -> None:
"""Mock configuration for tests."""
from package_name import config as config_module
monkeypatch.setattr(config_module.Config, "DEBUG", True)
monkeypatch.setattr(config_module.Config, "LOG_LEVEL", "DEBUG")"""Tests for module_name."""
import pytest
from package_name.module import function_to_test
def test_function_basic() -> None:
"""Test basic functionality.
Arrange-Act-Assert pattern.
"""
# Arrange
input_data = "test input"
expected = "expected output"
# Act
result = function_to_test(input_data)
# Assert
assert result == expected
@pytest.mark.parametrize("input_val,expected", [
("input1", "output1"),
("input2", "output2"),
("", "default"),
])
def test_function_parametrized(input_val: str, expected: str) -> None:
"""Test with multiple parameter combinations."""
result = function_to_test(input_val)
assert result == expected
def test_function_with_mocks(mocker) -> None:
"""Test with mocked external dependencies."""
# Mock external function call
mock_external = mocker.patch("package_name.module.external_function")
mock_external.return_value = {"status": "success"}
result = function_to_test()
assert result is not None
mock_external.assert_called_once()
def test_function_raises_error() -> None:
"""Test that function raises expected error."""
with pytest.raises(ValueError, match="Invalid input"):
function_to_test(invalid_input="bad")# 1. Install Poetry
curl -sSL https://install.python-poetry.org | python3 -
# 2. Create new project
poetry new --src project-name
cd project-name
# 3. Install dependencies
poetry install
# 4. Configure environment
cp .env.example .env
# Edit .env with your configuration
# 5. Verify setup
poetry run poe check-all# Run tests during development
poetry run poe test-fast
# Format code
poetry run poe format
# Run all checks before commit
poetry run poe check-all
# Run specific module
poetry run python -m package_name.module
# Start application
poetry run poe start# Standard workflow
git status
git add .
git commit -m "Description of changes"
git push
# Before committing, always run:
poetry run poe check-allfrom pathlib import Path
from collections.abc import Generator
from typing import Any
def process_items(
items: list[str],
limit: int = 10,
filter_fn: callable[[str], bool] | None = None
) -> list[str]:
"""Process list of items."""
results: list[str] = []
# Implementation
return results
def read_file(file_path: Path) -> Generator[str, None, None]:
"""Generate lines from file."""
with file_path.open() as f:
for line in f:
yield line.strip()def function_name(param1: str, param2: int = 5) -> bool:
"""Brief description of what the function does.
More detailed description if needed. Can span multiple
lines to explain complex behavior.
Args:
param1: Description of param1.
param2: Description of param2. Defaults to 5.
Returns:
Description of return value.
Raises:
ValueError: When param1 is empty.
TypeError: When param2 is not an integer.
Example:
>>> function_name("test", 10)
True
"""
if not param1:
raise ValueError("param1 cannot be empty")
return True# Standard library imports
import os
import sys
from pathlib import Path
from typing import Any
# Third-party imports
import pytest
import requests
from fastapi import FastAPI
# Local imports
from package_name.config import Config
from package_name.utils import helper_functionA comprehensive README should include:
# Project Name
Brief description of what the project does.
## Features
- Feature 1
- Feature 2
- Feature 3
## Prerequisites
- Python 3.10 or higher
- Any other requirements
## Installation
### 1. Clone the repository
\`\`\`bash
git clone <repository-url>
cd project-name
\`\`\`
### 2. Install Poetry
\`\`\`bash
curl -sSL https://install.python-poetry.org | python3 -
\`\`\`
### 3. Install dependencies
\`\`\`bash
poetry install
\`\`\`
### 4. Configure environment
\`\`\`bash
cp .env.example .env
# Edit .env with your configuration
\`\`\`
## Usage
### Basic usage
\`\`\`bash
poetry run python -m package_name
\`\`\`
### Using as a library
\`\`\`python
from package_name import main_function
result = main_function("input")
print(result)
\`\`\`
## Development
### Running Tests
\`\`\`bash
# Run all tests with coverage
poetry run poe test
# Run tests without coverage (faster)
poetry run poe test-fast
\`\`\`
### Code Quality
\`\`\`bash
# Format code
poetry run poe format
# Check formatting
poetry run poe format-check
# Lint code
poetry run poe lint
# Type checking
poetry run poe typecheck
# Run all checks
poetry run poe check-all
\`\`\`
## Project Structure
\`\`\`
project-name/
├── src/package_name/ # Source code
├── tests/ # Test suite
├── examples/ # Usage examples
├── pyproject.toml # Project configuration
└── README.md
\`\`\`
## Configuration
Configuration is done via environment variables in `.env`:
\`\`\`bash
APP_ENV=development
DEBUG=true
\`\`\`
## Contributing
1. Run `poe check-all` before committing
2. Add tests for new features
3. Update documentation as needed
## License
[Your license here]# Dependency management
poetry add package-name # Add production dependency
poetry add --group dev package-name # Add dev dependency
poetry remove package-name # Remove dependency
poetry update # Update dependencies
poetry update package-name # Update specific package
poetry show # List dependencies
poetry show --tree # Show dependency tree
poetry show package-name # Show package details
# Environment management
poetry env info # Show environment info
poetry env list # List environments
poetry env remove python # Remove environment
poetry shell # Activate virtual environment
# Running commands
poetry run python script.py # Run Python script
poetry run pytest # Run tests
poetry run poe task-name # Run Poe task
# Building and publishing
poetry build # Build package
poetry publish # Publish to PyPI
poetry version # Show current version
poetry version patch # Bump patch version
poetry version minor # Bump minor version
poetry version major # Bump major version- Use type hints for all functions
- Write docstrings for public APIs
- Maintain 80%+ test coverage
- Run
poe check-allbefore committing - Use
src/layout for packages - Follow AAA pattern in tests (Arrange-Act-Assert)
- Keep line length to 100 characters
- Lock dependencies with
poetry.lock(commit to version control) - Use
^for compatible versions (^1.2.0=>=1.2.0 <2.0.0) - Separate dev dependencies from production
- Document required environment variables in
.env.example - Pin dependencies for libraries, use ranges for applications
- Mock external APIs and dependencies
- Use fixtures for reusable test data
- Parametrize tests for multiple cases
- Test both success and error paths
- Use descriptive test names (test_function_does_specific_thing)
- One assertion per test (when possible)
- Write clear, descriptive commit messages
- Keep
.gitignorecomprehensive - Don't commit
.env, cache files, or build artifacts - Do commit
.env.example,poetry.lock, and config files - Create feature branches for new work
- One module = one responsibility
- Keep modules runnable with
if __name__ == "__main__" - Configuration in separate module
- Tests mirror source structure
- Examples in separate directory
- Use absolute imports from package root
When creating a new project based on this structure:
- Initialize with
poetry new --src project-name - Configure
pyproject.tomlwith all tool sections - Create
ruff.tomlor add ruff config topyproject.toml - Create
.env.examplewith required variables - Create comprehensive
.gitignore - Create
src/package_name/config.pyfor configuration - Create
tests/conftest.pywith common fixtures - Set up
README.mdwith proper sections - Install dev dependencies:
poetry add --group dev pytest pytest-cov pytest-mockpoetry add --group dev ruff mypy poethepoet
- Configure Poe tasks in
pyproject.toml - Add type hint overrides in mypy config for third-party packages
- Create example scripts in
examples/ - Run
poetry run poe check-allto verify setup - Create initial git commit
- Add LICENSE file
- Set up CI/CD (GitHub Actions, GitLab CI, etc.)
[tool.poetry.scripts]
myapp = "package_name.cli:main"# src/package_name/cli.py
import argparse
def main() -> None:
parser = argparse.ArgumentParser(description="Application description")
parser.add_argument("input", help="Input file")
parser.add_argument("--verbose", action="store_true")
args = parser.parse_args()
# Implementation# src/package_name/api.py
from fastapi import FastAPI
app = FastAPI()
@app.get("/")
def read_root():
return {"message": "Hello World"}
if __name__ == "__main__":
import uvicorn
uvicorn.run(app, host="0.0.0.0", port=8000)# src/package_name/__init__.py
"""Library description."""
__version__ = "0.1.0"
from package_name.core import main_function, helper_function
__all__ = ["main_function", "helper_function"]This structure provides a solid foundation for any modern Python project with comprehensive testing, code quality tools, and professional development practices.