Files
c9s-skills/skills/python/SKILL.md
T
aj-geddes e219cfc94e feat: initial release of BMAD Skills for Claude Code
BMAD Skills for Claude Code - v1.0.0

This repository provides a comprehensive implementation of the BMAD Method
for Claude Code using native Claude Code features.

Features:
- 7 comprehensive skills (BMAD, security, Python, JS/TS, DevOps, testing, Git)
- 5 slash commands (/bmad-init, /bmad-prd, /bmad-arch, /bmad-story, /bmad-assess)
- Memory integration for context preservation
- Auto-detection and intelligent suggestions
- Todo tracking for stories
- Hooks for project-level automation
- One-command installation

Credits:
- BMAD Method™ by BMAD Code Organization
- Implementation for Claude Code by contributors

All BMAD methodology credit belongs to the BMAD Code Organization.
See: https://github.com/bmad-code-org/BMAD-METHOD
2025-10-25 03:53:21 -05:00

25 KiB

Python Development Excellence Skill

Overview

This skill provides comprehensive Python development guidance covering modern Python (3.8+), best practices, idioms, and professional development patterns.

Python Philosophy - The Zen of Python

import this

Key principles to follow:

  • Explicit is better than implicit - Clear code over clever tricks
  • Simple is better than complex - Favor readability
  • Readability counts - Code is read more than written
  • Errors should never pass silently - Fail loud and clear
  • There should be one obvious way to do it - Prefer idiomatic Python

Modern Python Features (3.8+)

Type Hints (Python 3.5+, improved 3.8+)

from typing import List, Dict, Optional, Union, Callable, TypeVar, Generic
from dataclasses import dataclass

# Function annotations
def process_items(
    items: List[str],
    max_count: Optional[int] = None
) -> Dict[str, int]:
    """Process items and return count mapping."""
    result = {}
    for item in items[:max_count]:
        result[item] = result.get(item, 0) + 1
    return result

# Complex types
def apply_function(
    func: Callable[[int], int],
    values: List[int]
) -> List[int]:
    return [func(x) for x in values]

# Generic types
T = TypeVar('T')

class Stack(Generic[T]):
    """Type-safe stack implementation."""
    
    def __init__(self) -> None:
        self._items: List[T] = []
    
    def push(self, item: T) -> None:
        self._items.append(item)
    
    def pop(self) -> T:
        return self._items.pop()
    
    def peek(self) -> Optional[T]:
        return self._items[-1] if self._items else None

# Usage
stack: Stack[int] = Stack()
stack.push(42)
value: int = stack.pop()

Walrus Operator (Python 3.8)

# Assign and use in same expression
if (n := len(items)) > 10:
    print(f"List is long ({n} items)")

# In list comprehensions
filtered = [result for item in items 
            if (result := process(item)) is not None]

# While loops
while (line := file.readline()):
    process_line(line)

Positional-Only and Keyword-Only Parameters (Python 3.8)

def complex_function(
    positional_only,    # Before /
    /,                  # Positional-only marker
    positional_or_kw,   # Can be either
    *,                  # Keyword-only marker
    keyword_only        # After *
):
    """
    positional_only: Must pass by position
    positional_or_kw: Can pass either way
    keyword_only: Must pass by keyword
    """
    pass

# Valid calls
complex_function(1, 2, keyword_only=3)
complex_function(1, positional_or_kw=2, keyword_only=3)

# Invalid calls
# complex_function(positional_only=1, 2, keyword_only=3)  # Error
# complex_function(1, 2, 3)  # Error

Structural Pattern Matching (Python 3.10)

def process_command(command: dict) -> str:
    """Process commands using pattern matching."""
    match command:
        case {"action": "create", "type": "user", "name": name}:
            return f"Creating user: {name}"
        
        case {"action": "delete", "type": type_, "id": id_}:
            return f"Deleting {type_} with id {id_}"
        
        case {"action": "list", "type": type_}:
            return f"Listing all {type_}s"
        
        case _:
            return "Unknown command"

# Complex patterns
def process_point(point):
    match point:
        case (0, 0):
            return "Origin"
        case (0, y):
            return f"On Y-axis at {y}"
        case (x, 0):
            return f"On X-axis at {x}"
        case (x, y) if x == y:
            return f"On diagonal at {x}"
        case (x, y):
            return f"Point at ({x}, {y})"

Dataclasses (Python 3.7+)

from dataclasses import dataclass, field, asdict
from typing import List
from datetime import datetime

@dataclass
class User:
    """User model with automatic __init__, __repr__, __eq__."""
    
    id: int
    username: str
    email: str
    created_at: datetime = field(default_factory=datetime.now)
    tags: List[str] = field(default_factory=list)
    
    def __post_init__(self):
        """Validation after initialization."""
        if not self.email or '@' not in self.email:
            raise ValueError("Invalid email")
        self.username = self.username.lower()

# Usage
user = User(id=1, username="John", email="john@example.com")
print(user)  # Automatic __repr__
user_dict = asdict(user)  # Convert to dict

# Frozen dataclasses (immutable)
@dataclass(frozen=True)
class Point:
    x: float
    y: float
    
# Comparison
@dataclass(order=True)
class Task:
    priority: int
    name: str = field(compare=False)  # Exclude from comparison

Python Idioms and Best Practices

List Comprehensions and Generator Expressions

# List comprehension - creates list in memory
squares = [x**2 for x in range(10)]

# Generator expression - lazy evaluation
squares_gen = (x**2 for x in range(10))

# Dict comprehension
word_lengths = {word: len(word) for word in words}

# Set comprehension
unique_lengths = {len(word) for word in words}

# Nested comprehensions
matrix = [[1, 2, 3], [4, 5, 6]]
flattened = [num for row in matrix for num in row]

# Conditional comprehensions
evens = [x for x in range(10) if x % 2 == 0]

# Complex transformations
processed = [
    transform(item)
    for sublist in data
    for item in sublist
    if validate(item)
]

Context Managers

# Built-in context managers
with open('file.txt', 'r') as f:
    content = f.read()

# Multiple context managers
with open('input.txt') as infile, open('output.txt', 'w') as outfile:
    outfile.write(infile.read())

# Custom context manager with class
class DatabaseConnection:
    """Context manager for database connections."""
    
    def __init__(self, connection_string: str):
        self.connection_string = connection_string
        self.connection = None
    
    def __enter__(self):
        self.connection = connect(self.connection_string)
        return self.connection
    
    def __exit__(self, exc_type, exc_val, exc_tb):
        if self.connection:
            self.connection.close()
        return False  # Don't suppress exceptions

# Usage
with DatabaseConnection('postgresql://...') as conn:
    conn.execute("SELECT * FROM users")

# Context manager with decorator
from contextlib import contextmanager

@contextmanager
def timer(label: str):
    """Time a block of code."""
    import time
    start = time.time()
    try:
        yield
    finally:
        elapsed = time.time() - start
        print(f"{label}: {elapsed:.4f} seconds")

# Usage
with timer("Processing"):
    process_large_dataset()

Decorators

from functools import wraps
import time

# Basic decorator
def timing_decorator(func):
    """Measure function execution time."""
    @wraps(func)  # Preserves original function metadata
    def wrapper(*args, **kwargs):
        start = time.time()
        result = func(*args, **kwargs)
        elapsed = time.time() - start
        print(f"{func.__name__} took {elapsed:.4f}s")
        return result
    return wrapper

@timing_decorator
def slow_function():
    time.sleep(1)
    return "Done"

# Decorator with arguments
def retry(max_attempts: int = 3, delay: float = 1.0):
    """Retry decorator with configurable attempts."""
    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            for attempt in range(max_attempts):
                try:
                    return func(*args, **kwargs)
                except Exception as e:
                    if attempt == max_attempts - 1:
                        raise
                    time.sleep(delay)
            return None
        return wrapper
    return decorator

@retry(max_attempts=3, delay=2.0)
def unreliable_api_call():
    # Might fail, will retry
    return requests.get('https://api.example.com/data')

# Class decorator
def singleton(cls):
    """Ensure class has only one instance."""
    instances = {}
    @wraps(cls)
    def get_instance(*args, **kwargs):
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return get_instance

@singleton
class DatabaseConnection:
    pass

# Property decorator
class User:
    def __init__(self, first_name: str, last_name: str):
        self._first_name = first_name
        self._last_name = last_name
    
    @property
    def full_name(self) -> str:
        """Read-only computed property."""
        return f"{self._first_name} {self._last_name}"
    
    @property
    def first_name(self) -> str:
        return self._first_name
    
    @first_name.setter
    def first_name(self, value: str) -> None:
        if not value:
            raise ValueError("First name cannot be empty")
        self._first_name = value

Generators and Iterators

# Generator function
def fibonacci(n: int):
    """Generate first n Fibonacci numbers."""
    a, b = 0, 1
    for _ in range(n):
        yield a
        a, b = b, a + b

# Usage
for num in fibonacci(10):
    print(num)

# Generator expression
squares = (x**2 for x in range(1000000))  # Memory efficient

# Custom iterator
class CountDown:
    """Iterator that counts down."""
    
    def __init__(self, start: int):
        self.current = start
    
    def __iter__(self):
        return self
    
    def __next__(self):
        if self.current <= 0:
            raise StopIteration
        self.current -= 1
        return self.current + 1

# Usage
for num in CountDown(5):
    print(num)

# Infinite generator
def infinite_sequence():
    """Generate infinite sequence."""
    num = 0
    while True:
        yield num
        num += 1

# Use with itertools
from itertools import islice
first_ten = list(islice(infinite_sequence(), 10))

Argument Unpacking

# Function arguments
def process(a, b, c):
    return a + b + c

args = [1, 2, 3]
result = process(*args)  # Unpack list

kwargs = {'a': 1, 'b': 2, 'c': 3}
result = process(**kwargs)  # Unpack dict

# Extended unpacking
first, *middle, last = [1, 2, 3, 4, 5]
# first = 1, middle = [2, 3, 4], last = 5

# Dictionary unpacking
base_config = {'host': 'localhost', 'port': 5432}
user_config = {'port': 3306, 'database': 'mydb'}

# Merge dicts (Python 3.9+)
config = base_config | user_config

# Or using unpacking
config = {**base_config, **user_config}

Async Programming

Async/Await (Python 3.5+)

import asyncio
import aiohttp
from typing import List

async def fetch_url(session: aiohttp.ClientSession, url: str) -> str:
    """Fetch URL asynchronously."""
    async with session.get(url) as response:
        return await response.text()

async def fetch_all(urls: List[str]) -> List[str]:
    """Fetch multiple URLs concurrently."""
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in urls]
        return await asyncio.gather(*tasks)

# Run async code
async def main():
    urls = ['https://example.com', 'https://example.org']
    results = await fetch_all(urls)
    print(results)

# Execute
asyncio.run(main())

# Async context manager
class AsyncDatabaseConnection:
    async def __aenter__(self):
        self.conn = await create_connection()
        return self.conn
    
    async def __aexit__(self, exc_type, exc_val, exc_tb):
        await self.conn.close()

# Async generator
async def async_range(count: int):
    """Async generator example."""
    for i in range(count):
        await asyncio.sleep(0.1)
        yield i

# Usage
async def process():
    async for value in async_range(10):
        print(value)

Error Handling

Exception Best Practices

# Specific exceptions
try:
    result = risky_operation()
except ValueError as e:
    # Handle specific exception
    logger.error(f"Invalid value: {e}")
    raise
except KeyError as e:
    # Handle another specific exception
    logger.error(f"Missing key: {e}")
    return default_value
except (IOError, OSError) as e:
    # Handle multiple exceptions
    logger.error(f"IO error: {e}")
    raise
finally:
    # Always executed
    cleanup()

# Custom exceptions
class ApplicationError(Exception):
    """Base exception for application."""
    pass

class ValidationError(ApplicationError):
    """Raised when validation fails."""
    
    def __init__(self, field: str, message: str):
        self.field = field
        self.message = message
        super().__init__(f"{field}: {message}")

class NotFoundError(ApplicationError):
    """Raised when resource not found."""
    pass

# Exception chaining
try:
    process_data()
except ValueError as e:
    raise ApplicationError("Processing failed") from e

# Suppress specific exceptions (use sparingly)
from contextlib import suppress

with suppress(FileNotFoundError):
    os.remove('temp_file.txt')

# Exception groups (Python 3.11+)
try:
    complex_operation()
except* ValueError as eg:
    # Handle all ValueErrors in group
    for exc in eg.exceptions:
        logger.error(f"Value error: {exc}")
except* KeyError as eg:
    # Handle all KeyErrors in group
    for exc in eg.exceptions:
        logger.error(f"Key error: {exc}")

Testing

pytest Best Practices

import pytest
from typing import List

# Basic test
def test_addition():
    """Test basic addition."""
    assert 1 + 1 == 2

# Fixtures
@pytest.fixture
def sample_users():
    """Provide sample user data."""
    return [
        {'id': 1, 'name': 'Alice'},
        {'id': 2, 'name': 'Bob'}
    ]

def test_user_count(sample_users):
    """Test with fixture."""
    assert len(sample_users) == 2

# Parametrized tests
@pytest.mark.parametrize('input,expected', [
    (1, 2),
    (2, 4),
    (3, 6),
])
def test_double(input, expected):
    """Test with multiple inputs."""
    assert input * 2 == expected

# Exception testing
def test_division_by_zero():
    """Test exception is raised."""
    with pytest.raises(ZeroDivisionError):
        1 / 0

# Test classes
class TestUserManager:
    """Group related tests."""
    
    @pytest.fixture(autouse=True)
    def setup(self):
        """Setup before each test."""
        self.manager = UserManager()
        yield
        # Teardown after each test
        self.manager.cleanup()
    
    def test_add_user(self):
        user = self.manager.add_user('Alice')
        assert user.name == 'Alice'
    
    def test_remove_user(self):
        user = self.manager.add_user('Bob')
        self.manager.remove_user(user.id)
        assert self.manager.get_user(user.id) is None

# Mocking
from unittest.mock import Mock, patch

def test_api_call():
    """Test with mocked API."""
    with patch('requests.get') as mock_get:
        mock_get.return_value.json.return_value = {'status': 'ok'}
        result = fetch_data()
        assert result['status'] == 'ok'
        mock_get.assert_called_once()

# Property-based testing with hypothesis
from hypothesis import given, strategies as st

@given(st.lists(st.integers()))
def test_sort_idempotent(lst: List[int]):
    """Sorting is idempotent."""
    sorted_once = sorted(lst)
    sorted_twice = sorted(sorted_once)
    assert sorted_once == sorted_twice

Code Organization

Module Structure

# module.py
"""
Module docstring describing purpose.

Longer description of what the module does.
"""

# Standard library imports
import os
import sys
from datetime import datetime
from typing import List, Optional

# Third-party imports
import requests
import numpy as np
from sqlalchemy import create_engine

# Local imports
from .models import User
from .utils import validate_email
from . import constants

# Module-level constants
DEFAULT_TIMEOUT = 30
MAX_RETRIES = 3

# Module-level "private" variables
_cache = {}

# Public API
__all__ = ['process_user', 'validate_data']


class UserProcessor:
    """Process user data."""
    
    def __init__(self, config: dict):
        self.config = config
        self._setup()
    
    def _setup(self):
        """Private setup method."""
        pass
    
    def process(self, user: User) -> dict:
        """Public process method."""
        return self._internal_process(user)
    
    def _internal_process(self, user: User) -> dict:
        """Private helper method."""
        pass


def process_user(user_id: int) -> Optional[User]:
    """
    Process user by ID.
    
    Args:
        user_id: ID of user to process
        
    Returns:
        Processed user or None if not found
        
    Raises:
        ValueError: If user_id is invalid
    """
    pass


def _internal_helper():
    """Private module function."""
    pass

Package Structure

mypackage/
├── __init__.py           # Package initialization
├── __main__.py           # Makes package runnable (python -m mypackage)
├── config.py             # Configuration
├── constants.py          # Constants
├── exceptions.py         # Custom exceptions
├── models/               # Data models
│   ├── __init__.py
│   ├── user.py
│   └── product.py
├── services/             # Business logic
│   ├── __init__.py
│   ├── user_service.py
│   └── auth_service.py
├── api/                  # API layer
│   ├── __init__.py
│   ├── routes.py
│   └── schemas.py
└── utils/                # Utilities
    ├── __init__.py
    ├── validators.py
    └── helpers.py

Performance Optimization

Profiling

import cProfile
import pstats
from functools import wraps
import time

# Time function execution
def profile_time(func):
    """Decorator to profile execution time."""
    @wraps(func)
    def wrapper(*args, **kwargs):
        start = time.perf_counter()
        result = func(*args, **kwargs)
        elapsed = time.perf_counter() - start
        print(f"{func.__name__}: {elapsed:.4f}s")
        return result
    return wrapper

# CPU profiling
def profile_cpu(output_file='profile_stats.txt'):
    """Profile CPU usage."""
    profiler = cProfile.Profile()
    profiler.enable()
    
    # Code to profile
    expensive_function()
    
    profiler.disable()
    
    with open(output_file, 'w') as f:
        stats = pstats.Stats(profiler, stream=f)
        stats.sort_stats('cumulative')
        stats.print_stats()

# Memory profiling
from memory_profiler import profile

@profile
def memory_intensive_function():
    """Profile memory usage."""
    large_list = [i for i in range(1000000)]
    return sum(large_list)

Optimization Techniques

# Use __slots__ for memory efficiency
class Point:
    """Memory-efficient point class."""
    __slots__ = ['x', 'y']
    
    def __init__(self, x: float, y: float):
        self.x = x
        self.y = y

# Use generators for large datasets
def read_large_file(filename: str):
    """Read file line by line (memory efficient)."""
    with open(filename) as f:
        for line in f:
            yield line.strip()

# Use sets for membership testing
# O(1) average case vs O(n) for lists
large_set = set(range(1000000))
if 500000 in large_set:  # Fast
    pass

# Cache expensive computations
from functools import lru_cache

@lru_cache(maxsize=128)
def expensive_computation(n: int) -> int:
    """Cached Fibonacci."""
    if n < 2:
        return n
    return expensive_computation(n-1) + expensive_computation(n-2)

# Use list comprehensions over loops
# Faster: list comprehension
squares = [x**2 for x in range(1000)]

# Slower: loop with append
squares = []
for x in range(1000):
    squares.append(x**2)

# Use built-in functions (implemented in C)
# Faster
total = sum(numbers)

# Slower
total = 0
for num in numbers:
    total += num

Documentation

Docstring Styles

def calculate_average(numbers: List[float]) -> float:
    """
    Calculate the average of a list of numbers.
    
    Google Style:
    
    Args:
        numbers: List of numbers to average
        
    Returns:
        The arithmetic mean of the numbers
        
    Raises:
        ValueError: If the list is empty
        
    Examples:
        >>> calculate_average([1, 2, 3, 4, 5])
        3.0
        >>> calculate_average([10, 20])
        15.0
    """
    if not numbers:
        raise ValueError("Cannot calculate average of empty list")
    return sum(numbers) / len(numbers)


class User:
    """
    Represent a user in the system.
    
    Attributes:
        id: Unique user identifier
        username: User's chosen username
        email: User's email address
        created_at: Timestamp of user creation
        
    Example:
        >>> user = User(id=1, username="alice", email="alice@example.com")
        >>> print(user.username)
        alice
    """
    
    def __init__(self, id: int, username: str, email: str):
        """
        Initialize a new User.
        
        Args:
            id: Unique identifier
            username: User's username
            email: User's email address
        """
        self.id = id
        self.username = username
        self.email = email
        self.created_at = datetime.now()

Common Patterns

Singleton Pattern

class Singleton:
    """Singleton using metaclass."""
    _instances = {}
    
    def __new__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super().__new__(cls)
        return cls._instances[cls]

# Or using decorator (shown earlier)

Factory Pattern

from abc import ABC, abstractmethod

class Animal(ABC):
    """Abstract animal class."""
    
    @abstractmethod
    def speak(self) -> str:
        pass

class Dog(Animal):
    def speak(self) -> str:
        return "Woof!"

class Cat(Animal):
    def speak(self) -> str:
        return "Meow!"

class AnimalFactory:
    """Factory for creating animals."""
    
    _animals = {
        'dog': Dog,
        'cat': Cat
    }
    
    @classmethod
    def create(cls, animal_type: str) -> Animal:
        """Create animal by type."""
        animal_class = cls._animals.get(animal_type.lower())
        if not animal_class:
            raise ValueError(f"Unknown animal type: {animal_type}")
        return animal_class()

# Usage
dog = AnimalFactory.create('dog')
print(dog.speak())  # "Woof!"

Builder Pattern

class UserBuilder:
    """Builder for User objects."""
    
    def __init__(self):
        self._user = {}
    
    def with_id(self, user_id: int):
        self._user['id'] = user_id
        return self
    
    def with_username(self, username: str):
        self._user['username'] = username
        return self
    
    def with_email(self, email: str):
        self._user['email'] = email
        return self
    
    def build(self) -> User:
        """Build and validate user."""
        if 'id' not in self._user or 'username' not in self._user:
            raise ValueError("Missing required fields")
        return User(**self._user)

# Usage
user = (UserBuilder()
    .with_id(1)
    .with_username("alice")
    .with_email("alice@example.com")
    .build())

Python Pitfalls to Avoid

Mutable Default Arguments

# ❌ BAD - Mutable default
def add_item(item, items=[]):
    items.append(item)
    return items

# This causes unexpected behavior:
# add_item(1)  # [1]
# add_item(2)  # [1, 2] - Unexpected!

# ✅ GOOD - Use None and create new list
def add_item(item, items=None):
    if items is None:
        items = []
    items.append(item)
    return items

Late Binding Closures

# ❌ BAD - All functions reference same i
functions = [lambda: i for i in range(5)]
results = [f() for f in functions]  # [4, 4, 4, 4, 4]

# ✅ GOOD - Capture value immediately
functions = [lambda i=i: i for i in range(5)]
results = [f() for f in functions]  # [0, 1, 2, 3, 4]

Modifying List While Iterating

# ❌ BAD - Modifying list during iteration
numbers = [1, 2, 3, 4, 5]
for num in numbers:
    if num % 2 == 0:
        numbers.remove(num)  # Dangerous!

# ✅ GOOD - Create new list
numbers = [1, 2, 3, 4, 5]
numbers = [num for num in numbers if num % 2 != 0]

# Or iterate over copy
numbers = [1, 2, 3, 4, 5]
for num in numbers[:]:  # Iterate over copy
    if num % 2 == 0:
        numbers.remove(num)

Tools and Configuration

pyproject.toml

[project]
name = "myproject"
version = "0.1.0"
description = "My Python project"
requires-python = ">=3.8"
dependencies = [
    "requests>=2.28.0",
    "pydantic>=2.0.0",
]

[project.optional-dependencies]
dev = [
    "pytest>=7.0.0",
    "black>=23.0.0",
    "mypy>=1.0.0",
    "ruff>=0.1.0",
]

[tool.black]
line-length = 100
target-version = ['py38', 'py39', 'py310', 'py311']

[tool.mypy]
python_version = "3.8"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true

[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = "test_*.py"
python_functions = "test_*"
addopts = "-v --cov=myproject --cov-report=term-missing"

[tool.ruff]
line-length = 100
select = ["E", "F", "W", "C90", "I", "N", "UP", "B", "A", "C4", "PT"]

Conclusion

Python excellence comes from:

  • Understanding and applying idiomatic Python
  • Using modern language features effectively
  • Writing clear, maintainable code
  • Following PEP 8 and PEP 257
  • Proper error handling and testing
  • Performance awareness without premature optimization

When in doubt, import this and remember: Readability counts.