standardizing-python
Python code standards enforced across all skills. Reference skill for type annotations, naming conventions, and linting rules.
SKILL.md
| Name | standardizing-python |
| Description | Python code standards enforced across all skills. Reference skill for type annotations, naming conventions, and linting rules. |
name: standardizing-python description: Python code standards enforced across all skills. Reference skill for type annotations, naming conventions, and linting rules. allowed-tools: Read
<objective> Python code standards enforced by linters (ruff, mypy) and manual review. Defines what `/coding-python` must follow and `/reviewing-python` enforces. </objective><quick_start> Reference this skill when coding or reviewing Python. Standards grouped by category with ruff rule codes. All examples show correct (✅) and incorrect (❌) patterns. </quick_start>
<success_criteria> Code follows these standards when all ruff rules and mypy checks pass. See summary table at the end for the complete rejection criteria with rule codes. </success_criteria>
<reference_note>
This is a reference skill. Other Python skills reference these standards. You typically don't invoke this directly—invoke /coding-python, /testing-python, or /reviewing-python instead.
These standards apply to ALL Python code: production and test code alike. </reference_note>
<type_annotations>
ALL functions require complete type annotations. No exceptions.
# ✅ REQUIRED: Return types on ALL functions
def process_items(
items: list[str],
config: Config,
logger: logging.Logger,
) -> ProcessResult:
"""Process items according to config."""
# ✅ REQUIRED: -> None on functions that return nothing
def test_validates_input(self) -> None:
result = validate("test")
assert result.valid
# ✅ REQUIRED: -> None on __init__
def __init__(self, config: Config) -> None:
self.config = config
# ✅ REQUIRED: Type annotations on ALL parameters
def test_creates_file(self, tmp_path: Path) -> None:
file = tmp_path / "test.txt"
assert not file.exists()
# ✅ REQUIRED: Return types on fixtures
@pytest.fixture
def config(tmp_path: Path) -> Config:
return Config(path=tmp_path)
# ❌ REJECTED: Missing return type (ANN201)
def test_something(self):
pass
# ❌ REJECTED: Missing parameter type (ANN001)
def test_with_fixture(self, tmp_path) -> None:
pass
# ❌ REJECTED: Missing __init__ return type (ANN204)
def __init__(self, config: Config):
self.config = config
Ruff rules enforced:
| Rule | What it catches |
|---|---|
| ANN001 | Missing type annotation on parameter |
| ANN201 | Missing return type on public function |
| ANN204 | Missing return type on __init__ |
</type_annotations>
<named_constants>
Test values and configuration must use named constants, not inline literals.
# ✅ REQUIRED: Named constants at module level
VALID_SCORE = 85
MIN_SCORE = 0
MAX_SCORE = 100
VALID_INPUT = "simple"
EXPECTED_RESULT = 42
class TestScoreValidation:
def test_accepts_valid_score(self) -> None:
assert validate_score(VALID_SCORE) is True
def test_rejects_above_maximum(self) -> None:
assert validate_score(MAX_SCORE + 1) is False
# ❌ REJECTED: Magic values (PLR2004)
class TestScoreValidationBad:
def test_accepts_valid_score(self) -> None:
assert validate_score(85) is True # What is 85?
def test_rejects_above_maximum(self) -> None:
assert validate_score(101) is False # Magic number
Why named constants matter:
- Sharing between tests and production code
- Clear documentation of what values mean
- Easy updates when requirements change
- Self-documenting test intent
PLR2004 exemptions: Ruff's magic value rule already exempts common idiomatic values: 0, 1, "", and "__main__". You don't need constants for these.
# ✅ OK: Idiomatic values are exempt
assert len(results) == 0
assert count == 1
if __name__ == "__main__":
main()
</named_constants>
<naming_conventions>
Lowercase Argument Names (N803)
# ❌ REJECTED: Uppercase argument names
def __init__(self, domain: ClockDomain, WIDTH: int = 8) -> None:
pass
# ✅ REQUIRED: Lowercase argument names
def __init__(self, domain: ClockDomain, width: int = 8) -> None:
pass
Avoid Shadowing Builtins
# ❌ BAD: Shadows builtin `input`
@pytest.mark.parametrize("input,expected", TEST_CASES)
def test_processing(self, input: str, expected: int) -> None:
pass
# ✅ GOOD: Descriptive name, no shadowing
@pytest.mark.parametrize(("input_val", "expected"), TEST_CASES)
def test_processing(self, input_val: str, expected: int) -> None:
pass
Why avoid input as a parameter name:
- Shadows Python's builtin
input()function - Causes A002 (argument shadows builtin) lint errors
- Makes code confusing if you need the actual
input()function - The tuple form
("input_val", "expected")is also preferred by pytest for clarity
</naming_conventions>
<s101_policy>
Ruff's S101 rule flags assert statements because they can be disabled with Python's -O flag.
Policy: assert is ACCEPTED in test files because:
- pytest rewrites assertions for better error messages
- Tests are never run with
-Ooptimization - The alternative (
if not x: raise AssertionError) adds noise
Required project configuration in pyproject.toml:
[tool.ruff.lint.per-file-ignores]
"**/test_*.py" = ["S101"]
"**/tests/**/*.py" = ["S101"]
If the project hasn't configured this, tests will fail linting. Fix by adding the config, not by avoiding assert.
</s101_policy>
<type_strictness>
# ❌ REJECTED: Unqualified Any — hides real types
def process(data: Any) -> Any: ...
# ✅ REQUIRED: Use concrete types or justify Any
def process(data: dict[str, str]) -> ProcessResult: ...
# ❌ REJECTED: type: ignore without explanation
result = cast(str, value) # type: ignore
# ✅ REQUIRED: Explain what's being suppressed and why
result = cast(str, value) # type: ignore[no-untyped-call] # third-party lib missing stubs
Rules:
| Rule | What it catches |
|---|---|
| mypy strict | Unqualified Any usage |
| (manual review) | # type: ignore without justification |
</type_strictness>
<modern_syntax>
# ❌ REJECTED: Old-style type unions (UP007)
from typing import Optional, Union
def get_user(id: int) -> Optional[User]: ...
def process(value: Union[str, int]) -> None: ...
# ✅ REQUIRED: Modern union syntax
def get_user(id: int) -> User | None: ...
def process(value: str | int) -> None: ...
# ❌ REJECTED: Old-style generic types (UP006)
from typing import List, Dict, Tuple
def get_users() -> List[User]: ...
def get_config() -> Dict[str, Any]: ...
# ✅ REQUIRED: Lowercase generics
def get_users() -> list[User]: ...
def get_config() -> dict[str, Any]: ...
Ruff rules enforced:
| Rule | What it catches |
|---|---|
| UP006 | List, Dict, Tuple instead of list, dict, tuple |
| UP007 | Optional[X], Union[X, Y] instead of X | None, X | Y |
</modern_syntax>
<error_handling>
# ❌ REJECTED: Bare except (E722)
try:
process()
except:
pass
# ❌ REJECTED: Swallowing all errors (S110)
try:
process()
except Exception:
pass
# ✅ REQUIRED: Catch specific exceptions
try:
process()
except ValueError as e:
log.error("Invalid input: %s", e)
raise
Ruff rules enforced:
| Rule | What it catches |
|---|---|
| E722 | Bare except: clause |
| S110 | try-except-pass on broad exception |
</error_handling>
<security>
# ❌ REJECTED: Hardcoded secrets (S105/S106)
API_KEY = "sk-1234567890"
password = "hunter2"
# ❌ REJECTED: eval/exec (S307/S102)
result = eval(user_input)
exec(code_string)
# ❌ REJECTED: shell=True with untrusted input (S602)
subprocess.run(f"grep {user_input} file.txt", shell=True)
# ❌ REJECTED: Pickle with untrusted data (S301)
data = pickle.loads(untrusted_bytes)
# ❌ REJECTED: SSL verification disabled (S501)
requests.get(url, verify=False)
Context matters for security rules — a CLI tool invoked by the user has different trust boundaries than a web service. See /reviewing-python for false positive handling.
Ruff rules enforced:
| Rule | What it catches |
|---|---|
| S105 | Hardcoded password in variable assignment |
| S106 | Hardcoded password in function argument |
| S307 | Use of eval() |
| S102 | Use of exec() |
| S602 | subprocess call with shell=True |
| S301 | Use of pickle.loads |
| S501 | SSL verification disabled |
<resource_management>
# ❌ REJECTED: File not opened with context manager (SIM115)
f = open("file.txt")
data = f.read()
f.close()
# ✅ REQUIRED: Use context managers
with open("file.txt") as f:
data = f.read()
Ruff rules enforced:
| Rule | What it catches |
|---|---|
| SIM115 | Open file without context manager |
</resource_management>
<code_hygiene>
# ❌ REJECTED: Commented-out code (ERA001)
# result = old_function(x)
# if condition:
# do_something()
# ❌ REJECTED: Unused imports (F401)
import os # never used
# ❌ REJECTED: sys.path manipulation
import sys
sys.path.insert(0, str(Path(__file__).parent.parent))
Ruff rules enforced:
| Rule | What it catches |
|---|---|
| ERA001 | Commented-out code |
| F401 | Unused imports |
</code_hygiene>
<import_hygiene>
Depth Rules
| Depth | Syntax | Verdict | Rationale |
|---|---|---|---|
| Same dir | from . import x | OK | Module-internal, same package |
| 1 level | from .. import x | REVIEW | Is this truly module-internal? |
| 2+ levels | from ... import x | REJECT | Use absolute import — crosses package boundary |
Module-Internal vs. Infrastructure
Module-internal files live in the same package and move together. Relative imports are acceptable:
# ✅ ACCEPTABLE: Same package, files move together
from . import tokens
from .position import Position
Infrastructure is stable code that doesn't move when your feature moves. Must use absolute imports:
# ❌ REJECTED: Deep relative to infrastructure
from .......tests.helpers import create_tree
# ✅ REQUIRED: Absolute import
from myproject_testing.helpers import create_tree
Anti-Patterns
# ❌ REJECTED: sys.path manipulation
import sys
sys.path.insert(0, str(Path(__file__).parent.parent))
# ❌ REJECTED: Deep relative imports
from .....lib.utils import helper
# ❌ REJECTED: Assuming working directory
from lib.utils import helper # Only works if CWD is project root
Required Project Setup
1. Use src layout:
myproject/
├── src/
│ └── myproject/
│ ├── __init__.py
│ └── ...
├── tests/
│ ├── __init__.py
│ └── ...
└── pyproject.toml
2. Configure pyproject.toml:
[project]
name = "myproject"
[tool.setuptools.packages.find]
where = ["src"]
3. Install in editable mode:
uv pip install -e .
</import_hygiene>
<rejection_criteria_summary>
| Issue | Example | Rule |
|---|---|---|
Missing -> None on test | def test_foo(self): | ANN201 |
| Untyped fixture parameter | def test_foo(self, tmp_path): | ANN001 |
Missing -> None on init | def __init__(self, x: int): | ANN204 |
| Magic values in assertions | assert result == 42 | PLR2004 |
| Uppercase argument names | def __init__(self, WIDTH=8): | N803 |
| Shadowing builtins | def foo(input: str): | A002 |
Bare except: | except: pass | E722 |
| Swallowing exceptions | except Exception: pass | S110 |
| Hardcoded secrets | API_KEY = "sk-..." | S105 |
eval() / exec() | eval(user_input) | S307 |
shell=True | subprocess.run(cmd, shell=True) | S602 |
| Pickle with untrusted data | pickle.loads(data) | S301 |
| SSL disabled | requests.get(url, verify=False) | S501 |
| No context manager | f = open(...); f.close() | SIM115 |
| Old union syntax | Optional[X], Union[X, Y] | UP007 |
| Old generic syntax | List[str], Dict[str, int] | UP006 |
| Commented-out code | # old_function(x) | ERA001 |
| Unused imports | import os # never used | F401 |
| Deep relative imports | from ... import x | manual |
sys.path manipulation | sys.path.insert(0, ...) | manual |
Unqualified Any | def f(x: Any) -> Any: | mypy |
type: ignore no reason | x = foo() # type: ignore | manual |
</rejection_criteria_summary>