python-skills 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (105) hide show
  1. python_skills/__init__.py +10 -0
  2. python_skills/__main__.py +6 -0
  3. python_skills/adapters/__init__.py +48 -0
  4. python_skills/adapters/agent_skills.py +415 -0
  5. python_skills/adapters/aider_adapter.py +226 -0
  6. python_skills/adapters/base.py +153 -0
  7. python_skills/adapters/claude.py +474 -0
  8. python_skills/adapters/cline.py +332 -0
  9. python_skills/adapters/codex.py +24 -0
  10. python_skills/adapters/continue_adapter.py +198 -0
  11. python_skills/adapters/cursor.py +327 -0
  12. python_skills/adapters/gemini.py +26 -0
  13. python_skills/adapters/goose.py +26 -0
  14. python_skills/adapters/junie.py +25 -0
  15. python_skills/adapters/kiro.py +382 -0
  16. python_skills/adapters/opencode.py +27 -0
  17. python_skills/adapters/roo.py +25 -0
  18. python_skills/adapters/universal.py +203 -0
  19. python_skills/adapters/vscode.py +27 -0
  20. python_skills/adapters/windsurf.py +26 -0
  21. python_skills/adapters/zed.py +27 -0
  22. python_skills/cli.py +326 -0
  23. python_skills/config.py +160 -0
  24. python_skills/detector.py +152 -0
  25. python_skills/installer.py +163 -0
  26. python_skills/markers.py +115 -0
  27. python_skills/skills/__init__.py +14 -0
  28. python_skills/skills/loader.py +171 -0
  29. python_skills/skills/metadata.py +152 -0
  30. python_skills/skills/registry.py +101 -0
  31. python_skills/state.py +204 -0
  32. python_skills-1.0.0.dist-info/METADATA +99 -0
  33. python_skills-1.0.0.dist-info/RECORD +105 -0
  34. python_skills-1.0.0.dist-info/WHEEL +4 -0
  35. python_skills-1.0.0.dist-info/entry_points.txt +2 -0
  36. python_skills-1.0.0.dist-info/licenses/LICENSE +21 -0
  37. skills/advanced_python.md +239 -0
  38. skills/anti_patterns/index.md +406 -0
  39. skills/comprehensions.md +167 -0
  40. skills/control_flow.md +175 -0
  41. skills/data_structures.md +243 -0
  42. skills/debugging/common_bugs.md +222 -0
  43. skills/debugging/inspection_techniques.md +249 -0
  44. skills/debugging/root_cause.md +203 -0
  45. skills/engineering/application_logging.md +195 -0
  46. skills/engineering/cli_apps.md +207 -0
  47. skills/engineering/configuration.md +218 -0
  48. skills/engineering/database.md +240 -0
  49. skills/engineering/dependency_management.md +205 -0
  50. skills/engineering/http_clients.md +267 -0
  51. skills/engineering/modules_packages.md +211 -0
  52. skills/engineering/packaging.md +197 -0
  53. skills/engineering/project_structure.md +155 -0
  54. skills/engineering/pyproject_toml.md +302 -0
  55. skills/engineering/virtual_environments.md +206 -0
  56. skills/functions.md +244 -0
  57. skills/generation/async_concurrency.md +291 -0
  58. skills/generation/error_handling.md +276 -0
  59. skills/generation/protocols_generics.md +243 -0
  60. skills/generation/type_hints.md +290 -0
  61. skills/generation/validation_pipeline.md +274 -0
  62. skills/generation/workflow.md +190 -0
  63. skills/oop.md +228 -0
  64. skills/quality/abstractions.md +154 -0
  65. skills/quality/comments.md +177 -0
  66. skills/quality/documentation.md +176 -0
  67. skills/quality/duplication.md +137 -0
  68. skills/quality/maintainability.md +142 -0
  69. skills/quality/naming.md +171 -0
  70. skills/quality/quality_functions.md +245 -0
  71. skills/quality/readability.md +239 -0
  72. skills/quality/type_annotations.md +192 -0
  73. skills/refactoring/behavior_preservation.md +157 -0
  74. skills/refactoring/incremental.md +187 -0
  75. skills/refactoring/interface_stability.md +199 -0
  76. skills/refactoring/safe_refactoring.md +206 -0
  77. skills/security/auth_boundaries.md +200 -0
  78. skills/security/command_injection.md +207 -0
  79. skills/security/dependency_risks.md +282 -0
  80. skills/security/file_handling.md +156 -0
  81. skills/security/input_validation.md +190 -0
  82. skills/security/path_traversal.md +172 -0
  83. skills/security/secrets.md +171 -0
  84. skills/security/sql_injection.md +188 -0
  85. skills/security/unsafe_deserialization.md +164 -0
  86. skills/stdlib/argparse.md +178 -0
  87. skills/stdlib/collections.md +212 -0
  88. skills/stdlib/datetime.md +187 -0
  89. skills/stdlib/functools.md +238 -0
  90. skills/stdlib/itertools.md +183 -0
  91. skills/stdlib/json.md +162 -0
  92. skills/stdlib/logging.md +185 -0
  93. skills/stdlib/os_sys.md +184 -0
  94. skills/stdlib/pathlib.md +218 -0
  95. skills/stdlib/re.md +171 -0
  96. skills/stdlib/statistics.md +112 -0
  97. skills/stdlib/subprocess.md +211 -0
  98. skills/testing/async_tests.md +249 -0
  99. skills/testing/coverage.md +168 -0
  100. skills/testing/edge_cases.md +197 -0
  101. skills/testing/fixtures_mocks.md +203 -0
  102. skills/testing/organization.md +205 -0
  103. skills/testing/parameterized.md +174 -0
  104. skills/testing/regression_tests.md +165 -0
  105. skills/variables_types.md +107 -0
@@ -0,0 +1,190 @@
1
+ ---
2
+ name: security_input_validation
3
+ purpose: Validate all external input at system boundaries
4
+ category: security
5
+ triggers:
6
+ - validation
7
+ - input
8
+ - pydantic
9
+ - boundary
10
+ - sanitize
11
+ - allowlist
12
+ dependencies:
13
+ - security/sql_injection.md
14
+ - security/command_injection.md
15
+ - security/path_traversal.md
16
+ - security/unsafe_deserialization.md
17
+ - generation/error_handling.md
18
+ priority: supporting
19
+ estimated_tokens: 1700
20
+ ---
21
+ # Security: Input Validation
22
+
23
+ **Purpose**: Validate all external input at system boundaries.
24
+
25
+ **When to use**: All code handling user input, API requests, file uploads, config.
26
+
27
+ ---
28
+
29
+ ## Core Rules
30
+
31
+ ### Validate at Boundaries
32
+ ```python
33
+ # API boundary
34
+ @app.post("/users")
35
+ def create_user(request: CreateUserRequest) -> UserResponse:
36
+ # Request validated by Pydantic/FastAPI automatically
37
+ return user_service.create(request)
38
+
39
+ # CLI boundary
40
+ def main(args: list[str]) -> int:
41
+ parsed = parse_args(args) # argparse validates
42
+ config = load_config(parsed.config_file) # Validates file
43
+ ...
44
+
45
+ # Config boundary
46
+ def load_config(path: Path) -> Config:
47
+ raw = tomllib.load(path)
48
+ return Config.model_validate(raw) # Pydantic validates
49
+ ```
50
+
51
+ ### Validation Libraries
52
+ | Use Case | Library |
53
+ |----------|---------|
54
+ | API/Config/Data | `pydantic` (v2) |
55
+ | Simple CLI | `argparse` + custom types |
56
+ | JSON Schema | `jsonschema` |
57
+ | Form data | `wtforms` |
58
+
59
+ ### Pydantic Patterns
60
+ ```python
61
+ from pydantic import BaseModel, Field, field_validator, EmailStr
62
+ from typing import Annotated
63
+
64
+ class CreateUserRequest(BaseModel):
65
+ email: EmailStr
66
+ name: Annotated[str, Field(min_length=2, max_length=100)]
67
+ age: Annotated[int, Field(ge=13, le=120)]
68
+ tags: list[str] = Field(default_factory=list)
69
+
70
+ @field_validator("name")
71
+ @classmethod
72
+ def name_no_special_chars(cls, v: str) -> str:
73
+ if not v.replace(" ", "").isalnum():
74
+ raise ValueError("Name must be alphanumeric")
75
+ return v.strip()
76
+
77
+ # Usage
78
+ def create_user(data: CreateUserRequest) -> User:
79
+ # data is guaranteed valid here
80
+ ...
81
+ ```
82
+
83
+ ### Allowlist Over Blocklist
84
+ ```python
85
+ # GOOD — allowlist
86
+ ALLOWED_EXTENSIONS = {".jpg", ".png", ".pdf"}
87
+ def validate_extension(filename: str) -> None:
88
+ ext = Path(filename).suffix.lower()
89
+ if ext not in ALLOWED_EXTENSIONS:
90
+ raise ValidationError("file", f"Extension not allowed: {ext}")
91
+
92
+ # BAD — blocklist (incomplete)
93
+ def validate_extension(filename: str) -> None:
94
+ if filename.endswith(".exe"):
95
+ raise ValidationError("file", "Executable not allowed")
96
+ # Misses .bat, .sh, .php, .jar, etc.
97
+ ```
98
+
99
+ ### Size Limits
100
+ ```python
101
+ # Request body
102
+ app = FastAPI()
103
+ app.add_middleware(MaxBodySizeMiddleware, max_size=10_000_000) # 10MB
104
+
105
+ # File upload
106
+ def upload(file: UploadFile) -> None:
107
+ if file.size > MAX_FILE_SIZE:
108
+ raise ValidationError("file", "File too large")
109
+
110
+ # Stream, don't load entirely
111
+ while chunk := file.read(8192):
112
+ process(chunk)
113
+ ```
114
+
115
+ ### Sanitization
116
+ ```python
117
+ # HTML output
118
+ from markupsafe import escape
119
+ safe_html = escape(user_input)
120
+
121
+ # SQL — use parameterized queries (see sql_injection.md)
122
+ # Path — use pathlib.resolve() + is_relative_to() (see path_traversal.md)
123
+ # Shell — never use shell=True (see command_injection.md)
124
+ ```
125
+
126
+ ---
127
+
128
+ ## Decision Rules
129
+
130
+ | Input Type | Validation |
131
+ |------------|------------|
132
+ | API request | Pydantic model |
133
+ | Config file | Pydantic model |
134
+ | CLI args | argparse + custom types |
135
+ | File upload | Size + type + content validation |
136
+ | Database input | ORM/parameterized queries |
137
+ | User content (display) | Escape on output |
138
+
139
+ ---
140
+
141
+ ## Preferred Patterns
142
+
143
+ ```python
144
+ # Centralized validation
145
+ class ValidationError(Exception):
146
+ def __init__(self, field: str, message: str):
147
+ self.field = field
148
+ self.message = message
149
+ super().__init__(f"{field}: {message}")
150
+
151
+ def validate_and_load[T](
152
+ model: type[BaseModel],
153
+ data: dict,
154
+ context: dict | None = None,
155
+ ) -> T:
156
+ try:
157
+ return model.model_validate(data, context=context)
158
+ except ValidationError as e:
159
+ # Convert to domain exceptions
160
+ errors = e.errors()
161
+ raise ValidationError(errors[0]["loc"][0], errors[0]["msg"]) from e
162
+ ```
163
+
164
+ ---
165
+
166
+ ## Avoid
167
+
168
+ - Trusting any external input
169
+ - Validation only in UI (client-side only)
170
+ - Blocklist-based validation
171
+ - No size limits on uploads/requests
172
+ - Manual string parsing for structured data
173
+
174
+ ---
175
+
176
+ ## Validation Considerations
177
+
178
+ - Test with malicious inputs (injection, oversized, malformed)
179
+ - Fuzz testing for parsers
180
+ - Validate error messages don't leak info
181
+
182
+ ---
183
+
184
+ ## Related Skills
185
+
186
+ - `security/sql_injection.md`
187
+ - `security/command_injection.md`
188
+ - `security/path_traversal.md`
189
+ - `security/unsafe_deserialization.md`
190
+ - `generation/error_handling.md`
@@ -0,0 +1,172 @@
1
+ # Security: Path Traversal Prevention
2
+
3
+ **Purpose**: Prevent directory traversal attacks when handling file paths.
4
+
5
+ **When to use**: File uploads, path parameters, file serving, archive extraction.
6
+
7
+ ---
8
+
9
+ ## Core Rules
10
+
11
+ ### Always Resolve and Validate
12
+ ```python
13
+ from pathlib import Path
14
+
15
+ def safe_path(user_input: str, base_dir: Path) -> Path:
16
+ """Resolve path and ensure it's within base directory."""
17
+ base = base_dir.resolve()
18
+ requested = (base / user_input).resolve()
19
+
20
+ # Critical: check AFTER resolve (handles symlinks)
21
+ if not requested.is_relative_to(base):
22
+ raise ValueError(f"Path traversal attempt: {user_input}")
23
+
24
+ return requested
25
+
26
+ # Usage
27
+ @app.get("/files/{path:path}")
28
+ def serve_file(path: str):
29
+ safe = safe_path(path, FILES_ROOT)
30
+ return FileResponse(safe)
31
+ ```
32
+
33
+ ### Common Attack Vectors
34
+ ```python
35
+ # These all attempt to escape:
36
+ "../../etc/passwd"
37
+ "..\\..\\windows\\system32"
38
+ "%2e%2e%2f" # URL encoded
39
+ "....//" # Double dots
40
+ "subdir/../../etc/passwd"
41
+ "symlink_to_root/target"
42
+ ```
43
+
44
+ ### Archive Extraction (Critical)
45
+ ```python
46
+ import tarfile
47
+ import zipfile
48
+
49
+ def safe_extract_tar(tar_path: Path, dest: Path) -> None:
50
+ dest = dest.resolve()
51
+ with tarfile.open(tar_path) as tar:
52
+ for member in tar.getmembers():
53
+ member_path = (dest / member.name).resolve()
54
+ if not member_path.is_relative_to(dest):
55
+ raise ValueError(f"Path traversal in archive: {member.name}")
56
+ tar.extractall(dest) # Safe after validation
57
+
58
+ def safe_extract_zip(zip_path: Path, dest: Path) -> None:
59
+ dest = dest.resolve()
60
+ with zipfile.ZipFile(zip_path) as zf:
61
+ for name in zf.namelist():
62
+ member_path = (dest / name).resolve()
63
+ if not member_path.is_relative_to(dest):
64
+ raise ValueError(f"Path traversal in archive: {name}")
65
+ zf.extractall(dest)
66
+ ```
67
+
68
+ ### File Upload
69
+ ```python
70
+ def save_upload(file: UploadFile, upload_dir: Path) -> Path:
71
+ # Validate filename
72
+ filename = Path(file.filename).name # Strip directory components
73
+ if not filename:
74
+ raise ValueError("Invalid filename")
75
+
76
+ # Allowlist extension
77
+ allowed = {".jpg", ".png", ".pdf", ".txt"}
78
+ if filename.suffix.lower() not in allowed:
79
+ raise ValueError("File type not allowed")
80
+
81
+ # Generate safe name (UUID + extension)
82
+ safe_name = f"{uuid4()}{filename.suffix.lower()}"
83
+ dest = safe_path(safe_name, upload_dir)
84
+
85
+ # Stream write (memory efficient)
86
+ with dest.open("wb") as f:
87
+ while chunk := file.file.read(8192):
88
+ f.write(chunk)
89
+
90
+ return dest
91
+ ```
92
+
93
+ ### Symlink Safety
94
+ ```python
95
+ def read_file_safe(path: Path, base: Path) -> bytes:
96
+ """Read file, following symlinks but validating final target."""
97
+ resolved = path.resolve()
98
+ base_resolved = base.resolve()
99
+
100
+ if not resolved.is_relative_to(base_resolved):
101
+ raise ValueError("Path traversal via symlink")
102
+
103
+ # Optional: reject symlinks entirely
104
+ if path.is_symlink():
105
+ raise ValueError("Symlinks not allowed")
106
+
107
+ return resolved.read_bytes()
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Decision Rules
113
+
114
+ | Operation | Protection |
115
+ |-----------|------------|
116
+ | Serve static file | `safe_path` + `is_relative_to` |
117
+ | User upload | Strip path, allowlist ext, UUID name |
118
+ | Archive extract | Validate each member before extract |
119
+ | Config file read | Fixed known paths only |
120
+ | Temp file | `tempfile.mkstemp` (secure) |
121
+
122
+ ---
123
+
124
+ ## Preferred Patterns
125
+
126
+ ```python
127
+ # Centralized path validator
128
+ class PathValidator:
129
+ def __init__(self, base: Path):
130
+ self.base = base.resolve()
131
+
132
+ def validate(self, user_path: str | Path) -> Path:
133
+ requested = (self.base / user_path).resolve()
134
+ if not requested.is_relative_to(self.base):
135
+ raise SecurityError("Path traversal attempt")
136
+ return requested
137
+
138
+ def validate_exists(self, user_path: str | Path) -> Path:
139
+ path = self.validate(user_path)
140
+ if not path.exists():
141
+ raise FileNotFoundError(path)
142
+ return path
143
+ ```
144
+
145
+ ---
146
+
147
+ ## Avoid
148
+
149
+ - `os.path.join` without validation
150
+ - `Path(user_input)` directly
151
+ - `../` in user-controlled paths
152
+ - Extracting archives without member validation
153
+ - Serving files from user input without validation
154
+
155
+ ---
156
+
157
+ ## Validation Considerations
158
+
159
+ - Test with traversal payloads
160
+ - Test with symlinks
161
+ - Test with URL encoding
162
+ - Test with null bytes (`\0`)
163
+ - `bandit` B108, B306 checks
164
+
165
+ ---
166
+
167
+ ## Related Skills
168
+
169
+ - `security/command_injection.md`
170
+ - `security/input_validation.md`
171
+ - `stdlib/pathlib.md`
172
+ - `stdlib/subprocess.md`
@@ -0,0 +1,171 @@
1
+ # Security: Secrets Management
2
+
3
+ **Purpose**: Handle secrets (API keys, passwords, tokens) securely.
4
+
5
+ **When to use**: Any code dealing with credentials or sensitive data.
6
+
7
+ ---
8
+
9
+ ## Core Rules
10
+
11
+ ### Never Hardcode Secrets
12
+ ```python
13
+ # NEVER
14
+ API_KEY = "sk_live_abc123"
15
+ DB_PASSWORD = "supersecret"
16
+
17
+ # NEVER in config files committed to git
18
+ # config.yaml
19
+ # database:
20
+ # password: "supersecret"
21
+ ```
22
+
23
+ ### Environment Variables
24
+ ```python
25
+ import os
26
+ from pydantic_settings import BaseSettings
27
+
28
+ class Settings(BaseSettings):
29
+ database_url: str
30
+ api_key: str
31
+ jwt_secret: str
32
+
33
+ model_config = SettingsConfigDict(
34
+ env_file=".env",
35
+ env_file_encoding="utf-8",
36
+ extra="ignore",
37
+ )
38
+
39
+ settings = Settings() # Loads from env vars + .env
40
+ ```
41
+
42
+ ### .env File Pattern
43
+ ```bash
44
+ # .env.example (COMMITTED)
45
+ DATABASE_URL=postgresql://user:pass@localhost/db
46
+ API_KEY=
47
+ JWT_SECRET=
48
+
49
+ # .env (GITIGNORED - local only)
50
+ DATABASE_URL=postgresql://prod:realpass@db/prod
51
+ API_KEY=sk_live_realkey
52
+ JWT_SECRET=supersecretkey
53
+ ```
54
+
55
+ ### Secret Managers (Production)
56
+ ```python
57
+ # AWS Secrets Manager
58
+ import boto3
59
+
60
+ def get_secret(name: str) -> str:
61
+ client = boto3.client("secretsmanager")
62
+ response = client.get_secret_value(SecretId=name)
63
+ return response["SecretString"]
64
+
65
+ # HashiCorp Vault
66
+ import hvac
67
+
68
+ client = hvac.Client(url="https://vault.example.com")
69
+ secret = client.secrets.kv.v2.read_secret_version(path="app/config")
70
+ ```
71
+
72
+ ### In Memory Only
73
+ ```python
74
+ # Don't log secrets
75
+ logger.info("Connecting to database", extra={"url": sanitize_url(db_url)})
76
+
77
+ def sanitize_url(url: str) -> str:
78
+ # postgresql://user:pass@host/db -> postgresql://user:***@host/db
79
+ from urllib.parse import urlparse, urlunparse
80
+ parsed = urlparse(url)
81
+ if parsed.password:
82
+ netloc = f"{parsed.username}:***@{parsed.hostname}"
83
+ if parsed.port:
84
+ netloc += f":{parsed.port}"
85
+ return urlunparse(parsed._replace(netloc=netloc))
86
+ return url
87
+ ```
88
+
89
+ ### Key Rotation
90
+ ```python
91
+ # Support multiple keys for rotation
92
+ class Settings(BaseSettings):
93
+ jwt_secrets: list[str] = Field(default_factory=list) # Current + previous
94
+ current_jwt_secret_index: int = 0
95
+
96
+ @property
97
+ def jwt_secret(self) -> str:
98
+ return self.jwt_secrets[self.current_jwt_secret_index]
99
+
100
+ def verify_token(self, token: str) -> dict:
101
+ # Try all secrets for backward compatibility
102
+ for secret in self.jwt_secrets:
103
+ try:
104
+ return jwt.decode(token, secret, algorithms=["HS256"])
105
+ except jwt.InvalidTokenError:
106
+ continue
107
+ raise InvalidTokenError()
108
+ ```
109
+
110
+ ---
111
+
112
+ ## Decision Rules
113
+
114
+ | Environment | Approach |
115
+ |-------------|----------|
116
+ | Local dev | `.env` file (gitignored) |
117
+ | CI/CD | Platform secrets (GitHub Actions, GitLab CI) |
118
+ | Kubernetes | Secrets + External Secrets Operator |
119
+ | Cloud | AWS Secrets Manager, GCP Secret Manager, Azure Key Vault |
120
+ | Serverless | Platform env vars + secret manager |
121
+
122
+ ---
123
+
124
+ ## Preferred Patterns
125
+
126
+ ```python
127
+ # Single settings instance
128
+ _settings: Settings | None = None
129
+
130
+ def get_settings() -> Settings:
131
+ global _settings
132
+ if _settings is None:
133
+ _settings = Settings()
134
+ return _settings
135
+
136
+ # Dependency injection for testing
137
+ def create_app(settings: Settings | None = None) -> FastAPI:
138
+ settings = settings or get_settings()
139
+ app = FastAPI()
140
+ app.state.settings = settings
141
+ return app
142
+ ```
143
+
144
+ ---
145
+
146
+ ## Avoid
147
+
148
+ - Committing `.env` with real secrets
149
+ - Printing/logging secrets
150
+ - Passing secrets in URLs (query params, paths)
151
+ - Storing secrets in code (even encoded)
152
+ - Single key without rotation plan
153
+ - Secrets in Docker images
154
+
155
+ ---
156
+
157
+ ## Validation Considerations
158
+
159
+ - `git-secrets` / `truffleHog` / `gitleaks` in CI
160
+ - No secrets in logs (test with log capture)
161
+ - Secret rotation tested
162
+ - Least privilege for secret access
163
+
164
+ ---
165
+
166
+ ## Related Skills
167
+
168
+ - `security/input_validation.md`
169
+ - `engineering/configuration.md`
170
+ - `engineering/logging.md`
171
+ - `engineering/virtual_environments.md`
@@ -0,0 +1,188 @@
1
+ # Security: SQL Injection Prevention
2
+
3
+ **Purpose**: Prevent SQL injection through proper query parameterization.
4
+
5
+ **When to use**: All database interactions.
6
+
7
+ ---
8
+
9
+ ## Core Rules
10
+
11
+ ### Always Use Parameterized Queries
12
+ ```python
13
+ # CORRECT — asyncpg
14
+ async with pool.acquire() as conn:
15
+ await conn.execute(
16
+ "INSERT INTO users (email, name) VALUES ($1, $2)",
17
+ email, name
18
+ )
19
+
20
+ # CORRECT — psycopg
21
+ with conn.cursor() as cur:
22
+ cur.execute(
23
+ "INSERT INTO users (email, name) VALUES (%s, %s)",
24
+ (email, name)
25
+ )
26
+
27
+ # CORRECT — SQLAlchemy
28
+ session.execute(
29
+ text("SELECT * FROM users WHERE email = :email"),
30
+ {"email": email}
31
+ )
32
+
33
+ # CORRECT — ORM
34
+ User.query.filter_by(email=email).first()
35
+ ```
36
+
37
+ ### Never Use String Interpolation
38
+ ```python
39
+ # WRONG — SQL INJECTION!
40
+ cursor.execute(f"SELECT * FROM users WHERE email = '{email}'")
41
+ cursor.execute("SELECT * FROM users WHERE email = '" + email + "'")
42
+ cursor.execute("SELECT * FROM users WHERE id = " + str(user_id))
43
+
44
+ # WRONG — .format()
45
+ cursor.execute("SELECT * FROM users WHERE email = '{}'".format(email))
46
+ ```
47
+
48
+ ### Dynamic Queries (Safe Patterns)
49
+ ```python
50
+ # WHERE IN with variable length
51
+ ids = [1, 2, 3]
52
+ placeholders = ", ".join(["%s"] * len(ids))
53
+ query = f"SELECT * FROM users WHERE id IN ({placeholders})"
54
+ cursor.execute(query, ids) # Parameters still parameterized!
55
+
56
+ # Optional filters
57
+ conditions = []
58
+ params = []
59
+ if name:
60
+ conditions.append("name ILIKE %s")
61
+ params.append(f"%{name}%")
62
+ if email:
63
+ conditions.append("email = %s")
64
+ params.append(email)
65
+
66
+ query = "SELECT * FROM users"
67
+ if conditions:
68
+ query += " WHERE " + " AND ".join(conditions)
69
+ cursor.execute(query, params)
70
+ ```
71
+
72
+ ### ORM Safety
73
+ ```python
74
+ # SQLAlchemy — safe
75
+ User.query.filter(User.email == email).all()
76
+ session.query(User).filter_by(email=email).first()
77
+
78
+ # Django — safe
79
+ User.objects.filter(email=email)
80
+
81
+ # Unsafe in ORM — raw()
82
+ User.objects.raw("SELECT * FROM users WHERE email = '%s'" % email) # BAD!
83
+ User.objects.raw("SELECT * FROM users WHERE email = %s", [email]) # GOOD
84
+ ```
85
+
86
+ ### Identifier Quoting (Table/Column Names)
87
+ ```python
88
+ # Can't parameterize identifiers — validate against allowlist
89
+ ALLOWED_TABLES = {"users", "orders", "products"}
90
+ ALLOWED_COLUMNS = {"id", "email", "name", "created_at"}
91
+
92
+ def query_table(table: str, column: str, value: str):
93
+ if table not in ALLOWED_TABLES:
94
+ raise ValueError("Invalid table")
95
+ if column not in ALLOWED_COLUMNS:
96
+ raise ValueError("Invalid column")
97
+
98
+ # Safe: validated identifiers
99
+ query = f"SELECT * FROM {table} WHERE {column} = %s"
100
+ cursor.execute(query, (value,))
101
+ ```
102
+
103
+ ---
104
+
105
+ ## Decision Rules
106
+
107
+ | Query Type | Safe Pattern |
108
+ |------------|--------------|
109
+ | Static query | Parameterized |
110
+ | Dynamic WHERE | Build conditions, parameterize values |
111
+ | IN clause | Generate placeholders, parameterize values |
112
+ | Table/column names | Allowlist validation |
113
+ | Complex reporting | Views / stored procedures / CTEs |
114
+
115
+ ---
116
+
117
+ ## Preferred Patterns
118
+
119
+ ```python
120
+ # Repository pattern encapsulates safety
121
+ class UserRepository:
122
+ def __init__(self, pool: asyncpg.Pool):
123
+ self.pool = pool
124
+
125
+ async def find_by_email(self, email: str) -> User | None:
126
+ async with self.pool.acquire() as conn:
127
+ row = await conn.fetchrow(
128
+ "SELECT * FROM users WHERE email = $1",
129
+ email
130
+ )
131
+ return User(**row) if row else None
132
+
133
+ async def search(
134
+ self,
135
+ name: str | None = None,
136
+ email: str | None = None,
137
+ limit: int = 100,
138
+ ) -> list[User]:
139
+ conditions = []
140
+ params = []
141
+ param_num = 1
142
+
143
+ if name:
144
+ conditions.append(f"name ILIKE ${param_num}")
145
+ params.append(f"%{name}%")
146
+ param_num += 1
147
+ if email:
148
+ conditions.append(f"email = ${param_num}")
149
+ params.append(email)
150
+ param_num += 1
151
+
152
+ query = "SELECT * FROM users"
153
+ if conditions:
154
+ query += " WHERE " + " AND ".join(conditions)
155
+ query += f" LIMIT ${param_num}"
156
+ params.append(limit)
157
+
158
+ async with self.pool.acquire() as conn:
159
+ rows = await conn.fetch(query, *params)
160
+ return [User(**row) for row in rows]
161
+ ```
162
+
163
+ ---
164
+
165
+ ## Avoid
166
+
167
+ - Any string concatenation in SQL
168
+ - `f-strings` in SQL
169
+ - `.format()` in SQL
170
+ - `%` formatting in SQL (except parameterized drivers)
171
+ - Dynamic ORDER BY without validation
172
+
173
+ ---
174
+
175
+ ## Validation Considerations
176
+
177
+ - Code review: grep for `f"SELECT` `f"INSERT` `f"UPDATE` `f"DELETE`
178
+ - `bandit` SQL injection checks
179
+ - SQLMap testing
180
+ - ORM raw query audit
181
+
182
+ ---
183
+
184
+ ## Related Skills
185
+
186
+ - `security/input_validation.md`
187
+ - `engineering/database.md`
188
+ - `generation/error_handling.md`