Agent Skill
2/7/2026

standardizing-python

Python code standards enforced across all skills. Reference skill for type annotations, naming conventions, and linting rules.

S
simonheimlicher
1GitHub Stars
1Views
npx skills add simonheimlicher/spx-claude

SKILL.md

Namestandardizing-python
DescriptionPython 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:

RuleWhat it catches
ANN001Missing type annotation on parameter
ANN201Missing return type on public function
ANN204Missing 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:

  1. pytest rewrites assertions for better error messages
  2. Tests are never run with -O optimization
  3. 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:

RuleWhat it catches
mypy strictUnqualified 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:

RuleWhat it catches
UP006List, Dict, Tuple instead of list, dict, tuple
UP007Optional[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:

RuleWhat it catches
E722Bare except: clause
S110try-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:

RuleWhat it catches
S105Hardcoded password in variable assignment
S106Hardcoded password in function argument
S307Use of eval()
S102Use of exec()
S602subprocess call with shell=True
S301Use of pickle.loads
S501SSL verification disabled
</security>

<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:

RuleWhat it catches
SIM115Open 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:

RuleWhat it catches
ERA001Commented-out code
F401Unused imports

</code_hygiene>


<import_hygiene>

Depth Rules

DepthSyntaxVerdictRationale
Same dirfrom . import xOKModule-internal, same package
1 levelfrom .. import xREVIEWIs this truly module-internal?
2+ levelsfrom ... import xREJECTUse 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>

IssueExampleRule
Missing -> None on testdef test_foo(self):ANN201
Untyped fixture parameterdef test_foo(self, tmp_path):ANN001
Missing -> None on initdef __init__(self, x: int):ANN204
Magic values in assertionsassert result == 42PLR2004
Uppercase argument namesdef __init__(self, WIDTH=8):N803
Shadowing builtinsdef foo(input: str):A002
Bare except:except: passE722
Swallowing exceptionsexcept Exception: passS110
Hardcoded secretsAPI_KEY = "sk-..."S105
eval() / exec()eval(user_input)S307
shell=Truesubprocess.run(cmd, shell=True)S602
Pickle with untrusted datapickle.loads(data)S301
SSL disabledrequests.get(url, verify=False)S501
No context managerf = open(...); f.close()SIM115
Old union syntaxOptional[X], Union[X, Y]UP007
Old generic syntaxList[str], Dict[str, int]UP006
Commented-out code# old_function(x)ERA001
Unused importsimport os # never usedF401
Deep relative importsfrom ... import xmanual
sys.path manipulationsys.path.insert(0, ...)manual
Unqualified Anydef f(x: Any) -> Any:mypy
type: ignore no reasonx = foo() # type: ignoremanual

</rejection_criteria_summary>

Skills Info
Original Name:standardizing-pythonAuthor:simonheimlicher