Python Style Guide (ROCm / TheRock)
Follows PEP 8, enforced by Ruff (amd-smi) or Black (TheRock) via pre-commit.
Source: TheRock python_style_guide.md
Core Principles
- Fail fast and loud — false positives over silent corruption
- When in doubt, raise an exception
Type Hints
- Type hints on all function signatures
- Syntax that is compatible with python 3.6.8+
- Never use
Any except in rare generic code
- Never use
Optional[T], Union[X, Y], List, Dict — use built-in equivalents
- Do NOT use
from __future__ import annotations
Extract Complex Types
- Type appears in multiple signatures →
NamedTuple or TypeAlias
- Dict/tuple with 3+ fields →
dataclass or NamedTuple
- If you document what tuple fields mean → use
NamedTuple
# Good
class KpackInfo(NamedTuple):
kpack_path: Path
size: int
kernel_count: int
# Bad
def get_info() -> tuple[Path, int, int]: ...
Dataclasses Over Tuples
Structured data with multiple fields → @dataclass or NamedTuple, not raw tuples.
Tuples OK for: simple pairs (x, y), stdlib unpacking, single-use immediate unpacking.
Function Calls & Arguments
- 3+ parameters or ambiguous types → use keyword arguments
- Multiple booleans → always use keyword arguments
# Good
result = build(amdgpu_family="gfx942", enable_testing=True, use_ccache=False)
# Bad
result = build("gfx942", True, False)
Code Organization
- All imports at top of file — inline only for documented circular deps (add comment explaining why)
- Scripts must have
if __name__ == "__main__": guard
- CLI scripts must use
argparse (not raw sys.argv)
- Access
argparse attrs directly: args.foo, not getattr(args, "foo", default)
- No duplicate code — extract to shared functions
- No magic numbers or fake estimates
- Methods < 30 lines, classes < 200 lines (ideally)
- If a class has 7+ responsibilities → split into focused classes
- God objects doing everything → multiple focused classes; 100+ line methods → extract helpers
Error Handling
| Rule |
Detail |
| Fail fast |
Never silently continue or produce incomplete results |
| Specific exceptions |
FileNotFoundError, subprocess.CalledProcessError — not bare except Exception |
| Preserve chains |
Always use from e when re-raising |
| Validate results |
After critical ops: check file exists, non-empty, expected size |
| No binutils timeouts |
Never timeout= on readelf, objcopy, etc. |
# Good — distinguish error conditions
try:
output = subprocess.check_output([readelf, "-S", str(file_path)])
except subprocess.CalledProcessError as e:
if e.returncode == 1:
return False # legitimate "not found"
raise RuntimeError(f"readelf failed: {e.output}") from e
except FileNotFoundError as e:
raise RuntimeError(f"readelf not found: {readelf}") from e
# Bad — swallows everything
try:
output = subprocess.check_output([readelf, "-S", str(file_path)])
except Exception:
return False
Filesystem & Paths
pathlib.Path everywhere — no os.path, no string concatenation for paths
- No assumptions about CWD — derive from
Path(__file__).resolve().parent
- No hard-coded project paths — use env vars,
tempfile, or relative to __file__
# Good
THIS_DIR = Path(__file__).resolve().parent
config = THIS_DIR / "config.json"
# Bad
config = Path("build_tools/config.json") # assumes CWD
config = Path("/home/user/project/config.json") # hard-coded
Performance
- Compile regexes at module level, not inside functions
- Check cheap conditions before expensive ones (magic bytes before subprocess)
- Cache expensive computations when called repeatedly
- Use generators for large datasets
Testing
- Verify fail-fast behavior with
pytest.raises
- Use real temp files over mocks for filesystem operations
- Mock only external dependencies (network, expensive tools)
- Integration tests should exercise the full path
amd-smi Python Conventions
Naming
py-interface/amdsmi_interface.py mirrors C API names (amdsmi_get_*, amdsmi_set_*)
amdsmi_cli/ uses snake_case methods
- Private functions/methods:
_ prefix
Project Layout
| Directory |
Purpose |
py-interface/ |
Python bindings — amdsmi_interface.py, amdsmi_wrapper.py, amdsmi_exception.py |
amdsmi_cli/ |
CLI tool — amdsmi_commands.py, amdsmi_parser.py, amdsmi_helpers.py |
Critical Python Files (high churn — review carefully)
amdsmi_cli/amdsmi_commands.py — CLI behavior regressions, output format changes
py-interface/amdsmi_interface.py — must stay in sync with C header amdsmi.h
py-interface/amdsmi_wrapper.py — generated bindings + library loader (see loader rules below)
amdsmi_cli/amdsmi_parser.py — argument parsing
Library Loader Rules (amdsmi_wrapper.py)
Changes to _detect_install_context, _build_candidate_paths, or _load_library are ❌ BLOCKING if they break system or pip install context. Verify:
Path(__file__).resolve() used correctly
- pip detection logic intact
_libraries['libamd_smi.so'] key preserved
- Sync with
tools/generator.py
Python Testing
- Tests must work with both system-installed and pip-installed amdsmi
- CLI tests live in
amdsmi_cli/
- Verify fail-fast behavior with
pytest.raises
- Use real temp files over mocks for filesystem operations
- Mock only external dependencies (network, expensive tools)
- Integration tests should exercise the full path
Review Checklist
When reviewing Python code, verify:
1---2name: amdsmi-python-style-guide3description: ROCm Python style guide based on TheRock. Use when: writing Python code, reviewing Python PRs, checking Python style, creating Python scripts, type hints, error handling patterns, pathlib usage, argparse CLI design.4---56# Python Style Guide (ROCm / TheRock)78Follows PEP 8, enforced by **Ruff** (amd-smi) or **Black** (TheRock) via pre-commit.9Source: [TheRock python_style_guide.md](https://github.com/ROCm/TheRock/blob/main/docs/development/style_guides/python_style_guide.md)1011## Core Principles1213- Fail fast and loud — false positives over silent corruption14- When in doubt, raise an exception1516---1718## Type Hints1920- Type hints on **all** function signatures21- Syntax that is compatible with python 3.6.8+22- **Never** use `Any` except in rare generic code23- **Never** use `Optional[T]`, `Union[X, Y]`, `List`, `Dict` — use built-in equivalents24- **Do NOT** use `from __future__ import annotations`2526### Extract Complex Types2728- Type appears in multiple signatures → `NamedTuple` or `TypeAlias`29- Dict/tuple with 3+ fields → `dataclass` or `NamedTuple`30- If you document what tuple fields mean → use `NamedTuple`3132```python33# Good34class KpackInfo(NamedTuple):35 kpack_path: Path36 size: int37 kernel_count: int3839# Bad40def get_info() -> tuple[Path, int, int]: ...41```4243### Dataclasses Over Tuples4445Structured data with multiple fields → `@dataclass` or `NamedTuple`, not raw tuples.46Tuples OK for: simple pairs `(x, y)`, stdlib unpacking, single-use immediate unpacking.4748---4950## Function Calls & Arguments5152- 3+ parameters or ambiguous types → **use keyword arguments**53- Multiple booleans → **always** use keyword arguments5455```python56# Good57result = build(amdgpu_family="gfx942", enable_testing=True, use_ccache=False)5859# Bad60result = build("gfx942", True, False)61```6263---6465## Code Organization6667- All imports at **top of file** — inline only for documented circular deps (add comment explaining why)68- Scripts must have `if __name__ == "__main__":` guard69- CLI scripts must use `argparse` (not raw `sys.argv`)70- Access `argparse` attrs directly: `args.foo`, not `getattr(args, "foo", default)`71- No duplicate code — extract to shared functions72- No magic numbers or fake estimates73- Methods < 30 lines, classes < 200 lines (ideally)74- If a class has 7+ responsibilities → split into focused classes75- God objects doing everything → multiple focused classes; 100+ line methods → extract helpers7677---7879## Error Handling8081| Rule | Detail |82|------|--------|83| Fail fast | Never silently continue or produce incomplete results |84| Specific exceptions | `FileNotFoundError`, `subprocess.CalledProcessError` — not bare `except Exception` |85| Preserve chains | Always use `from e` when re-raising |86| Validate results | After critical ops: check file exists, non-empty, expected size |87| No binutils timeouts | Never `timeout=` on `readelf`, `objcopy`, etc. |8889```python90# Good — distinguish error conditions91try:92 output = subprocess.check_output([readelf, "-S", str(file_path)])93except subprocess.CalledProcessError as e:94 if e.returncode == 1:95 return False # legitimate "not found"96 raise RuntimeError(f"readelf failed: {e.output}") from e97except FileNotFoundError as e:98 raise RuntimeError(f"readelf not found: {readelf}") from e99100# Bad — swallows everything101try:102 output = subprocess.check_output([readelf, "-S", str(file_path)])103except Exception:104 return False105```106107---108109## Filesystem & Paths110111- **`pathlib.Path` everywhere** — no `os.path`, no string concatenation for paths112- No assumptions about CWD — derive from `Path(__file__).resolve().parent`113- No hard-coded project paths — use env vars, `tempfile`, or relative to `__file__`114115```python116# Good117THIS_DIR = Path(__file__).resolve().parent118config = THIS_DIR / "config.json"119120# Bad121config = Path("build_tools/config.json") # assumes CWD122config = Path("/home/user/project/config.json") # hard-coded123```124125---126127## Performance128129- Compile regexes at **module level**, not inside functions130- Check cheap conditions before expensive ones (magic bytes before subprocess)131- Cache expensive computations when called repeatedly132- Use generators for large datasets133134---135136## Testing137138- Verify fail-fast behavior with `pytest.raises`139- Use real temp files over mocks for filesystem operations140- Mock only external dependencies (network, expensive tools)141- Integration tests should exercise the full path142143---144145## amd-smi Python Conventions146147### Naming148- `py-interface/amdsmi_interface.py` mirrors C API names (`amdsmi_get_*`, `amdsmi_set_*`)149- `amdsmi_cli/` uses `snake_case` methods150- Private functions/methods: `_` prefix151152### Project Layout153| Directory | Purpose |154|-----------|---------|155| `py-interface/` | Python bindings — `amdsmi_interface.py`, `amdsmi_wrapper.py`, `amdsmi_exception.py` |156| `amdsmi_cli/` | CLI tool — `amdsmi_commands.py`, `amdsmi_parser.py`, `amdsmi_helpers.py` |157158### Critical Python Files (high churn — review carefully)159- `amdsmi_cli/amdsmi_commands.py` — CLI behavior regressions, output format changes160- `py-interface/amdsmi_interface.py` — must stay in sync with C header `amdsmi.h`161- `py-interface/amdsmi_wrapper.py` — generated bindings + library loader (see loader rules below)162- `amdsmi_cli/amdsmi_parser.py` — argument parsing163164### Library Loader Rules (`amdsmi_wrapper.py`)165Changes to `_detect_install_context`, `_build_candidate_paths`, or `_load_library` are **❌ BLOCKING** if they break system or pip install context. Verify:166- `Path(__file__).resolve()` used correctly167- pip detection logic intact168- `_libraries['libamd_smi.so']` key preserved169- Sync with `tools/generator.py`170171### Python Testing172- Tests must work with **both** system-installed and pip-installed amdsmi173- CLI tests live in `amdsmi_cli/`174- Verify fail-fast behavior with `pytest.raises`175- Use real temp files over mocks for filesystem operations176- Mock only external dependencies (network, expensive tools)177- Integration tests should exercise the full path178179---180181## Review Checklist182183When reviewing Python code, verify:184185- [ ] Type hints on all functions — modern syntax, no `Any`186- [ ] No `from __future__ import annotations`187- [ ] Complex types extracted to `NamedTuple`/`dataclass`188- [ ] Specific exception handling — no bare `except Exception`189- [ ] Fail-fast — no silent `continue` on errors190- [ ] `from e` on all re-raises191- [ ] `pathlib.Path` for all filesystem ops192- [ ] No CWD assumptions, no hard-coded paths193- [ ] No magic numbers or fake estimates194- [ ] No duplicate code195- [ ] `argparse` for CLI, `__main__` guard for scripts196- [ ] Keyword args for complex function calls197- [ ] All imports at top (inline only with circular-dep comment)198- [ ] No timeouts on binutils199- [ ] Output validation after critical operations200- [ ] Methods < 30 lines, classes < 200 lines