- name
- programming-python
- description
- Python programming skill based on PEP 8 and modern Python best practices - use for implementing Python code
# Python Programming Skill
Use this skill when writing or modifying Python code.
<IMPORTANT>
Follow [PEP 8 - Style Guide for Python Code](https://peps.python.org/pep-0008/) as the primary reference for style and conventions.
**Python 3.8+ Standard.** This project uses modern Python (3.8 or later). Use type hints, dataclasses, and modern features available in Python 3.8+.
**Type hints are REQUIRED.** All functions, methods, and class attributes must have type annotations. Use mypy for static type checking.
**Readability counts.** Python emphasizes clear, readable code. Follow the Zen of Python (PEP 20): "Explicit is better than implicit", "Simple is better than complex".
**All code MUST be unit testable.** Use dependency injection, avoid global state, and design for testability from the start.
**Guidelines override existing code style.** If you see existing code that violates the rules in this skill, apply these guidelines first and ignore the current project style. Do NOT propagate bad patterns just because they exist in the codebase.
</IMPORTANT>
## The Zen of Python (PEP 20)
Key principles to guide Python development:
- **Beautiful is better than ugly**
- **Explicit is better than implicit**
- **Simple is better than complex**
- **Complex is better than complicated**
- **Flat is better than nested**
- **Sparse is better than dense**
- **Readability counts**
- **Special cases aren't special enough to break the rules**
- **Errors should never pass silently**
- **In the face of ambiguity, refuse the temptation to guess**
## Code Style (PEP 8)
### Naming Conventions
```python
# Modules and packages: lowercase with underscores
import json_parser
from utils.data_processing import clean_data
# Classes: PascalCase
class UserAccount:
pass
class HTTPConnectionPool:
pass
# Functions and variables: snake_case
def calculate_total_price(items: list[Item]) -> float:
total_amount = sum(item.price for item in items)
return total_amount
# Constants: SCREAMING_SNAKE_CASE
MAX_CONNECTIONS = 100
DEFAULT_TIMEOUT = 30
# Private: leading underscore
class Database:
def _internal_method(self) -> None:
pass
def __very_private(self) -> None: # Name mangling
pass
# Protected (convention): single leading underscore
_module_level_private = "hidden"
```
### Indentation and Whitespace
```python
# Use 4 spaces per indentation level (NEVER tabs)
def long_function_name(
var_one: str,
var_two: int,
var_three: dict[str, Any],
) -> bool:
"""Hanging indent for function arguments."""
print(var_one)
return True
# Line length: max 79 characters for code, 72 for docstrings/comments
# Break long lines at logical points
result = some_function_that_takes_arguments(
argument1, argument2, argument3,
argument4, argument5
)
# Two blank lines before top-level classes and functions
class MyClass:
pass
def my_function() -> None:
pass
# One blank line between methods
class Example:
def method_one(self) -> None:
pass
def method_two(self) -> None:
pass
# Whitespace in expressions
# GOOD
spam(ham[1], {eggs: 2})
if x == 4:
print(x, y)
x, y = y, x
# BAD
spam( ham[ 1 ], { eggs: 2 } )
if x == 4 :
print(x , y)
x , y = y , x
```
### Imports
```python
# Standard library imports first, then third-party, then local
# Each group separated by blank line, alphabetically sorted
import os
import sys
from typing import Any, Optional
import numpy as np
import requests
from myproject.utils import helper
from myproject.models import User
# Avoid wildcard imports
from module import * # BAD
from module import specific_function # GOOD
# One import per line for regular imports
import os
import sys
# Multiple items OK for 'from' imports
from typing import Any, Dict, List, Optional
```
## Type Hints (REQUIRED)
### Basic Type Hints
```python
from typing import Any, Optional, Union
from collections.abc import Sequence, Mapping, Callable
# Variables
name: str = "Alice"
age: int = 30
is_active: bool = True
scores: list[int] = [95, 87, 91]
user_data: dict[str, Any] = {"name": "Alice", "age": 30}
# Functions
def greet(name: str) -> str:
return f"Hello, {name}!"
def process_data(
items: list[str],
batch_size: int = 10,
validate: bool = True,
) -> dict[str, int]:
"""Process items and return statistics."""
return {"processed": len(items), "batch_size": batch_size}
# Optional values (can be None)
def find_user(user_id: int) -> Optional[User]:
"""Returns User if found, None otherwise."""
return users.get(user_id)
# Union types (Python 3.10+: use | instead)
def parse_input(value: Union[str, int]) -> int:
"""Accept string or int, return int."""
return int(value)
# Python 3.10+ union syntax (preferred if available)
def parse_input(value: str | int) -> int:
return int(value)
```
### Advanced Type Hints
```python
from typing import TypeVar, Generic, Protocol, Literal
from collections.abc import Callable, Iterator, Iterable
# Callable types
Callback = Callable[[str, int], bool]
def register_handler(callback: Callback) -> None:
pass
# Generic types
T = TypeVar('T')
def first(items: list[T]) -> Optional[T]:
return items[0] if items else None
# Generic classes
class Stack(Generic[T]):
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()
# Protocol (structural subtyping / duck typing)
class Drawable(Protocol):
def draw(self) -> None:
...
def render(obj: Drawable) -> None:
obj.draw() # Any object with draw() method works
# Literal types
def set_mode(mode: Literal["read", "write", "append"]) -> None:
pass
# Type aliases
UserId = int
UserMap = dict[UserId, User]
users: UserMap = {1: User("Alice"), 2: User("Bob")}
```
### Type Checking with mypy
```python
# Run mypy to check types
# $ mypy your_module.py
# Type ignore comments (use sparingly)
result = legacy_function() # type: ignore[no-untyped-call]
# reveal_type for debugging (mypy only)
reveal_type(some_variable) # mypy will show the inferred type
```
## Modern Python Features (3.8+)
### f-strings (Formatted String Literals)
```python
# GOOD: f-strings (readable, fast)
name = "Alice"
age = 30
message = f"Hello, {name}! You are {age} years old."
formatted = f"Result: {value:.2f}" # With formatting
debug = f"{variable=}" # Python 3.8+ debug syntax
# BAD: old-style formatting
message = "Hello, %s! You are %d years old." % (name, age)
message = "Hello, {}! You are {} years old.".format(name, age)
```
### Dataclasses
```python
from dataclasses import dataclass, field
# Simple dataclass
@dataclass
class Point:
x: float
y: float
def distance(self) -> float:
return (self.x**2 + self.y**2) ** 0.5
p = Point(3.0, 4.0) # Auto-generated __init__
print(p) # Auto-generated __repr__
# With default values and field options
@dataclass
class User:
username: str
email: str
active: bool = True
roles: list[str] = field(default_factory=list) # Mutable defaults
_internal_id: int = field(default=0, repr=False, compare=False)
# Frozen (immutable) dataclass
@dataclass(frozen=True)
class Config:
host: str
port: int
timeout: float = 30.0
```
### Walrus Operator (:=) - Python 3.8+
```python
# GOOD: Assign and use in one expression
if (match := pattern.search(text)) is not None:
print(match.group(0))
# GOOD: In list comprehensions
filtered = [y for x in data if (y := transform(x)) is not None]
# GOOD: In while loops
while (line := file.readline()) != "":
process(line)
```
### Pattern Matching (Python 3.10+)
```python
def process_command(command: dict[str, Any]) -> str:
match command:
case {"action": "create", "item": item}:
return f"Creating {item}"
case {"action": "delete", "id": user_id}:
return f"Deleting user {user_id}"
case {"action": "update", "id": user_id, "data": data}:
return f"Updating {user_id} with {data}"
case _:
return "Unknown command"
```
### Context Managers
```python
# Built-in context managers
with open("file.txt", "r") as f:
content = f.read()
# Custom context manager
from contextlib import contextmanager
from typing import Iterator
@contextmanager
def database_transaction(db: Database) -> Iterator[None]:
"""Context manager for database transactions."""
db.begin()
try:
yield
db.commit()
except Exception:
db.rollback()
raise
# Usage
with database_transaction(db):
db.execute("INSERT INTO users ...")
db.execute("UPDATE accounts ...")
```
### Generators and Iterators
```python
from collections.abc import Iterator
# Generator function
def fibonacci(n: int) -> Iterator[int]:
"""Generate first n Fibonacci numbers."""
a, b = 0, 1
for _ in range(n):
yield a
a, b = b, a + b
# Generator expression (memory efficient)
squares = (x**2 for x in range(1000000)) # Lazy evaluation
sum_of_squares = sum(x**2 for x in range(1000))
# Avoid creating unnecessary lists
# BAD
total = sum([x**2 for x in range(1000)]) # Creates intermediate list
# GOOD
total = sum(x**2 for x in range(1000)) # Generator expression
```
## Error Handling
### Exceptions
```python
# GOOD: Specific exceptions
try:
result = risky_operation()
except FileNotFoundError as e:
logger.error(f"File not found: {e}")
raise
except ValueError as e:
logger.warning(f"Invalid value: {e}")
return default_value
finally:
cleanup()
# BAD: Bare except
try:
risky_operation()
except: # Catches everything, including KeyboardInterrupt!
pass
# GOOD: Specific exception catching
try:
process()
except (ValueError, TypeError) as e:
handle_error(e)
# Custom exceptions
class ValidationError(ValueError):
"""Raised when validation fails."""
pass
class APIError(Exception):
"""Base exception for API errors."""
def __init__(self, message: str, status_code: int) -> None:
super().__init__(message)
self.status_code = status_code
# Raising exceptions
def validate_age(age: int) -> None:
if age < 0:
raise ValidationError(f"Age cannot be negative: {age}")
if age > 150:
raise ValidationError(f"Age is unrealistic: {age}")
```
### Exception Chaining
```python
# Chain exceptions to preserve context
try:
result = parse_json(data)
except json.JSONDecodeError as e:
raise ValidationError("Invalid JSON data") from e
# Suppress chaining if irrelevant
try:
result = alternative_parser(data)
except Exception:
raise ValidationError("Parsing failed") from None
```
### EAFP vs LBYL
```python
# EAFP: Easier to Ask Forgiveness than Permission (Pythonic)
try:
value = dictionary[key]
except KeyError:
value = default
# LBYL: Look Before You Leap (not Pythonic)
if key in dictionary:
value = dictionary[key]
else:
value = default
# EAFP is preferred in Python for:
# 1. Better performance (one lookup vs two)
# 2. Race condition safety
# 3. More readable for typical case
```
## Docstrings
### Google Style (Recommended)
```python
def calculate_distance(
point1: tuple[float, float],
point2: tuple[float, float],
metric: str = "euclidean",
) -> float:
"""Calculate distance between two points.
Computes the distance between two 2D points using the specified
distance metric.
Args:
point1: First point as (x, y) coordinates.
point2: Second point as (x, y) coordinates.
metric: Distance metric to use. Options: "euclidean", "manhattan".
Defaults to "euclidean".
Returns:
The calculated distance as a float.
Raises:
ValueError: If metric is not supported.
Examples:
>>> calculate_distance((0, 0), (3, 4))
5.0
View on GitHub